@rallycry/conveyor-agent 11.0.25 → 11.0.27

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.
@@ -74,6 +74,7 @@ import {
74
74
  hasTaskPlan,
75
75
  hasUncommittedChanges,
76
76
  hasUnpushedCommits,
77
+ isCapabilityServableMime,
77
78
  isCodexReasoningEffort,
78
79
  isPlaceholderLocator,
79
80
  locatorMatchesContent,
@@ -91,7 +92,7 @@ import {
91
92
  statWorkspacePath,
92
93
  updateRemoteToken,
93
94
  verifyGitCredential
94
- } from "./chunk-BZZ4B37Q.js";
95
+ } from "./chunk-PBSFLQA7.js";
95
96
  import {
96
97
  registerBootMilestoneSocketFallback,
97
98
  reportBootMilestone
@@ -5312,7 +5313,16 @@ function wrapBridgeWithDirectStream(inner, reporter, options = {}) {
5312
5313
 
5313
5314
  // src/execution/query-executor.ts
5314
5315
  import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
5315
- import { existsSync, readFileSync as readFileSync3, renameSync as renameSync2, truncateSync } from "fs";
5316
+ import {
5317
+ closeSync,
5318
+ existsSync,
5319
+ openSync,
5320
+ readFileSync as readFileSync3,
5321
+ readSync,
5322
+ renameSync as renameSync2,
5323
+ statSync as statSync2,
5324
+ truncateSync
5325
+ } from "fs";
5316
5326
 
5317
5327
  // src/execution/chat-instructions.ts
5318
5328
  function buildChatInstructions(context, scenario, newMessages) {
@@ -5431,6 +5441,20 @@ function formatChatFile(file) {
5431
5441
  }
5432
5442
  return [`[Attached: ${file.fileName} (${file.mimeType}${sizeStr})]`];
5433
5443
  }
5444
+ function formatParentTaskFiles(parent) {
5445
+ const lines = [
5446
+ `
5447
+ ## Files on the parent card ${parent.slug}`,
5448
+ `These are attached to the parent card "${parent.title}", not to this one. Read one with get_attachment(fileId, task_id: "${parent.slug}"); list_task_files(task_id: "${parent.slug}") shows the current set.`
5449
+ ];
5450
+ for (const file of parent.files) {
5451
+ const size = formatFileSize(file.fileSize);
5452
+ lines.push(
5453
+ `- **${file.fileName}** (${file.mimeType}${size ? `, ${size}` : ""}) fileId: ${file.fileId}`
5454
+ );
5455
+ }
5456
+ return lines;
5457
+ }
5434
5458
  function formatTaskFile(file) {
5435
5459
  if (file.content && file.contentEncoding === "utf-8") {
5436
5460
  return [`
@@ -5783,7 +5807,7 @@ var PACK_RUNNER_FOOTER = [
5783
5807
  ``,
5784
5808
  `Your turn output appears ONLY in the live agent terminal \u2014 it is NOT posted to the task chat. The team does NOT see your replies unless you post them.`,
5785
5809
  `Use post_to_chat (omit task_id \u2192 this task's chat) to report which child you are on, status as you work the pack, and any blocker or escalation the team needs to act on.`,
5786
- `Pass task_id only to message a DIFFERENT task's chat (e.g. a child task).`,
5810
+ `Pass task_id only to reach a DIFFERENT card: a child task's chat on post_to_chat and read_task_chat, or a child's files on list_task_files and get_attachment.`,
5787
5811
  `Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`,
5788
5812
  ``,
5789
5813
  `If a Conveyor tool call fails or reports the MCP server is unavailable/disconnected, this is almost always a transient socket reconnect \u2014 RETRY the same call; it will succeed once the connection re-establishes. A tool error is NOT a reason to go idle or stop the pack. (When a turn may end is the pack path's call \u2014 see its goal and finish line.)`,
@@ -6386,7 +6410,7 @@ ${context.agentInstructions}`);
6386
6410
  Your turn output appears ONLY in the live agent terminal \u2014 it is NOT posted to the task chat. The team does NOT see your replies unless you post them.`,
6387
6411
  `Use post_to_chat (omit task_id \u2192 your own task's chat) to share meaningful status as you work, a summary when you finish, and any blocker or question you need a human to answer.`,
6388
6412
  `A question you need answered goes through AskUserQuestion \u2014 that is what pages a person. post_to_chat with milestone: "blocked" records that you are parked and does not page anyone.`,
6389
- `Pass task_id only to message a DIFFERENT task's chat (e.g. a parent task via get_task).`,
6413
+ `Pass task_id only to reach a DIFFERENT card: a child task's chat on post_to_chat and read_task_chat, or any card in this project's files on list_task_files and get_attachment (a pack parent's source documents, for example). Omit it for your own card.`,
6390
6414
  `Use read_task_chat only if you need to re-read earlier messages beyond the chat context above.`,
6391
6415
  `
6392
6416
  If a Conveyor tool call fails or reports the MCP server is unavailable/disconnected, this is almost always a transient socket reconnect \u2014 simply RETRY the same call and it will succeed once the connection re-establishes. Do NOT stop work, go idle, or treat a tool error as a reason to end your turn. Keep making progress on the code; only post_to_chat to ask for a human if a tool keeps failing across several retries over a sustained period.`,
@@ -6561,6 +6585,9 @@ ${truncatePlanForPrompt(context.plan)}`);
6561
6585
  parts.push(...formatTaskFile(file));
6562
6586
  }
6563
6587
  }
6588
+ if (context.parentTaskFiles && context.parentTaskFiles.files.length > 0) {
6589
+ parts.push(...formatParentTaskFiles(context.parentTaskFiles));
6590
+ }
6564
6591
  if (context.repoRefs && context.repoRefs.length > 0) {
6565
6592
  parts.push(...formatRepoRefs(context.repoRefs));
6566
6593
  }
@@ -7454,11 +7481,18 @@ var subtasksContracts = [
7454
7481
  setTaskParentContract
7455
7482
  ];
7456
7483
  var mcpTaskIdOrSlug = f.string({ desc: "The task ID or slug" });
7484
+ var agentTaskIdOrSlug = f.optional(
7485
+ f.string({
7486
+ desc: "Omit for the current card; pass a parent, sibling, or any other card in this project (id or slug) to read its files instead."
7487
+ })
7488
+ );
7457
7489
  var listTaskFilesContract = defineToolContract({
7458
7490
  name: "list_task_files",
7459
7491
  agent: {
7460
- description: "List all files attached to this task with metadata. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
7461
- fields: {}
7492
+ description: "List all files attached to this task with metadata. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment. Pass task_id to list another card's files in this project, for example the pack parent's source documents.",
7493
+ fields: {
7494
+ task_id: agentTaskIdOrSlug
7495
+ }
7462
7496
  },
7463
7497
  mcp: {
7464
7498
  description: "List all files attached to a task with metadata (no contents \u2014 fast and small). Pass projectId to target a specific project; otherwise the configured default project is used. Use before fetching a specific file to see what is available and how large each is. For file contents use get_attachment.",
@@ -7471,13 +7505,14 @@ var listTaskFilesContract = defineToolContract({
7471
7505
  var getAttachmentContract = defineToolContract({
7472
7506
  name: "get_attachment",
7473
7507
  agent: {
7474
- description: "Fetch one task file's content plus metadata by file ID. Call list_task_files first to discover IDs and check sizes \u2014 large binaries may be truncated by the service's size limit.",
7508
+ description: "Fetch one task file's content plus metadata by file ID. This is the way to read attachment bytes: a document `downloadUrl` (anything that is not an image or video) answers 404 without a signed-in project member's session, so do not curl or WebFetch it from the pod. Call list_task_files first to discover IDs and check sizes \u2014 large binaries may be truncated by the service's size limit. Pass task_id when the file is attached to another card in this project, such as the pack parent.",
7475
7509
  fields: {
7476
- fileId: f.string({ desc: "The file ID to retrieve" })
7510
+ fileId: f.string({ desc: "The file ID to retrieve" }),
7511
+ task_id: agentTaskIdOrSlug
7477
7512
  }
7478
7513
  },
7479
7514
  mcp: {
7480
- description: "Fetch one task file's content plus metadata by file ID (accepts task id or slug). Pass projectId to target a specific project; otherwise the configured default project is used. Images are returned as viewable image blocks. Large text files (logs, JSON) are returned in pages \u2014 use `offset`/`maxBytes` to read more, or fetch `downloadUrl` for the whole file. Call list_task_files first to discover IDs and sizes.",
7515
+ description: "Fetch one task file's content plus metadata by file ID (accepts task id or slug). Pass projectId to target a specific project; otherwise the configured default project is used. Images are returned as viewable image blocks. Large text files (logs, JSON) are returned in pages \u2014 use `offset`/`maxBytes` to read more. This tool is the way to read bytes: a document `downloadUrl` (anything that is not an image or video) answers 404 unless the request carries a signed-in project member's session, so do not curl or fetch it. Call list_task_files first to discover IDs and sizes.",
7481
7516
  fields: {
7482
7517
  projectId: mcpProjectId,
7483
7518
  taskId: mcpTaskIdOrSlug,
@@ -7544,10 +7579,29 @@ var uploadAttachmentContract = defineToolContract({
7544
7579
  }
7545
7580
  }
7546
7581
  });
7582
+ var DELETE_ATTACHMENT_WARNING = "Permanent, with no trash and no undo (unlike drive_delete_file): the file and its bytes are gone, and the chat message that announced it keeps its text but loses the attachment. Use it to remove a superseded upload once its replacement is attached. `fileId` comes from list_task_files.";
7583
+ var deleteAttachmentContract = defineToolContract({
7584
+ name: "delete_attachment",
7585
+ agent: {
7586
+ description: `Delete one attachment from this task. ${DELETE_ATTACHMENT_WARNING} A pod can only delete files on its own card.`,
7587
+ fields: {
7588
+ fileId: f.string({ desc: "The file ID to delete" })
7589
+ }
7590
+ },
7591
+ mcp: {
7592
+ description: `Delete one attachment from a task (accepts task id or slug). ${DELETE_ATTACHMENT_WARNING} Requires Moderate on the project, the same bar as the web Files delete button. Pass projectId to target a specific project; otherwise the configured default project is used.`,
7593
+ fields: {
7594
+ projectId: mcpProjectId,
7595
+ taskId: mcpTaskIdOrSlug,
7596
+ fileId: f.string({ desc: "The file ID to delete" })
7597
+ }
7598
+ }
7599
+ });
7547
7600
  var attachmentsContracts = [
7548
7601
  listTaskFilesContract,
7549
7602
  getAttachmentContract,
7550
- uploadAttachmentContract
7603
+ uploadAttachmentContract,
7604
+ deleteAttachmentContract
7551
7605
  ];
7552
7606
  var createSuggestionContract = defineToolContract({
7553
7607
  name: "create_suggestion",
@@ -7870,7 +7924,7 @@ var createPullRequestContract = defineToolContract({
7870
7924
  fields: {
7871
7925
  title: f.string({ desc: "The PR title" }),
7872
7926
  body: f.string({
7873
- desc: "The PR description/body in markdown. If the diff changes anything a user sees rendered, the body MUST show the visual proof inline, not just point at the card: upload the capture with upload_attachment, then embed the downloadUrl it returns under a '## Screenshots' heading (![caption](url) for images, [caption](url) for video). Attach to the card as well \u2014 the card is the durable home, the PR body is what the reviewer reads."
7927
+ desc: "The PR description/body in markdown. If the diff changes anything a user sees rendered, the body MUST show the visual proof inline, not just point at the card: upload the capture with upload_attachment, then embed the downloadUrl it returns under a '## Screenshots' heading (![caption](url) for images, [caption](url) for video). Only image and video URLs belong in a PR body: a document downloadUrl (csv, pdf, md, \u2026) needs a signed-in project member and shows 404 to a reviewer on GitHub, so point at the card instead. Attach to the card as well \u2014 the card is the durable home, the PR body is what the reviewer reads."
7874
7928
  }),
7875
7929
  branch: f.optional(
7876
7930
  f.string({
@@ -8727,17 +8781,20 @@ function buildGetExecutionLogsTool(connection) {
8727
8781
  function buildListTaskFilesTool(connection) {
8728
8782
  return defineContractTool(
8729
8783
  listTaskFilesContract,
8730
- async () => {
8784
+ async ({ task_id }) => {
8731
8785
  try {
8732
8786
  const files = await connection.call("getTaskFiles", {
8733
- sessionId: connection.sessionId
8787
+ sessionId: connection.sessionId,
8788
+ ...task_id ? { taskId: task_id } : {}
8734
8789
  });
8735
8790
  const metadata = files.map((file) => {
8736
8791
  const { content: _c, ...rest } = file;
8737
8792
  return rest;
8738
8793
  });
8794
+ const heading = task_id ? `Files on card ${task_id}:
8795
+ ` : "";
8739
8796
  const content = [
8740
- { type: "text", text: JSON.stringify(metadata, null, 2) }
8797
+ { type: "text", text: heading + JSON.stringify(metadata, null, 2) }
8741
8798
  ];
8742
8799
  for (const file of files) {
8743
8800
  if (file.content && file.contentEncoding === "base64" && isImageMimeType(file.mimeType)) {
@@ -8755,11 +8812,12 @@ function buildListTaskFilesTool(connection) {
8755
8812
  function buildGetAttachmentTool(connection) {
8756
8813
  return defineContractTool(
8757
8814
  getAttachmentContract,
8758
- async ({ fileId }) => {
8815
+ async ({ fileId, task_id }) => {
8759
8816
  try {
8760
8817
  const file = await connection.call("getTaskFile", {
8761
8818
  sessionId: connection.sessionId,
8762
- fileId
8819
+ fileId,
8820
+ ...task_id ? { taskId: task_id } : {}
8763
8821
  });
8764
8822
  const { content: rawContent, ...metadata } = file;
8765
8823
  const content = [
@@ -9346,6 +9404,9 @@ var MIME_BY_EXT = {
9346
9404
  ".yaml": "application/yaml",
9347
9405
  ".yml": "application/yaml",
9348
9406
  ".pdf": "application/pdf",
9407
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
9408
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
9409
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
9349
9410
  ".zip": "application/zip",
9350
9411
  ".gz": "application/gzip",
9351
9412
  ".mp4": "video/mp4",
@@ -9368,11 +9429,21 @@ var EMBEDDABLE_IN_MARKDOWN = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", "
9368
9429
  function describeEmbed(fileName, downloadUrl) {
9369
9430
  if (!downloadUrl) return "";
9370
9431
  const isImage = EMBEDDABLE_IN_MARKDOWN.has(extname(fileName).toLowerCase());
9371
- const snippet = isImage ? `![${fileName}](${downloadUrl})` : `[${fileName}](${downloadUrl})`;
9372
- return `
9432
+ if (isImage) {
9433
+ return `
9373
9434
 
9374
9435
  Paste this into the PR description to show it inline:
9375
- ${snippet}`;
9436
+ ![${fileName}](${downloadUrl})`;
9437
+ }
9438
+ if (isCapabilityServableMime(inferMimeType(fileName))) {
9439
+ return `
9440
+
9441
+ Paste this into the PR description to link it:
9442
+ [${fileName}](${downloadUrl})`;
9443
+ }
9444
+ return `
9445
+
9446
+ downloadUrl (signed-in project members only; agents read it with get_attachment): ${downloadUrl}`;
9376
9447
  }
9377
9448
  function buildUploadAttachmentTool(connection, config) {
9378
9449
  return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
@@ -9425,6 +9496,23 @@ function buildUploadAttachmentTool(connection, config) {
9425
9496
  }
9426
9497
  });
9427
9498
  }
9499
+ function buildDeleteAttachmentTool(connection) {
9500
+ return defineContractTool(deleteAttachmentContract, async ({ fileId }) => {
9501
+ try {
9502
+ const result = await connection.call("deleteTaskFile", {
9503
+ sessionId: connection.sessionId,
9504
+ fileId
9505
+ });
9506
+ return textResult(
9507
+ `Deleted ${result.fileName} (${result.id}) permanently. Any chat message that announced it keeps its text without the attachment.`
9508
+ );
9509
+ } catch (error) {
9510
+ return textResult(
9511
+ `Failed to delete attachment: ${error instanceof Error ? error.message : "Unknown error"}`
9512
+ );
9513
+ }
9514
+ });
9515
+ }
9428
9516
 
9429
9517
  // src/tools/checklist-tools.ts
9430
9518
  function buildListManualTestsTool(connection) {
@@ -9621,7 +9709,8 @@ function buildCommonTools(connection, config) {
9621
9709
  buildApproveManualTestTool(connection),
9622
9710
  buildRejectManualTestTool(connection),
9623
9711
  ...buildMutationTools(connection, config),
9624
- buildUploadAttachmentTool(connection, config)
9712
+ buildUploadAttachmentTool(connection, config),
9713
+ buildDeleteAttachmentTool(connection)
9625
9714
  ];
9626
9715
  }
9627
9716
 
@@ -12325,7 +12414,15 @@ function hasExistingSessionFile(taskId, cwd, lineage) {
12325
12414
  if (process.env.CONVEYOR_TUI === "codex") {
12326
12415
  return !!readCodexLineage(taskIdToSessionUuid(key), cwd);
12327
12416
  }
12328
- return sessionFileExists(taskIdToSessionUuid(key), cwd);
12417
+ const uuid = taskIdToSessionUuid(key);
12418
+ return sessionFileExists(uuid, cwd) && isResumableTranscript(sessionTranscriptPath(cwd, uuid));
12419
+ }
12420
+ var spawnedTranscripts = /* @__PURE__ */ new Set();
12421
+ function markSpawnedTranscript(cwd, sessionUuid) {
12422
+ spawnedTranscripts.add(sessionTranscriptPath(cwd, sessionUuid));
12423
+ }
12424
+ function isResumableTranscript(path) {
12425
+ return spawnedTranscripts.has(path) || transcriptHasConversation(path);
12329
12426
  }
12330
12427
  function resolveSessionStart(lineageKey, cwd, tui = process.env.CONVEYOR_TUI) {
12331
12428
  const sessionUuid = taskIdToSessionUuid(lineageKey);
@@ -12334,11 +12431,46 @@ function resolveSessionStart(lineageKey, cwd, tui = process.env.CONVEYOR_TUI) {
12334
12431
  return { sessionId: sessionUuid, ...resume ? { resume } : {} };
12335
12432
  }
12336
12433
  if (sessionFileExists(sessionUuid, cwd)) {
12337
- repairTornSessionFile(sessionTranscriptPath(cwd, sessionUuid));
12338
- return { resume: sessionUuid };
12434
+ const path = sessionTranscriptPath(cwd, sessionUuid);
12435
+ repairTornSessionFile(path);
12436
+ if (isResumableTranscript(path)) return { resume: sessionUuid };
12437
+ rotateSessionFile(cwd, sessionUuid);
12438
+ logger5.warn("Rotated a transcript with no conversation aside before resume", { path });
12339
12439
  }
12440
+ markSpawnedTranscript(cwd, sessionUuid);
12340
12441
  return { sessionId: sessionUuid };
12341
12442
  }
12443
+ var CONVERSATION_PROBE_BYTES = 256 * 1024;
12444
+ function transcriptHasConversation(path) {
12445
+ let fd;
12446
+ try {
12447
+ const size = statSync2(path).size;
12448
+ const length = Math.min(size, CONVERSATION_PROBE_BYTES);
12449
+ const buffer = Buffer.alloc(length);
12450
+ fd = openSync(path, "r");
12451
+ const read = readSync(fd, buffer, 0, length, 0);
12452
+ const text = buffer.subarray(0, read).toString("utf8");
12453
+ const complete = text.slice(0, text.lastIndexOf("\n") + 1);
12454
+ for (const line of complete.split("\n")) {
12455
+ if (!line.trim()) continue;
12456
+ try {
12457
+ const type = JSON.parse(line).type;
12458
+ if (type === "user" || type === "assistant") return true;
12459
+ } catch {
12460
+ }
12461
+ }
12462
+ return size > CONVERSATION_PROBE_BYTES;
12463
+ } catch {
12464
+ return true;
12465
+ } finally {
12466
+ if (fd !== void 0) {
12467
+ try {
12468
+ closeSync(fd);
12469
+ } catch {
12470
+ }
12471
+ }
12472
+ }
12473
+ }
12342
12474
  function repairTornSessionFile(path) {
12343
12475
  try {
12344
12476
  if (!existsSync(path)) return false;
@@ -12867,6 +12999,7 @@ function freshSessionOptions(host, context, options) {
12867
12999
  sessionLineageKey(context.taskId, host.agentMode, host.config.mode)
12868
13000
  );
12869
13001
  if (rotateSessionFile(host.config.workspaceDir, lineageUuid)) {
13002
+ markSpawnedTranscript(host.config.workspaceDir, lineageUuid);
12870
13003
  return { ...options, sessionId: lineageUuid };
12871
13004
  }
12872
13005
  return { ...options, sessionId: randomUUID2() };
@@ -12918,6 +13051,11 @@ async function handleAuthError(context, host, options) {
12918
13051
  async function handleStaleSession(context, host, options) {
12919
13052
  context.claudeSessionId = null;
12920
13053
  host.connection.storeSessionId("");
13054
+ logger5.warn("Resume target had no resumable conversation; rotating it aside and starting fresh", {
13055
+ taskId: context.taskId,
13056
+ runnerMode: host.config.mode,
13057
+ agentMode: host.agentMode
13058
+ });
12921
13059
  const freshPrompt = buildMultimodalPrompt(
12922
13060
  await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
12923
13061
  context,
@@ -13025,7 +13163,7 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
13025
13163
  return runWithRetry(freshQuery, context, host, options);
13026
13164
  }
13027
13165
  function handleRetryError(error, context, host, options, prevImageError) {
13028
- if (isStaleOrExitedSession(error, context) && context.claudeSessionId) {
13166
+ if (isStaleOrExitedSession(error, context)) {
13029
13167
  return handleStaleSession(context, host, options);
13030
13168
  }
13031
13169
  if (isAuthError(getErrorMessage(error))) {
@@ -13051,7 +13189,7 @@ function handleProcessResult(result, context, host, options) {
13051
13189
  )
13052
13190
  };
13053
13191
  }
13054
- if (result.staleSession && context.claudeSessionId) {
13192
+ if (result.staleSession) {
13055
13193
  return {
13056
13194
  action: "return_promise",
13057
13195
  promise: handleStaleSession(context, host, options)
@@ -15234,6 +15372,7 @@ var SessionRunner = class _SessionRunner {
15234
15372
  claudeSessionId: ctx.claudeSessionId ?? null,
15235
15373
  lastSeenMessageId: ctx.lastSeenMessageId ?? null,
15236
15374
  isParentTask: ctx.isParentTask ?? false,
15375
+ parentTaskFiles: ctx.parentTaskFiles ?? null,
15237
15376
  storyPoints: ctx.storyPoints ?? void 0,
15238
15377
  agents: ctx.agents ?? void 0,
15239
15378
  projectTags: ctx.projectTags ?? void 0,
@@ -1785,8 +1785,41 @@ var IDENTIFIED_WORK_STATUSES = ["Open", ...ACTIVE_WORK_STATUSES];
1785
1785
  var DEFAULT_TASK_STATUS_COLOR = "#a8a29e";
1786
1786
  var DEFAULT_TASK_STATUS_COLOR_INT = Number.parseInt(DEFAULT_TASK_STATUS_COLOR.slice(1), 16);
1787
1787
  var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
1788
+ function isCapabilityServableMime(mimeType) {
1789
+ const base = mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
1790
+ return base.startsWith("image/") || base.startsWith("video/");
1791
+ }
1788
1792
  var MAX_FILE_TAGS = 5;
1789
1793
  var MAX_FILE_TAG_LENGTH = 100;
1794
+ var ALLOWED_FILE_MIME_TYPES = [
1795
+ "image/png",
1796
+ "image/jpeg",
1797
+ "image/gif",
1798
+ "image/webp",
1799
+ "application/json",
1800
+ "text/csv",
1801
+ "text/plain",
1802
+ "text/markdown",
1803
+ "text/vnd.mermaid",
1804
+ "application/pdf",
1805
+ // Reports (conveyor-consensus and friends) are single-file, no-JS HTML. Safe
1806
+ // to store because the DOWNLOAD route serves these under a sandbox CSP with
1807
+ // an opaque origin — see SANDBOXED_MIME in services/file/routes.ts.
1808
+ "text/html",
1809
+ // Office documents: the OOXML trio only (legacy .doc/.xls/.ppt stay out).
1810
+ // Served inline like PDFs; browsers download them and nothing renders
1811
+ // in-origin, so they need no SANDBOXED_MIME entry.
1812
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1813
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1814
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1815
+ ];
1816
+ var ALLOWED_FILE_ACCEPT = [
1817
+ ...ALLOWED_FILE_MIME_TYPES,
1818
+ // Extensions too: a mime-only `accept` filters the OS picker by its own mime
1819
+ // registry, which greys out .md/.mmd (and Office files on a machine without
1820
+ // Office) even though the upload would succeed.
1821
+ ".png,.jpg,.jpeg,.gif,.webp,.json,.csv,.txt,.md,.mmd,.pdf,.html,.docx,.xlsx,.pptx"
1822
+ ].join(",");
1790
1823
  var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
1791
1824
  var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
1792
1825
  var IDLE_HEARTBEAT_MS = 90 * 1e3;
@@ -1992,9 +2025,15 @@ var GetChatMessagesRequestSchema = z3.object({
1992
2025
  taskId: z3.string().optional()
1993
2026
  });
1994
2027
  var GetTaskFilesRequestSchema = z3.object({
1995
- sessionId: z3.string()
2028
+ sessionId: z3.string(),
2029
+ taskId: z3.string().optional()
1996
2030
  });
1997
2031
  var GetTaskFileRequestSchema = z3.object({
2032
+ sessionId: z3.string(),
2033
+ fileId: z3.string(),
2034
+ taskId: z3.string().optional()
2035
+ });
2036
+ var DeleteTaskFileRequestSchema = z3.object({
1998
2037
  sessionId: z3.string(),
1999
2038
  fileId: z3.string()
2000
2039
  });
@@ -2924,6 +2963,12 @@ var GetProjectAttachmentRequestSchema = z5.object({
2924
2963
  /** Max bytes of text content to return from `offset`. Server default applies. */
2925
2964
  maxBytes: z5.number().int().positive().optional()
2926
2965
  });
2966
+ var DeleteProjectTaskFileRequestSchema = z5.object({
2967
+ projectId: z5.string(),
2968
+ taskId: z5.string(),
2969
+ fileId: z5.string(),
2970
+ requestingUserId: z5.string().optional()
2971
+ });
2927
2972
  var RequestProjectFileUploadRequestSchema = z5.object({
2928
2973
  projectId: z5.string(),
2929
2974
  taskId: z5.string(),
@@ -5061,6 +5106,7 @@ export {
5061
5106
  EXTERNAL_AGENT_MESSAGE_SOURCE,
5062
5107
  TUI_KINDS,
5063
5108
  MAX_FILE_SIZE_BYTES,
5109
+ isCapabilityServableMime,
5064
5110
  RUNNER_MODES,
5065
5111
  ReviewGuideContentSchema,
5066
5112
  CONTEXT_LINK_LOCATOR_MAX,
@@ -8,7 +8,7 @@ import {
8
8
  readWorkspaceBytes,
9
9
  statWorkspacePath,
10
10
  workspacePathExists
11
- } from "./chunk-BZZ4B37Q.js";
11
+ } from "./chunk-PBSFLQA7.js";
12
12
  import {
13
13
  reportBootMilestone
14
14
  } from "./chunk-Q4FQOJ7D.js";
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-7WO6NTOS.js";
10
+ } from "./chunk-SI7WNWZZ.js";
11
11
  import {
12
12
  PtyHarness,
13
13
  SessionRunner,
@@ -25,7 +25,7 @@ import {
25
25
  runUsageProbe,
26
26
  sampleKeyUsage,
27
27
  usesNativeUsageReporting
28
- } from "./chunk-RLROQPPT.js";
28
+ } from "./chunk-7HK6OIV4.js";
29
29
  import "./chunk-37J5MMQT.js";
30
30
  import {
31
31
  inheritedEnv,
@@ -44,7 +44,7 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-BZZ4B37Q.js";
47
+ } from "./chunk-PBSFLQA7.js";
48
48
  import "./chunk-Q4FQOJ7D.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-372R6E4C.js";
@@ -1458,7 +1458,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1458
1458
  process.exit(1);
1459
1459
  }
1460
1460
  if (CONVEYOR_MODE === "serving") {
1461
- const { runServingSession } = await import("./serve-boot-T7MNWASE.js");
1461
+ const { runServingSession } = await import("./serve-boot-WGUFEWYJ.js");
1462
1462
  exitContext.runnerMode = "serving";
1463
1463
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1464
1464
  const outcome = await runServingSession({
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-RLROQPPT.js";
4
+ } from "./chunk-7HK6OIV4.js";
5
5
  import "./chunk-37J5MMQT.js";
6
6
  import "./chunk-WCJ6HRVU.js";
7
7
  import {
@@ -17,7 +17,7 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-BZZ4B37Q.js";
20
+ } from "./chunk-PBSFLQA7.js";
21
21
  import "./chunk-Q4FQOJ7D.js";
22
22
  import {
23
23
  runAuthTokenCommand,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-7WO6NTOS.js";
3
+ } from "./chunk-SI7WNWZZ.js";
4
4
  import {
5
5
  AgentConnection,
6
6
  CodespacePortVisibility,
@@ -11,7 +11,7 @@ import {
11
11
  createServiceLogger,
12
12
  ensureOnTaskBranch,
13
13
  loadConveyorConfig
14
- } from "./chunk-BZZ4B37Q.js";
14
+ } from "./chunk-PBSFLQA7.js";
15
15
  import "./chunk-Q4FQOJ7D.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.25",
3
+ "version": "11.0.27",
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",
@@ -111,7 +111,11 @@ task binding and no user account.
111
111
  1. Resolve the card from the argument — slug, id, or URL. **None given → ask
112
112
  which card.** Never infer one from the branch you happen to be on.
113
113
  2. `mcp__conveyor__get_task` for the full plan and
114
- `mcp__conveyor__read_task_chat` for addenda and user answers. Consult
114
+ `mcp__conveyor__read_task_chat` for addenda and user answers. On a pack
115
+ child the source documents usually sit on the parent card:
116
+ `mcp__conveyor__list_task_files` with `task_id` set to the parent (in a pod;
117
+ the local surface spells it `taskId`) lists them, and
118
+ `mcp__conveyor__get_attachment` with the same `task_id` reads one. Consult
115
119
  `mcp__conveyor__get_tag` on the card's tags before diving in — a tag's
116
120
  overview and linked files are the fast path into the subsystem.
117
121
  3. Route on the card's shape:
@@ -249,7 +253,10 @@ Instead: post the answer, config, or findings with
249
253
  `mcp__conveyor__upload_attachment` (any file type, up to 25MB), and complete
250
254
  the card directly with `force_update_task_status("Complete")` — there is no PR
251
255
  or review step for a no-code task. Never publish a deliverable as an off-card
252
- link; the card is where it belongs.
256
+ link; the card is where it belongs. When a revision supersedes an earlier
257
+ upload, remove the old copy with `mcp__conveyor__delete_attachment` (permanent,
258
+ `fileId` from `list_task_files`) so the Files list shows one current version
259
+ of each deliverable.
253
260
 
254
261
  When unsure, check the diff: a real diff means open a PR, no diff means finish
255
262
  in chat and mark it Complete.
@@ -100,6 +100,10 @@ Scale to the idea's size; a one-file tweak needs minutes, not a survey.
100
100
  already in your context. **Stop when you can cite a specific `file.ts:line`
101
101
  and a symbol name for every step your plan will contain** — that is the
102
102
  threshold, and more reading past it buys nothing.
103
+ - **Attachments**: `mcp__conveyor__list_task_files` on the card, and on its
104
+ parent when the card is a pack child (in a pod pass `task_id`; the local
105
+ surface takes `taskId`). Source documents, screenshots and worksheets usually
106
+ live there, and a plan written without reading them plans the wrong work.
103
107
  - **Prior art**: `mcp__conveyor__search_tasks` on 2-3 keyword variants
104
108
  (`typeFilters` to include incidents/suggestions when relevant), then
105
109
  `mcp__conveyor__get_task` on the closest hits. You're looking for duplicates
@@ -80,7 +80,9 @@ Find the code path the report implicates and, where you can, reproduce it. A
80
80
  diagnosis grounded only in reading is a hypothesis; a reproduction makes it a
81
81
  finding. Attach evidence to the card with `mcp__conveyor__upload_attachment` —
82
82
  a screenshot, a log excerpt, a short recording — so the Builder inherits proof
83
- rather than your assertion.
83
+ rather than your assertion. A re-captured version replaces the old one:
84
+ `mcp__conveyor__delete_attachment` (permanent, `fileId` from
85
+ `list_task_files`) keeps the card's Files list to what is current.
84
86
 
85
87
  ## 5. Decide
86
88