@rallycry/conveyor-agent 11.0.5 → 11.0.7

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.
@@ -0,0 +1,464 @@
1
+ import {
2
+ redactToken
3
+ } from "./chunk-GL2DIQEQ.js";
4
+
5
+ // src/boot/graphify.ts
6
+ var USER_HOME_MOUNT = "/mnt/conveyor-users";
7
+ function computeGraphifyEnv(env, repoName, dirExists) {
8
+ if (env.CONVEYOR_GRAPHIFY_DISABLE === "1") {
9
+ return {};
10
+ }
11
+ const slug = env.CONVEYOR_GRAPHIFY_SLUG || repoName;
12
+ if (!slug) {
13
+ return {};
14
+ }
15
+ const primaryRoot = `${USER_HOME_MOUNT}/users/_shared/graphify`;
16
+ const legacyRoot = `${USER_HOME_MOUNT}/_shared/graphify`;
17
+ let sharedRoot = env.CONVEYOR_GRAPHIFY_SHARED_ROOT;
18
+ if (!sharedRoot) {
19
+ sharedRoot = dirExists(primaryRoot) || !dirExists(legacyRoot) ? primaryRoot : legacyRoot;
20
+ }
21
+ const dir = env.CONVEYOR_GRAPHIFY_DIR || `${sharedRoot}/${slug}/latest`;
22
+ const graph = env.CONVEYOR_GRAPHIFY_GRAPH || `${dir}/graph.json`;
23
+ return {
24
+ CONVEYOR_GRAPHIFY_SLUG: slug,
25
+ CONVEYOR_GRAPHIFY_SHARED_ROOT: sharedRoot,
26
+ CONVEYOR_GRAPHIFY_DIR: dir,
27
+ CONVEYOR_GRAPHIFY_GRAPH: graph
28
+ };
29
+ }
30
+
31
+ // src/runner/session-runner-helpers.ts
32
+ import { readFileSync } from "fs";
33
+ import { dirname, join } from "path";
34
+ import { fileURLToPath } from "url";
35
+ function mapChatHistory(messages) {
36
+ if (!messages) return [];
37
+ return messages.map((m) => ({
38
+ id: m.id,
39
+ role: m.role ?? "user",
40
+ content: m.content ?? "",
41
+ userId: m.userId,
42
+ userName: m.user?.name ?? void 0,
43
+ createdAt: m.createdAt,
44
+ ...m.source ? { source: m.source } : {},
45
+ ...m.files && m.files.length > 0 ? {
46
+ files: m.files.map((f) => ({
47
+ fileId: f.id,
48
+ fileName: f.fileName,
49
+ mimeType: f.mimeType,
50
+ fileSize: f.fileSize,
51
+ downloadUrl: f.downloadUrl ?? "",
52
+ content: f.content,
53
+ contentEncoding: f.contentEncoding
54
+ }))
55
+ } : {}
56
+ }));
57
+ }
58
+ function readAgentVersion() {
59
+ try {
60
+ const here = dirname(fileURLToPath(import.meta.url));
61
+ for (const rel of ["../package.json", "../../package.json"]) {
62
+ try {
63
+ const pkg = JSON.parse(readFileSync(join(here, rel), "utf-8"));
64
+ if (pkg.version) return pkg.version;
65
+ } catch {
66
+ }
67
+ }
68
+ } catch {
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // src/boot/workspace-extras.ts
74
+ import {
75
+ appendFileSync,
76
+ cpSync,
77
+ existsSync,
78
+ lstatSync,
79
+ mkdirSync,
80
+ readdirSync,
81
+ readFileSync as readFileSync2,
82
+ readlinkSync,
83
+ renameSync,
84
+ rmSync,
85
+ statSync,
86
+ symlinkSync
87
+ } from "fs";
88
+ import { dirname as dirname2, join as join2 } from "path";
89
+ import { fileURLToPath as fileURLToPath2 } from "url";
90
+ function errText(err) {
91
+ return redactToken(err instanceof Error ? err.message : String(err));
92
+ }
93
+ function isDir(path) {
94
+ try {
95
+ return statSync(path).isDirectory();
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function isSymlink(path) {
101
+ try {
102
+ return lstatSync(path).isSymbolicLink();
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ function isDanglingSymlink(path) {
108
+ return !existsSync(path) && isSymlink(path);
109
+ }
110
+ var GRAPHIFY_BUNDLE_FILES = [
111
+ "graph.json",
112
+ "GRAPH_REPORT.md",
113
+ "manifest.json",
114
+ ".graphify_analysis.json",
115
+ ".graphify_labels.json",
116
+ "publish-manifest.json",
117
+ "cost.json"
118
+ ];
119
+ function bindGraphifyBundle(workspace, graphifyEnv, log) {
120
+ const slug = graphifyEnv.CONVEYOR_GRAPHIFY_SLUG;
121
+ const sourceDir = graphifyEnv.CONVEYOR_GRAPHIFY_DIR;
122
+ const graphFile = graphifyEnv.CONVEYOR_GRAPHIFY_GRAPH;
123
+ if (!slug || !sourceDir || !graphFile) return;
124
+ if (!existsSync(graphFile)) {
125
+ log.info(`[boot] Graphify bundle not found for '${slug}' at ${sourceDir}`);
126
+ return;
127
+ }
128
+ const targetDir = join2(workspace, "graphify-out");
129
+ try {
130
+ mkdirSync(targetDir, { recursive: true });
131
+ } catch {
132
+ log.warn(`[boot] WARN: unable to create graphify-out at ${targetDir}`);
133
+ return;
134
+ }
135
+ for (const rel of GRAPHIFY_BUNDLE_FILES) {
136
+ const source = join2(sourceDir, rel);
137
+ const target = join2(targetDir, rel);
138
+ if (!existsSync(source)) continue;
139
+ if (existsSync(target) || isSymlink(target)) continue;
140
+ try {
141
+ symlinkSync(source, target);
142
+ } catch {
143
+ }
144
+ }
145
+ log.info(`[boot] Bound graphify bundle '${slug}' into ${targetDir}`);
146
+ }
147
+ async function ensureGrimoireSubmodule(workspace, githubToken, git, log) {
148
+ try {
149
+ if (!existsSync(join2(workspace, ".gitmodules"))) return;
150
+ let paths = "";
151
+ try {
152
+ ({ stdout: paths } = await git(
153
+ ["config", "--file", ".gitmodules", "--get-regexp", String.raw`submodule\..*\.path`],
154
+ { cwd: workspace, timeoutMs: 1e4 }
155
+ ));
156
+ } catch {
157
+ return;
158
+ }
159
+ if (!paths.split("\n").some((line) => line.trim().endsWith(".claude/grimoire"))) return;
160
+ if (isDir(join2(workspace, ".claude", "grimoire", "skills"))) return;
161
+ if (!githubToken) {
162
+ log.warn("[boot] WARN: grimoire submodule absent and no token to fetch it");
163
+ return;
164
+ }
165
+ try {
166
+ await git(
167
+ [
168
+ "-c",
169
+ `url.https://x-access-token:${githubToken}@github.com/.insteadOf=https://github.com/`,
170
+ "submodule",
171
+ "update",
172
+ "--init",
173
+ ".claude/grimoire"
174
+ ],
175
+ { cwd: workspace, timeoutMs: 3e5 }
176
+ );
177
+ log.info("[boot] Initialized grimoire submodule");
178
+ } catch (err) {
179
+ log.warn(
180
+ `[boot] WARN: grimoire submodule init failed (baked pods may lack rc-* skills): ${errText(err)}`
181
+ );
182
+ }
183
+ } catch (err) {
184
+ log.warn(`[boot] WARN: grimoire submodule check failed: ${errText(err)}`);
185
+ }
186
+ }
187
+ function excludeLinkedSkills(workspace, names, log) {
188
+ if (names.length === 0) return;
189
+ const infoDir = join2(workspace, ".git", "info");
190
+ if (!isDir(join2(workspace, ".git"))) return;
191
+ try {
192
+ mkdirSync(infoDir, { recursive: true });
193
+ const excludePath = join2(infoDir, "exclude");
194
+ const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf8") : "";
195
+ const missing = names.map((name) => `.claude/skills/${name}`).filter((line) => !existing.split("\n").includes(line));
196
+ if (missing.length === 0) return;
197
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
198
+ appendFileSync(excludePath, `${prefix}${missing.join("\n")}
199
+ `);
200
+ } catch (err) {
201
+ log.warn(`[boot] WARN: unable to exclude grimoire skill links: ${errText(err)}`);
202
+ }
203
+ }
204
+ function symlinkTargetsGrimoire(path) {
205
+ try {
206
+ return readlinkSync(path).includes(join2("grimoire", "skills"));
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+ function linkGrimoireSkills(workspace, log) {
212
+ const sourceDir = join2(workspace, ".claude", "grimoire", "skills");
213
+ const targetDir = join2(workspace, ".claude", "skills");
214
+ if (!isDir(sourceDir)) return;
215
+ try {
216
+ mkdirSync(targetDir, { recursive: true });
217
+ } catch {
218
+ log.warn(`[boot] WARN: unable to create ${targetDir}; skipping grimoire skill links`);
219
+ return;
220
+ }
221
+ const linkedNames = [];
222
+ let names = [];
223
+ try {
224
+ names = readdirSync(sourceDir);
225
+ } catch {
226
+ return;
227
+ }
228
+ for (const name of names) {
229
+ if (!isDir(join2(sourceDir, name))) continue;
230
+ const target = join2(targetDir, name);
231
+ if (existsSync(target) && !isSymlink(target)) {
232
+ log.warn(`[boot] WARN: ${target} exists and is not a symlink; skipping`);
233
+ continue;
234
+ }
235
+ if (isSymlink(target) && !symlinkTargetsGrimoire(target)) continue;
236
+ try {
237
+ rmSync(target, { force: true });
238
+ symlinkSync(join2("..", "grimoire", "skills", name), target);
239
+ linkedNames.push(name);
240
+ } catch {
241
+ }
242
+ }
243
+ excludeLinkedSkills(workspace, linkedNames, log);
244
+ log.info(`[boot] Linked ${linkedNames.length} grimoire skills into ${targetDir}`);
245
+ }
246
+ function bundledSkillsDir(moduleUrl = import.meta.url) {
247
+ try {
248
+ const dir = join2(dirname2(fileURLToPath2(moduleUrl)), "..", "skills");
249
+ return isDir(dir) ? dir : null;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ var EMPTY_MATERIALIZE = { filled: [], shadowed: [], stale: [] };
255
+ async function hideShadowedPins(workspace, names, git, log) {
256
+ if (names.length === 0) return;
257
+ const paths = names.map((name) => `.claude/skills/${name}`);
258
+ if (!git) {
259
+ log.warn(
260
+ `[boot] WARN: replaced skill pin(s) ${names.join(", ")} stay visible to git (no git runner supplied)`
261
+ );
262
+ return;
263
+ }
264
+ try {
265
+ await git(["update-index", "--assume-unchanged", ...paths], {
266
+ cwd: workspace,
267
+ timeoutMs: 1e4
268
+ });
269
+ } catch (err) {
270
+ log.warn(`[boot] WARN: unable to hide replaced skill pins from git: ${errText(err)}`);
271
+ }
272
+ }
273
+ function readlinkOr(path) {
274
+ try {
275
+ return readlinkSync(path);
276
+ } catch {
277
+ return "unknown target";
278
+ }
279
+ }
280
+ function fillOneSkill(sourceDir, targetDir, name, log) {
281
+ if (!isDir(join2(sourceDir, name))) return null;
282
+ const target = join2(targetDir, name);
283
+ const dangling = isDanglingSymlink(target);
284
+ if (!dangling && (existsSync(target) || isSymlink(target))) return null;
285
+ if (!dangling) {
286
+ try {
287
+ cpSync(join2(sourceDir, name), target, { recursive: true, dereference: true });
288
+ return "filled";
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+ log.warn(
294
+ `[boot] WARN: repo skill '${name}' pointed at a missing target (${readlinkOr(target)}); shadowing it with this agent build's bundled copy`
295
+ );
296
+ const staging = join2(targetDir, `.${name}.conveyor-staging`);
297
+ try {
298
+ rmSync(staging, { recursive: true, force: true });
299
+ cpSync(join2(sourceDir, name), staging, { recursive: true, dereference: true });
300
+ rmSync(target, { force: true });
301
+ renameSync(staging, target);
302
+ return "shadowed";
303
+ } catch (err) {
304
+ try {
305
+ rmSync(staging, { recursive: true, force: true });
306
+ } catch {
307
+ }
308
+ log.warn(
309
+ `[boot] WARN: could not replace dangling skill pin '${name}' (${errText(err)}) \u2014 it stays unusable in this session's skill registry`
310
+ );
311
+ return "stale";
312
+ }
313
+ }
314
+ function reportStalePins(targetDir, bundled, log) {
315
+ let present = [];
316
+ try {
317
+ present = readdirSync(targetDir);
318
+ } catch {
319
+ return [];
320
+ }
321
+ const stale = present.filter(
322
+ (name) => !bundled.has(name) && isDanglingSymlink(join2(targetDir, name))
323
+ );
324
+ for (const name of stale) {
325
+ log.warn(
326
+ `[boot] WARN: repo skill '${name}' is a dangling pin and this agent build has no bundled copy \u2014 it will not appear in the session's skill registry`
327
+ );
328
+ }
329
+ return stale;
330
+ }
331
+ async function materializeBundledSkills(workspace, log, sourceDirOverride, git) {
332
+ const sourceDir = sourceDirOverride === void 0 ? bundledSkillsDir() : sourceDirOverride;
333
+ if (!sourceDir) {
334
+ log.warn(
335
+ "[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."
336
+ );
337
+ return EMPTY_MATERIALIZE;
338
+ }
339
+ const targetDir = join2(workspace, ".claude", "skills");
340
+ try {
341
+ mkdirSync(targetDir, { recursive: true });
342
+ } catch {
343
+ log.warn(`[boot] WARN: unable to create ${targetDir}; skipping bundled skills`);
344
+ return EMPTY_MATERIALIZE;
345
+ }
346
+ const filled = [];
347
+ const shadowed = [];
348
+ const unreplaced = [];
349
+ let names = [];
350
+ try {
351
+ names = readdirSync(sourceDir);
352
+ } catch {
353
+ return EMPTY_MATERIALIZE;
354
+ }
355
+ for (const name of names) {
356
+ const outcome = fillOneSkill(sourceDir, targetDir, name, log);
357
+ if (outcome === "filled") filled.push(name);
358
+ else if (outcome === "shadowed") shadowed.push(name);
359
+ else if (outcome === "stale") unreplaced.push(name);
360
+ }
361
+ const stale = [...unreplaced, ...reportStalePins(targetDir, new Set(names), log)];
362
+ excludeLinkedSkills(workspace, [...filled, ...shadowed], log);
363
+ await hideShadowedPins(workspace, shadowed, git, log);
364
+ log.info(
365
+ `[boot] Materialized ${filled.length} bundled skills into ${targetDir}` + (shadowed.length > 0 ? `; replaced ${shadowed.length} dangling pin(s)` : "") + (stale.length > 0 ? `; ${stale.length} dangling pin(s) left unusable` : "")
366
+ );
367
+ return { filled, shadowed, stale };
368
+ }
369
+ function refreshSkillsAfterCheckout(workspace, git, log, sourceDirOverride) {
370
+ linkGrimoireSkills(workspace, log);
371
+ return materializeBundledSkills(workspace, log, sourceDirOverride, git);
372
+ }
373
+ async function neutralizeGrimoirePointer(workspace, git, log) {
374
+ if (!existsSync(join2(workspace, ".claude", "grimoire"))) return;
375
+ try {
376
+ await git(["update-index", "--assume-unchanged", ".claude/grimoire"], {
377
+ cwd: workspace,
378
+ timeoutMs: 1e4
379
+ });
380
+ log.info("[boot] Marked .claude/grimoire assume-unchanged (submodule pointer drift is noise)");
381
+ } catch (err) {
382
+ log.warn(`[boot] WARN: grimoire assume-unchanged failed: ${errText(err)}`);
383
+ }
384
+ }
385
+ function parseReferenceRepos(refsJson) {
386
+ let parsed;
387
+ try {
388
+ parsed = JSON.parse(refsJson);
389
+ } catch {
390
+ return [];
391
+ }
392
+ if (!Array.isArray(parsed)) return [];
393
+ const refs = [];
394
+ for (const entry of parsed) {
395
+ if (typeof entry !== "object" || entry === null) continue;
396
+ const raw = entry;
397
+ const str = (key) => typeof raw[key] === "string" ? raw[key] : "";
398
+ const ref = {
399
+ slug: str("slug"),
400
+ owner: str("owner"),
401
+ name: str("name"),
402
+ branch: str("branch") || "main",
403
+ token: str("token")
404
+ };
405
+ if (ref.token && ref.slug && ref.owner && ref.name) refs.push(ref);
406
+ }
407
+ return refs;
408
+ }
409
+ async function runPreReadyBinds(workspace, bundle, git, log) {
410
+ const graphifyEnv = computeGraphifyEnv(process.env, bundle.gitPlan.repoName, isDir);
411
+ bindGraphifyBundle(workspace, graphifyEnv, log);
412
+ await ensureGrimoireSubmodule(workspace, bundle.githubToken ?? "", git, log);
413
+ linkGrimoireSkills(workspace, log);
414
+ await materializeBundledSkills(workspace, log, void 0, git);
415
+ await neutralizeGrimoirePointer(workspace, git, log);
416
+ }
417
+ async function cloneReferenceRepos(refsJson, git, log, referencesDir = "/workspaces/references") {
418
+ if (!refsJson) return;
419
+ const refs = parseReferenceRepos(refsJson);
420
+ if (refs.length === 0) return;
421
+ try {
422
+ mkdirSync(referencesDir, { recursive: true });
423
+ } catch {
424
+ return;
425
+ }
426
+ for (const ref of refs) {
427
+ const dest = join2(referencesDir, ref.slug);
428
+ if (existsSync(join2(dest, ".git"))) continue;
429
+ try {
430
+ await git(
431
+ [
432
+ "clone",
433
+ "--depth",
434
+ "1",
435
+ "--single-branch",
436
+ "--branch",
437
+ ref.branch,
438
+ `https://x-access-token:${ref.token}@github.com/${ref.owner}/${ref.name}.git`,
439
+ dest
440
+ ],
441
+ { timeoutMs: 6e5 }
442
+ );
443
+ try {
444
+ await git(
445
+ ["remote", "set-url", "origin", `https://github.com/${ref.owner}/${ref.name}.git`],
446
+ { cwd: dest, timeoutMs: 1e4 }
447
+ );
448
+ } catch {
449
+ }
450
+ log.info(`[boot] cloned reference ${ref.slug} (${ref.owner}/${ref.name}@${ref.branch})`);
451
+ } catch {
452
+ log.warn(`[boot] WARN: reference clone failed: ${ref.slug}`);
453
+ }
454
+ }
455
+ }
456
+
457
+ export {
458
+ computeGraphifyEnv,
459
+ refreshSkillsAfterCheckout,
460
+ runPreReadyBinds,
461
+ cloneReferenceRepos,
462
+ mapChatHistory,
463
+ readAgentVersion
464
+ };
@@ -138,6 +138,24 @@ var WorkbenchClient = class {
138
138
  );
139
139
  });
140
140
  }
141
+ /** Ask the workbench — which owns the repo filesystem in split mode — to
142
+ * re-run the skill floor after the agent's authoritative task-branch
143
+ * checkout. A daemon without a provider answers an empty result. */
144
+ refreshSkills() {
145
+ return new Promise((resolve, reject) => {
146
+ let result = null;
147
+ this.open(
148
+ { op: "refreshSkills" },
149
+ (frame) => {
150
+ if (frame.t === "refreshSkills") result = frame;
151
+ },
152
+ (error) => {
153
+ if (result) resolve(result);
154
+ else reject(error ?? new WorkbenchError("connection closed before refreshSkills"));
155
+ }
156
+ );
157
+ });
158
+ }
141
159
  /** Mirrors setup/commands.ts runSetupCommand (shell form, streamed output,
142
160
  * abort → graceful term-group kill, non-zero exit rejects). */
143
161
  runSetupCommand(cmd, cwd, onOutput, signal) {
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-URYBO32C.js";
10
+ } from "./chunk-43XITDZP.js";
11
11
  import {
12
12
  DEFAULT_SONNET_MODEL,
13
13
  PtyHarness,
@@ -27,13 +27,13 @@ import {
27
27
  resolveTuiKindFromEnv,
28
28
  runUsageProbe,
29
29
  sampleKeyUsage
30
- } from "./chunk-J2NPWBVZ.js";
31
- import "./chunk-XORJ6SII.js";
30
+ } from "./chunk-NFNB2C35.js";
31
+ import "./chunk-SR66HQKB.js";
32
32
  import {
33
33
  inheritedEnv,
34
34
  resolvePtySpawn,
35
35
  sessionTempBase
36
- } from "./chunk-W6VILPQE.js";
36
+ } from "./chunk-JG3VMWRQ.js";
37
37
  import {
38
38
  AgentConnection,
39
39
  CodespacePortVisibility,
@@ -44,12 +44,12 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-SLUBOIYE.js";
47
+ } from "./chunk-APP6Y2D7.js";
48
48
  import "./chunk-GL2DIQEQ.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-7P6QZHXZ.js";
51
51
  import "./chunk-IA45XHOA.js";
52
- import "./chunk-EXQ6AHOY.js";
52
+ import "./chunk-TOTCEWJD.js";
53
53
  import "./chunk-KMB3BU4S.js";
54
54
  import "./chunk-6Q6LQBWO.js";
55
55
 
@@ -1201,7 +1201,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1201
1201
 
1202
1202
  // src/cli.ts
1203
1203
  if (process.argv[2] === "boot") {
1204
- const { runBoot } = await import("./boot-7XH4FFLO.js");
1204
+ const { runBoot } = await import("./boot-SQKXI5NY.js");
1205
1205
  process.exit(await runBoot(process.argv.slice(3)));
1206
1206
  }
1207
1207
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1303,7 +1303,7 @@ process.on("unhandledRejection", (reason) => {
1303
1303
  process.exit(1);
1304
1304
  });
1305
1305
  if (process.env.CONVEYOR_MODE === "workbench") {
1306
- const { startWorkbenchServer } = await import("./server-2Z5V3IPA.js");
1306
+ const { startWorkbenchServer } = await import("./server-2IZBKD3Q.js");
1307
1307
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1308
1308
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1309
1309
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
@@ -1454,7 +1454,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1454
1454
  process.exit(1);
1455
1455
  }
1456
1456
  if (CONVEYOR_MODE === "serving") {
1457
- const { runServingSession } = await import("./serve-boot-B4BIZMNB.js");
1457
+ const { runServingSession } = await import("./serve-boot-ESZKFN5M.js");
1458
1458
  exitContext.runnerMode = "serving";
1459
1459
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1460
1460
  const outcome = await runServingSession({
@@ -4,7 +4,7 @@ import {
4
4
  WorkbenchError,
5
5
  getWorkbenchClient,
6
6
  resetWorkbenchClient
7
- } from "./chunk-EXQ6AHOY.js";
7
+ } from "./chunk-TOTCEWJD.js";
8
8
  import "./chunk-KMB3BU4S.js";
9
9
  import "./chunk-6Q6LQBWO.js";
10
10
  export {
package/dist/index.d.ts CHANGED
@@ -131,6 +131,10 @@ declare class AgentConnection {
131
131
  private earlyProbeUsage;
132
132
  private ptyInputCallback;
133
133
  private ptyResizeCallback;
134
+ /** Epoch ms of the last relayed keystroke, or null before any input. The
135
+ * question-grace expiry reads it to defer un-parking while a human is
136
+ * actively typing into the Connected-TUI (they may be mid-answer). */
137
+ private _lastPtyInputAt;
134
138
  constructor(config: AgentConnectionConfig);
135
139
  get sessionId(): string;
136
140
  get connected(): boolean;
@@ -282,6 +286,8 @@ declare class AgentConnection {
282
286
  * just stays on the relay transport.
283
287
  */
284
288
  reportPtyStream(port: number | null): void;
289
+ /** Epoch ms of the last relayed TUI keystroke, or null before any. */
290
+ get lastPtyInputAt(): number | null;
285
291
  /** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
286
292
  onPtyInput(handler: (data: string) => void): () => void;
287
293
  /** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
@@ -729,6 +735,18 @@ declare class SessionRunner {
729
735
  /** Guards the one-shot `agent_live` boot-milestone report — fired on the
730
736
  * first harness event (createQueryBridge's onEvent hook). */
731
737
  private agentLiveReported;
738
+ /** Epoch ms since a TUI questionnaire has been parked on a human with no
739
+ * answer; null when none is pending. While set (and within the grace +
740
+ * slack bound) `resolveLoopStatus` reports `waiting`, which reaches the API
741
+ * as `active` and keeps the workspace activity clock fresh — the pod must
742
+ * not be slept out from under an unanswered question. Deliberately KEPT
743
+ * when the turn ends with the questionnaire still parked (the Shape-B
744
+ * silent death this hold exists for). */
745
+ private questionPendingSince;
746
+ /** Single-shot unref'd timer driving `handleQuestionGraceExpiry`. The
747
+ * liveness hold above is bounded by timestamp arithmetic, so a lost timer
748
+ * still expires the hold — it just skips the un-park injection. */
749
+ private questionGraceTimer;
732
750
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks, deps?: SessionRunnerDependencies);
733
751
  get state(): AgentRunnerStatus;
734
752
  /**
@@ -833,6 +851,19 @@ declare class SessionRunner {
833
851
  run(): Promise<void>;
834
852
  /** Convenience wrapper: connect() then run(). */
835
853
  start(): Promise<void>;
854
+ /**
855
+ * Re-run the skill floor against the tree `ensureOnTaskBranch` just produced.
856
+ *
857
+ * Split-mode pods keep the repo in the WORKBENCH container, so this routes
858
+ * over the launcher: running it in-process here would materialize skills into
859
+ * the AGENT container's own copy of /workspaces/repo — still on the base
860
+ * branch, already complete — and look like it worked while Claude spawned on
861
+ * the untouched real tree. Codespaces/local (no workbench) run it directly.
862
+ *
863
+ * Best-effort by design: a failure warns and boot continues, exactly like the
864
+ * pre-ready binds it repeats.
865
+ */
866
+ private refreshSkillsForCheckout;
836
867
  private coreLoop;
837
868
  /**
838
869
  * Handle dormant-after-completed idle. Returns true if a critical message
@@ -1016,6 +1047,29 @@ declare class SessionRunner {
1016
1047
  * for why an unknown status still fails open to `active`.
1017
1048
  */
1018
1049
  private resolveLoopStatus;
1050
+ /**
1051
+ * Question-pending transitions from the event loop. `armed` starts the
1052
+ * bounded liveness hold + the grace timer; `answered` clears both.
1053
+ * `turn_ended` — the turn's result arrived with the questionnaire still
1054
+ * parked — deliberately KEEPS them: that is the Shape-B silent death
1055
+ * (idle heartbeats stop bumping the activity clock and the reconciler
1056
+ * sleeps the pod minutes later) this hold exists to prevent.
1057
+ */
1058
+ private handleUserQuestionSignal;
1059
+ private armQuestionGraceTimer;
1060
+ private clearQuestionHold;
1061
+ /**
1062
+ * The grace expired with the questionnaire still unanswered. Un-park the
1063
+ * agent by injecting the timeout prompt as a superseding system message —
1064
+ * the proven supersede path (mid-turn: queue + queryBridge.stop(), teardown
1065
+ * kills the parked dialog and the respawn `--resume`s cleanly; idle: the
1066
+ * inputResolver picks it up). Never synthetic keypresses: Enter would
1067
+ * fabricate an answer and Esc handling is CLI-version-dependent.
1068
+ *
1069
+ * A human actively typing in the Connected-TUI defers the expiry — they may
1070
+ * be mid-answer, and superseding would yank the dialog out from under them.
1071
+ */
1072
+ private handleQuestionGraceExpiry;
1019
1073
  /** Mirror the resolved status into the loop-lag buffer so the starvation-proof
1020
1074
  * heartbeat worker reports it too. Returns what it wrote. */
1021
1075
  private refreshLoopStatus;
@@ -1055,9 +1109,31 @@ interface AgentRunnerConfig {
1055
1109
  */
1056
1110
  packExecution?: _project_shared.PackExecution;
1057
1111
  }
1112
+ /**
1113
+ * Question-pending transitions surfaced to the runner by the event loop
1114
+ * (`applyQuestionTransitions` in execution/event-processor.ts):
1115
+ * - `armed` — the PTY hook observed an AskUserQuestion; a questionnaire is
1116
+ * parked on a human.
1117
+ * - `answered` — the questionnaire resolved (PostToolUse envelope, or a new
1118
+ * assistant record without the question block).
1119
+ * - `turn_ended` — the turn's `result` arrived while the questionnaire was
1120
+ * still parked. The dialog is still on screen with nobody answering — the
1121
+ * silent-death shape the question grace hold exists for — so the runner
1122
+ * must KEEP its pending hold rather than clear it.
1123
+ */
1124
+ type UserQuestionSignal = {
1125
+ kind: "armed";
1126
+ questionText?: string;
1127
+ } | {
1128
+ kind: "answered";
1129
+ } | {
1130
+ kind: "turn_ended";
1131
+ };
1058
1132
  interface AgentRunnerCallbacks {
1059
1133
  onEvent: (event: Record<string, unknown>) => void | Promise<void>;
1060
1134
  onStatusChange: (status: string) => void | Promise<void>;
1135
+ /** Optional — fired on question-pending transitions (see UserQuestionSignal). */
1136
+ onUserQuestion?: (signal: UserQuestionSignal) => void;
1061
1137
  }
1062
1138
 
1063
1139
  interface GithubTokenFileSync {