@rallycry/conveyor-agent 11.0.23 → 11.0.24

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-EIHZRSYI.js";
3
+ } from "./chunk-EITUTXXJ.js";
4
4
  import {
5
5
  TOMBSTONE_MESSAGE,
6
6
  isLegacyEntrypointLaunch
@@ -12,7 +12,7 @@ import {
12
12
  refreshSkillsAfterCheckout,
13
13
  runPreReadyBinds
14
14
  } from "./chunk-37J5MMQT.js";
15
- import "./chunk-W5INK3NE.js";
15
+ import "./chunk-WCJ6HRVU.js";
16
16
  import {
17
17
  GitPrepJob,
18
18
  defaultGit,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadPtySpawn
3
- } from "./chunk-W5INK3NE.js";
3
+ } from "./chunk-WCJ6HRVU.js";
4
4
  import {
5
5
  terminateProcessGroup
6
6
  } from "./chunk-W4LZ7R6Z.js";
@@ -29,7 +29,7 @@ import {
29
29
  spawnOptionsFingerprint,
30
30
  transcriptSize,
31
31
  turnOptionsFrom
32
- } from "./chunk-W5INK3NE.js";
32
+ } from "./chunk-WCJ6HRVU.js";
33
33
  import {
34
34
  AGENT_STATUS_REASON_USER_QUESTION,
35
35
  AgentConnection,
@@ -91,7 +91,7 @@ import {
91
91
  statWorkspacePath,
92
92
  updateRemoteToken,
93
93
  verifyGitCredential
94
- } from "./chunk-I7QYTA2V.js";
94
+ } from "./chunk-YNI2HFZE.js";
95
95
  import {
96
96
  registerBootMilestoneSocketFallback,
97
97
  reportBootMilestone
@@ -831,9 +831,11 @@ function mapUsage(message) {
831
831
  if (!isRecord(usage)) return void 0;
832
832
  const result = {};
833
833
  const input = numberField(usage, "input_tokens");
834
+ const output = numberField(usage, "output_tokens");
834
835
  const cacheRead = numberField(usage, "cache_read_input_tokens");
835
836
  const cacheCreation = numberField(usage, "cache_creation_input_tokens");
836
837
  if (input !== void 0) result.input_tokens = input;
838
+ if (output !== void 0) result.output_tokens = output;
837
839
  if (cacheRead !== void 0) result.cache_read_input_tokens = cacheRead;
838
840
  if (cacheCreation !== void 0) result.cache_creation_input_tokens = cacheCreation;
839
841
  return result;
@@ -1807,8 +1809,13 @@ async function writeHookSettings(dir, opts = {}) {
1807
1809
  // `--dangerously-skip-permissions`.
1808
1810
  // NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
1809
1811
  // a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
1812
+ // `deny` carries the tools no pod agent may use (Artifact publishes to an
1813
+ // off-platform claude.ai URL; deliverables belong on the card). Bare tool
1814
+ // names are the valid rule form. The key is omitted when empty — see
1815
+ // HookSettingsOptions.disallowedTools for why that matters.
1810
1816
  permissions: {
1811
- allow: ALLOW_RULES
1817
+ allow: ALLOW_RULES,
1818
+ ...opts.disallowedTools && opts.disallowedTools.length > 0 ? { deny: opts.disallowedTools } : {}
1812
1819
  },
1813
1820
  hooks: {
1814
1821
  PreToolUse: buildPreToolUseHooks(helperPath, opts.gateAllTools === true),
@@ -2606,6 +2613,7 @@ var ClaudeTuiAdapter = class {
2606
2613
  permissionMode: options.permissionMode,
2607
2614
  settingsPath: input.settingsPath ?? "",
2608
2615
  ...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
2616
+ ...options.disallowedTools?.length ? { disallowedTools: options.disallowedTools } : {},
2609
2617
  ...input.mcpConfigPath ? { mcpConfigPath: input.mcpConfigPath, strictMcpConfig: true } : {}
2610
2618
  });
2611
2619
  return {
@@ -3155,7 +3163,8 @@ var PtySession = class {
3155
3163
  );
3156
3164
  await this.socket.listen();
3157
3165
  const { settingsPath } = await writeHookSettings(this.tempDir, {
3158
- gateAllTools: this.options.permissionMode === "plan"
3166
+ gateAllTools: this.options.permissionMode === "plan",
3167
+ ...this.options.disallowedTools?.length ? { disallowedTools: this.options.disallowedTools } : {}
3159
3168
  });
3160
3169
  await this.setupToolServers();
3161
3170
  const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
@@ -5641,7 +5650,8 @@ function buildDiscoveryPrompt(context, runnerMode) {
5641
5650
  ] : [],
5642
5651
  ``,
5643
5652
  `### Contracts this environment enforces`,
5644
- `- You are READ-ONLY. File writes are denied outside \`.claude/plans/\`, and so are the commands that would start the work: git commit/push/merge/rebase/checkout, git apply, gh pr create, package publish. Everything else runs without an approval prompt \u2014 the pod is a sandbox, so investigate freely (build, test, query the dev DB, read logs).`,
5653
+ `- You are READ-ONLY. File writes are denied outside \`.claude/plans/\` and \`/tmp/\`, and so are the commands that would start the work: git commit/push/merge/rebase/checkout, git apply, gh pr create, package publish. Everything else runs without an approval prompt \u2014 the pod is a sandbox, so investigate freely (build, test, query the dev DB, read logs).`,
5654
+ `- Research deliverables the team should keep (a diagram, an HTML report, a screenshot, a data export) go on the card with \`mcp__conveyor__upload_attachment\`: write the file under \`/tmp/\` first, and pass \`title\` as the caption so it shows in the thread. Never publish one as a Claude artifact.`,
5645
5655
  `- A plan file on disk is NOT the plan. Only \`update_task\` saves it to the card.`,
5646
5656
  `- **ExitPlanMode is the LAST step and it validates.** It fails until the card has a plan, story points, risk, and a title \u2014 the plan comes from \`update_task\`, and story points, risk and title from \`update_task_properties\`. Calling it early wastes a turn on a denial.`,
5647
5657
  `- ExitPlanMode does NOT start building. It parks this session for human review; the team decides when to build.`,
@@ -6340,7 +6350,7 @@ Working rules:`,
6340
6350
  `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
6341
6351
  `- App servers (including development and production servers) keep running after startup. Start them with the harness background/session facility, then run a separate bounded HTTP readiness check and continue verification. Never wait for server exit or a completion notification to mean the app is ready. If readiness times out, inspect build output and process/resource state before restarting.`,
6342
6352
  runtimeTui === "codex" ? `- Waiting on long-running commands: retain and resume the command session until it returns an exit result. Finish each required gate before ending the task. Never pretend a completion notification will resume you, and never start another gate while the current one is running.` : `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and tracked background work holds the workspace awake for up to 45 minutes. Inspect any gate approaching that cap. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth. A pod restart loses both foreground and background processes. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
6343
- runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- For a finite background gate, end your turn and its completion notification re-invokes you within the bounded liveness window. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire.`,
6353
+ runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- For a finite background gate, end your turn and its completion notification re-invokes you within the bounded liveness window. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire. One armed wakeup keeps this workspace awake until it fires (at most one hour), so arm exactly one and end the turn \u2014 a second one "as insurance" buys nothing.`,
6344
6354
  `
6345
6355
  Git:`,
6346
6356
  `- Stay on \`${context.githubBranch}\` for the whole task: do not check out another branch and do not create one. It was cut from \`${context.baseBranch}\`, and PRs target that automatically.`,
@@ -7502,6 +7512,11 @@ var uploadAttachmentContract = defineToolContract({
7502
7512
  title: f.optional(
7503
7513
  f.string({ desc: "Short caption posted with the file (defaults to the file name)" })
7504
7514
  ),
7515
+ comment: f.optional(
7516
+ f.string({
7517
+ desc: "Alias of `title` (the external conveyor-mcp surface names this field `comment`). Provide at most one of the two."
7518
+ })
7519
+ ),
7505
7520
  tags: attachmentTags
7506
7521
  }
7507
7522
  },
@@ -7514,6 +7529,11 @@ var uploadAttachmentContract = defineToolContract({
7514
7529
  comment: f.optional(
7515
7530
  f.string({ desc: "When set, also posts the attachment to the task chat with this text" })
7516
7531
  ),
7532
+ title: f.optional(
7533
+ f.string({
7534
+ desc: "Alias of `comment` (the in-pod agent surface names this field `title`). Provide at most one of the two."
7535
+ })
7536
+ ),
7517
7537
  tags: attachmentTags,
7518
7538
  mimeType: f.optional(
7519
7539
  f.string({ desc: "Override the mime type inferred from the file extension" })
@@ -7573,7 +7593,7 @@ var agentOption = f.object(
7573
7593
  var createDecisionContract = defineToolContract({
7574
7594
  name: "create_decision",
7575
7595
  agent: {
7576
- description: `Raise a product decision: a fork the plan cannot be right without. Two or three options, one default, a deadline. Run list_decisions FIRST \u2014 an answered decision is the answer, and re-asking it wastes everybody's time. Do NOT file a decision for an assumption you can simply state: an assumed behaviour belongs in the plan as a GIVEN / WHEN / THEN line under "Assumed behaviours". Returns the decision's id, slug, and url.`,
7596
+ description: `Raise a product decision: a fork the plan cannot be right without. Two or three options, one default, a deadline. Run list_decisions FIRST \u2014 an answered decision is the answer, and re-asking it wastes everybody's time. Do NOT file a decision for an assumption you can simply state: an assumed behaviour belongs in the plan as a GIVEN / WHEN / THEN line under "Assumed behaviours". Returns the decision in full (the same JSON as get_decision, with option ids and the deadline) plus which cards were linked.`,
7577
7597
  fields: {
7578
7598
  question: f.string({
7579
7599
  min: 1,
@@ -7610,6 +7630,16 @@ var createDecisionContract = defineToolContract({
7610
7630
  ),
7611
7631
  raised_by_pr: f.optional(
7612
7632
  f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7633
+ ),
7634
+ raised_by_card: f.optional(
7635
+ f.string({
7636
+ desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impacted_cards to block a card."
7637
+ })
7638
+ ),
7639
+ deadline_at: f.optional(
7640
+ f.string({
7641
+ desc: "Absolute deadline as an ISO 8601 timestamp, e.g. 2026-09-25T17:00:00Z. Wins over deadline_hours when both are given; must be in the future and within 720 hours."
7642
+ })
7613
7643
  )
7614
7644
  }
7615
7645
  },
@@ -7633,6 +7663,16 @@ var createDecisionContract = defineToolContract({
7633
7663
  tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
7634
7664
  raisedByPr: f.optional(
7635
7665
  f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7666
+ ),
7667
+ raisedByCard: f.optional(
7668
+ f.string({
7669
+ desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impactedCards to block a card."
7670
+ })
7671
+ ),
7672
+ deadlineAt: f.optional(
7673
+ f.string({
7674
+ desc: "Absolute deadline (ISO 8601). Wins over deadlineHours when both are given; must be in the future and within 720 hours."
7675
+ })
7636
7676
  )
7637
7677
  }
7638
7678
  }
@@ -7640,7 +7680,7 @@ var createDecisionContract = defineToolContract({
7640
7680
  var listDecisionsContract = defineToolContract({
7641
7681
  name: "list_decisions",
7642
7682
  agent: {
7643
- description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Decided rows carry their resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
7683
+ description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Each row carries the summary, who raised it (and from which card or PR), when, and \u2014 once settled \u2014 the resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
7644
7684
  fields: {
7645
7685
  status: f.optional(
7646
7686
  f.enum(STATUS_LABELS, {
@@ -7672,7 +7712,7 @@ var listDecisionsContract = defineToolContract({
7672
7712
  var getDecisionContract = defineToolContract({
7673
7713
  name: "get_decision",
7674
7714
  agent: {
7675
- description: "Read one decision in full: its context, every option with its GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, the cards it blocks, and the resolution once there is one. Use read_task_chat with the same id for the discussion thread.",
7715
+ description: "Read one decision in full: its summary and context, who raised it and from where, every option with its GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, the cards it blocks, and the resolution once there is one. Use read_task_chat with the same id for the discussion thread.",
7676
7716
  fields: {
7677
7717
  decision: f.string({ min: 1, desc: "Decision slug or id" })
7678
7718
  }
@@ -7737,10 +7777,32 @@ var resolveDecisionContract = defineToolContract({
7737
7777
  var updateDecisionContract = defineToolContract({
7738
7778
  name: "update_decision",
7739
7779
  agent: {
7740
- description: "Change an open decision: extend its deadline, link or unlink the cards it blocks, or cancel it. Cancelling un-blocks the impacted cards without answering the question, so only do it when the question stopped mattering.",
7780
+ description: "Change an open decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the deadline, link or unlink the cards it blocks, or cancel it. Cancelling un-blocks the impacted cards without answering the question, so only do it when the question stopped mattering.",
7741
7781
  fields: {
7742
7782
  decision: f.string({ min: 1, desc: "Decision slug or id" }),
7783
+ question: f.optional(f.string({ min: 1, desc: "New question, one plain sentence" })),
7784
+ summary: f.optional(
7785
+ f.string({ desc: cardDescriptionDesc("New one- or two-sentence summary") })
7786
+ ),
7787
+ context: f.optional(f.string({ min: 1, desc: "New markdown background" })),
7788
+ options: f.optional(
7789
+ f.array(agentOption, {
7790
+ min: 2,
7791
+ desc: "Replacement options. Accepted only while the decision has no votes \u2014 once somebody has voted, cancel it and raise a new one instead. Send default_option with it: the old default no longer exists."
7792
+ })
7793
+ ),
7794
+ default_option: f.optional(
7795
+ f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
7796
+ ),
7797
+ tag_names: f.optional(
7798
+ f.array(f.string(), { desc: "Replacement tag names (replaces the current set)" })
7799
+ ),
7743
7800
  deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
7801
+ deadline_at: f.optional(
7802
+ f.string({
7803
+ desc: "New absolute deadline (ISO 8601). Wins over deadline_hours; must be in the future and within 720 hours."
7804
+ })
7805
+ ),
7744
7806
  add_impacted_cards: f.optional(
7745
7807
  f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7746
7808
  ),
@@ -7755,13 +7817,29 @@ var updateDecisionContract = defineToolContract({
7755
7817
  }
7756
7818
  },
7757
7819
  mcp: {
7758
- description: "Change an open Conveyor decision: extend the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
7820
+ description: "Change an open Conveyor decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
7759
7821
  fields: {
7760
7822
  projectId: mcpProjectId,
7761
7823
  decision: f.string({ desc: "Decision slug or id" }),
7824
+ question: f.optional(f.string({ desc: "New question" })),
7825
+ summary: f.optional(f.string({ desc: cardDescriptionDesc("New summary") })),
7826
+ context: f.optional(f.string({ desc: "New markdown background" })),
7827
+ options: f.optional(
7828
+ f.array(agentOption, {
7829
+ min: 2,
7830
+ desc: "Replacement options. Accepted only while the decision has no votes. Send defaultOption with it: the old default no longer exists."
7831
+ })
7832
+ ),
7833
+ defaultOption: f.optional(
7834
+ f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
7835
+ ),
7836
+ tagNames: f.optional(f.array(f.string(), { desc: "Replacement tag names" })),
7762
7837
  deadlineHours: f.optional(
7763
7838
  f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
7764
7839
  ),
7840
+ deadlineAt: f.optional(
7841
+ f.string({ desc: "New absolute deadline (ISO 8601). Wins over deadlineHours." })
7842
+ ),
7765
7843
  addImpactedCards: f.optional(
7766
7844
  f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7767
7845
  ),
@@ -8506,10 +8584,11 @@ function buildReadTaskChatTool(connection) {
8506
8584
  taskId: task_id
8507
8585
  });
8508
8586
  return textResult(JSON.stringify(messages, null, 2));
8509
- } catch {
8587
+ } catch (error) {
8510
8588
  return textResult(
8511
8589
  JSON.stringify({
8512
- note: "Could not fetch live chat. Chat history was provided in the initial context."
8590
+ error: `Failed to read chat: ${error instanceof Error ? error.message : "Unknown error"}`,
8591
+ note: "Usually transient \u2014 retry. Chat history up to this session's start was provided in the initial context."
8513
8592
  })
8514
8593
  );
8515
8594
  }
@@ -9293,7 +9372,8 @@ Paste this into the PR description to show it inline:
9293
9372
  ${snippet}`;
9294
9373
  }
9295
9374
  function buildUploadAttachmentTool(connection, config) {
9296
- return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
9375
+ return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
9376
+ const caption = title ?? comment;
9297
9377
  try {
9298
9378
  const filePath = isAbsolute(path) ? path : join16(config.workspaceDir, path);
9299
9379
  const mimeType = inferMimeType(filePath);
@@ -9329,11 +9409,11 @@ function buildUploadAttachmentTool(connection, config) {
9329
9409
  const result = await connection.call("confirmFileUpload", {
9330
9410
  sessionId: connection.sessionId,
9331
9411
  fileId,
9332
- title,
9412
+ title: caption,
9333
9413
  tags
9334
9414
  });
9335
9415
  return textResult(
9336
- `Uploaded ${fileName} (${info.size} bytes) and posted it to the task chat${title ? ` with caption "${title}"` : ""}. File ID: ${result.fileId}${describeTags(result.appliedTags, result.unknownTags)}${describeEmbed(fileName, result.downloadUrl)}`
9416
+ `Uploaded ${fileName} (${info.size} bytes) and posted it to the task chat${caption ? ` with caption "${caption}"` : ""}. File ID: ${result.fileId}${describeTags(result.appliedTags, result.unknownTags)}${describeEmbed(fileName, result.downloadUrl)}`
9337
9417
  );
9338
9418
  } catch (error) {
9339
9419
  return textResult(
@@ -10765,14 +10845,19 @@ function raiseAndVoteTools(connection, projectId) {
10765
10845
  includeCurrentCard: input.include_current_card,
10766
10846
  sessionId: connection.sessionId,
10767
10847
  tagNames: input.tag_names,
10768
- raisedByPr: input.raised_by_pr
10848
+ raisedByPr: input.raised_by_pr,
10849
+ raisedByCard: input.raised_by_card,
10850
+ deadlineAt: input.deadline_at
10769
10851
  });
10770
- const refused = res.refused.length > 0 ? `
10852
+ const { impactedLinked, refused, ...detail } = res;
10853
+ const refusedLine = refused.length > 0 ? `
10771
10854
 
10772
- Not linked: ${res.refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
10855
+ Not linked: ${refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
10773
10856
  return textResult(
10774
10857
  `Raised decision ${res.slug} \u2014 ${res.url}
10775
- Blocking ${res.impactedLinked.length} card(s).${refused}`
10858
+ Blocking ${impactedLinked.length} card(s).${refusedLine}
10859
+
10860
+ ${JSON.stringify(detail, null, 2)}`
10776
10861
  );
10777
10862
  } catch (error) {
10778
10863
  return errText5("Failed to raise the decision", error);
@@ -10817,7 +10902,14 @@ function settleTools(connection, projectId) {
10817
10902
  const res = await connection.call("updateProjectDecision", {
10818
10903
  projectId,
10819
10904
  decision: input.decision,
10905
+ question: input.question,
10906
+ summary: input.summary,
10907
+ context: input.context,
10908
+ options: input.options,
10909
+ defaultOption: input.default_option,
10910
+ tagNames: input.tag_names,
10820
10911
  deadlineHours: input.deadline_hours,
10912
+ deadlineAt: input.deadline_at,
10821
10913
  addImpactedCards: input.add_impacted_cards,
10822
10914
  removeImpactedCards: input.remove_impacted_cards,
10823
10915
  status: input.status
@@ -11216,6 +11308,26 @@ function safeVoid(promise, context) {
11216
11308
  });
11217
11309
  }
11218
11310
  }
11311
+ function addTurnUsage(total, usage) {
11312
+ const next = {
11313
+ inputTokens: total.inputTokens + (usage.input_tokens ?? 0),
11314
+ outputTokens: total.outputTokens + (usage.output_tokens ?? 0),
11315
+ cacheReadInputTokens: total.cacheReadInputTokens + (usage.cache_read_input_tokens ?? 0),
11316
+ cacheCreationInputTokens: total.cacheCreationInputTokens + (usage.cache_creation_input_tokens ?? 0),
11317
+ totalTokens: 0
11318
+ };
11319
+ next.totalTokens = next.inputTokens + next.outputTokens + next.cacheReadInputTokens + next.cacheCreationInputTokens;
11320
+ return next;
11321
+ }
11322
+ function emptyTurnUsage() {
11323
+ return {
11324
+ inputTokens: 0,
11325
+ outputTokens: 0,
11326
+ cacheReadInputTokens: 0,
11327
+ cacheCreationInputTokens: 0,
11328
+ totalTokens: 0
11329
+ };
11330
+ }
11219
11331
  function epochSecondsToISO(value) {
11220
11332
  if (typeof value === "string") return value;
11221
11333
  if (typeof value !== "number" || value <= 0) return void 0;
@@ -11297,11 +11409,16 @@ function emitContextUpdate(modelUsage, host, context, lastAssistantUsage) {
11297
11409
  });
11298
11410
  }
11299
11411
  }
11300
- function handleSuccessResult(event, host, context, startTime, lastAssistantUsage) {
11412
+ function handleSuccessResult(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11301
11413
  const durationMs = Date.now() - startTime;
11302
11414
  const summary = event.result || "Task completed.";
11303
11415
  const retriable = isRetriableMessage(summary);
11304
- host.connection.sendEvent({ type: "completed", summary, durationMs });
11416
+ host.connection.sendEvent({
11417
+ type: "completed",
11418
+ summary,
11419
+ durationMs,
11420
+ ...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
11421
+ });
11305
11422
  const { modelUsage } = event;
11306
11423
  if (modelUsage && typeof modelUsage === "object") {
11307
11424
  emitContextUpdate(modelUsage, host, context, lastAssistantUsage);
@@ -11322,7 +11439,7 @@ function handleErrorResult(event, host) {
11322
11439
  host.connection.sendEvent({ type: "error", message: errorMsg });
11323
11440
  return { retriable };
11324
11441
  }
11325
- function handleResultEvent(event, host, context, startTime, lastAssistantUsage) {
11442
+ function handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11326
11443
  const resultSummary = event.subtype === "success" ? event.result : event.errors.join(", ");
11327
11444
  if (event.subtype === "success") {
11328
11445
  const result2 = handleSuccessResult(
@@ -11330,15 +11447,16 @@ function handleResultEvent(event, host, context, startTime, lastAssistantUsage)
11330
11447
  host,
11331
11448
  context,
11332
11449
  startTime,
11333
- lastAssistantUsage
11450
+ lastAssistantUsage,
11451
+ turnUsage
11334
11452
  );
11335
11453
  return { ...result2, resultSummary };
11336
11454
  }
11337
11455
  const result = handleErrorResult(event, host);
11338
11456
  return { ...result, resultSummary };
11339
11457
  }
11340
- async function emitResultEvent(event, host, context, startTime, lastAssistantUsage) {
11341
- const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage);
11458
+ async function emitResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11459
+ const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage);
11342
11460
  const durationMs = Date.now() - startTime;
11343
11461
  if (event.subtype === "success") {
11344
11462
  const successEvent = event;
@@ -11346,7 +11464,8 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
11346
11464
  await host.callbacks.onEvent({
11347
11465
  type: "completed",
11348
11466
  summary,
11349
- durationMs
11467
+ durationMs,
11468
+ ...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
11350
11469
  });
11351
11470
  } else if (!result.staleSession) {
11352
11471
  const errorEvent = event;
@@ -11452,13 +11571,20 @@ async function handleAssistantCase(event, host, turnToolCalls) {
11452
11571
  const msgUsage = event.message.usage;
11453
11572
  return msgUsage ?? void 0;
11454
11573
  }
11455
- async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage) {
11574
+ async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage, turnUsage) {
11456
11575
  let stoppedTyping = false;
11457
11576
  if (isTyping) {
11458
11577
  host.connection.sendTypingStop();
11459
11578
  stoppedTyping = true;
11460
11579
  }
11461
- const resultInfo = await emitResultEvent(event, host, context, startTime, lastAssistantUsage);
11580
+ const resultInfo = await emitResultEvent(
11581
+ event,
11582
+ host,
11583
+ context,
11584
+ startTime,
11585
+ lastAssistantUsage,
11586
+ turnUsage
11587
+ );
11462
11588
  return {
11463
11589
  retriable: resultInfo.retriable,
11464
11590
  resultSummary: resultInfo.resultSummary,
@@ -11562,7 +11688,10 @@ async function processAssistantCase(event, host, state) {
11562
11688
  state.isTyping = true;
11563
11689
  }
11564
11690
  const usage = await handleAssistantCase(event, host, state.turnToolCalls);
11565
- if (usage) state.lastAssistantUsage = usage;
11691
+ if (usage) {
11692
+ state.lastAssistantUsage = usage;
11693
+ state.turnUsage = addTurnUsage(state.turnUsage, usage);
11694
+ }
11566
11695
  if (!state.sawApiError || event.isApiError && !state.terminalApiError) {
11567
11696
  const fullText = event.message.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
11568
11697
  if (API_ERROR_PATTERN.test(fullText)) {
@@ -11581,7 +11710,8 @@ async function processResultCase(event, host, context, startTime, state) {
11581
11710
  context,
11582
11711
  startTime,
11583
11712
  state.isTyping,
11584
- state.lastAssistantUsage
11713
+ state.lastAssistantUsage,
11714
+ state.turnUsage
11585
11715
  );
11586
11716
  if (info.stoppedTyping) state.isTyping = false;
11587
11717
  state.retriable = info.retriable;
@@ -11614,6 +11744,7 @@ async function processEvents(events, context, host) {
11614
11744
  staleSession: void 0,
11615
11745
  authError: void 0,
11616
11746
  lastAssistantUsage: void 0,
11747
+ turnUsage: emptyTurnUsage(),
11617
11748
  turnToolCalls: [],
11618
11749
  questionPending: false
11619
11750
  };
@@ -11693,6 +11824,9 @@ function fallbackResetIso(rateLimitType, now = Date.now()) {
11693
11824
  return new Date(now + (weekly ? TWENTY_FOUR_HOURS_MS : FIVE_HOURS_MS)).toISOString();
11694
11825
  }
11695
11826
 
11827
+ // src/execution/tool-access.ts
11828
+ import { isAbsolute as isAbsolute3, posix } from "path";
11829
+
11696
11830
  // src/execution/task-property-utils.ts
11697
11831
  function collectMissingProps(taskProps) {
11698
11832
  const missing = [];
@@ -11835,6 +11969,12 @@ function isPlanFile(input) {
11835
11969
  const filePath = String(input.file_path ?? input.path ?? "");
11836
11970
  return filePath.includes(".claude/plans/");
11837
11971
  }
11972
+ function isScratchPath(input) {
11973
+ const filePath = String(input.file_path ?? input.path ?? "");
11974
+ if (!isAbsolute3(filePath)) return false;
11975
+ return posix.normalize(filePath).startsWith("/tmp/");
11976
+ }
11977
+ var ARTIFACT_DENIED = "Publish deliverables on the card with upload_attachment; the Artifact tool is not available in Conveyor sessions.";
11838
11978
  var READ_ONLY_BLOCKED_BASH = [
11839
11979
  { name: "git commit", re: /\bgit\s+(?:-\S+\s+)*commit\b/ },
11840
11980
  { name: "git push", re: /\bgit\s+(?:-\S+\s+)*push\b/ },
@@ -11860,12 +12000,12 @@ function handleReadOnlyToolAccess(toolName, input) {
11860
12000
  return { behavior: "deny", message: CHANNEL_POST_DENIED };
11861
12001
  }
11862
12002
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
11863
- if (isPlanFile(input)) {
12003
+ if (isPlanFile(input) || isScratchPath(input)) {
11864
12004
  return { behavior: "allow", updatedInput: input };
11865
12005
  }
11866
12006
  return {
11867
12007
  behavior: "deny",
11868
- message: "Discovery mode is read-only. File writes are restricted to plan files."
12008
+ message: "Discovery mode is read-only. File writes are restricted to plan files and /tmp/ scratch files \u2014 write a deliverable under /tmp/ and attach it with upload_attachment."
11869
12009
  };
11870
12010
  }
11871
12011
  if (toolName === "Bash") {
@@ -12024,7 +12164,7 @@ var DENIAL_FORCE_STOP_THRESHOLD = 8;
12024
12164
  function handleDenialEscalation(host, consecutiveDenials) {
12025
12165
  if (consecutiveDenials === DENIAL_WARNING_THRESHOLD) {
12026
12166
  host.connection.postChatMessage(
12027
- `\u26A0\uFE0F Multiple tool denials detected. You are in ${host.agentMode} mode \u2014 file writes outside .claude/plans/ are not permitted. Focus on creating a plan instead of implementing code changes.`
12167
+ `\u26A0\uFE0F Multiple tool denials detected. You are in ${host.agentMode} mode \u2014 file writes outside .claude/plans/ and /tmp/ are not permitted. Focus on creating a plan instead of implementing code changes.`
12028
12168
  );
12029
12169
  }
12030
12170
  if (consecutiveDenials >= DENIAL_FORCE_STOP_THRESHOLD) {
@@ -12070,6 +12210,9 @@ function checkToolLoop(host, toolName, input) {
12070
12210
  return { behavior: "deny", message: buildRepeatLoopMessage(repeats, isHeavyGateActive()) };
12071
12211
  }
12072
12212
  function resolveToolAccess(host, toolName, input) {
12213
+ if (toolName === "Artifact") {
12214
+ return { behavior: "deny", message: ARTIFACT_DENIED };
12215
+ }
12073
12216
  switch (host.agentMode) {
12074
12217
  case "discovery":
12075
12218
  case "help":
@@ -13784,6 +13927,100 @@ var BackgroundWorkTracker = class {
13784
13927
  }
13785
13928
  };
13786
13929
 
13930
+ // src/runner/scheduled-wakeup.ts
13931
+ var SCHEDULE_WAKEUP_TOOL = "schedulewakeup";
13932
+ var SCHEDULED_WAKEUP_MIN_DELAY_SECONDS = 60;
13933
+ var SCHEDULED_WAKEUP_MAX_DELAY_SECONDS = 3600;
13934
+ var SCHEDULED_WAKEUP_GRACE_MS = 2 * 60 * 1e3;
13935
+ function asRecord3(input) {
13936
+ if (typeof input === "string") {
13937
+ try {
13938
+ const parsed = JSON.parse(input);
13939
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
13940
+ } catch {
13941
+ return null;
13942
+ }
13943
+ }
13944
+ return typeof input === "object" && input !== null ? input : null;
13945
+ }
13946
+ function parseScheduledWakeup(tool2, input, now = Date.now()) {
13947
+ if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return null;
13948
+ const record = asRecord3(input);
13949
+ if (!record) return null;
13950
+ if (record.stop === true) return null;
13951
+ const raw = record.delaySeconds;
13952
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
13953
+ const delaySeconds = Math.min(
13954
+ SCHEDULED_WAKEUP_MAX_DELAY_SECONDS,
13955
+ Math.max(SCHEDULED_WAKEUP_MIN_DELAY_SECONDS, raw)
13956
+ );
13957
+ return { delaySeconds, deadline: now + delaySeconds * 1e3 + SCHEDULED_WAKEUP_GRACE_MS };
13958
+ }
13959
+ function isScheduledWakeupStop(tool2, input) {
13960
+ if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return false;
13961
+ return asRecord3(input)?.stop === true;
13962
+ }
13963
+ var ScheduledWakeupTracker = class {
13964
+ /** Absolute expiry (epoch ms) of the armed wakeup; null when none. */
13965
+ deadline = null;
13966
+ onChange;
13967
+ log;
13968
+ constructor(options = {}) {
13969
+ this.onChange = options.onChange;
13970
+ this.log = options.log ?? ((message) => process.stderr.write(message));
13971
+ }
13972
+ /**
13973
+ * Feed one `tool_use` from the turn stream. Returns true when it changed the
13974
+ * armed state (test/diagnostic signal; callers can ignore it).
13975
+ */
13976
+ noteToolUse(tool2, input, now = Date.now()) {
13977
+ if (isScheduledWakeupStop(tool2, input)) {
13978
+ if (this.deadline === null) return false;
13979
+ this.deadline = null;
13980
+ this.log("[conveyor-agent] scheduled wakeup stopped \u2014 heartbeats may report idle again\n");
13981
+ this.onChange?.();
13982
+ return true;
13983
+ }
13984
+ const arm = parseScheduledWakeup(tool2, input, now);
13985
+ if (!arm) return false;
13986
+ this.deadline = arm.deadline;
13987
+ this.log(
13988
+ `[conveyor-agent] scheduled wakeup armed (${arm.delaySeconds}s) \u2014 heartbeats stay non-idle until it fires
13989
+ `
13990
+ );
13991
+ this.onChange?.();
13992
+ return true;
13993
+ }
13994
+ /** Is a non-expired wakeup armed? */
13995
+ hasPending(now = Date.now()) {
13996
+ this.prune(now);
13997
+ return this.deadline !== null;
13998
+ }
13999
+ /** Absolute expiry of the armed wakeup, or null when none is armed. Callers
14000
+ * bound their own deferrals by this rather than by a fixed cap — a one-hour
14001
+ * arm outlives `BACKGROUND_WORK_MAX_MS`. */
14002
+ pendingDeadline(now = Date.now()) {
14003
+ this.prune(now);
14004
+ return this.deadline;
14005
+ }
14006
+ /** Drop the arm (a turn started, or the session is shutting down). */
14007
+ clear() {
14008
+ if (this.deadline === null) return;
14009
+ this.deadline = null;
14010
+ this.onChange?.();
14011
+ }
14012
+ /** Expire a deadline that has passed, loudly — the wakeup should have fired
14013
+ * a turn by now, so a tripped expiry is worth seeing in the log. */
14014
+ prune(now) {
14015
+ if (this.deadline === null || this.deadline > now) return;
14016
+ this.deadline = null;
14017
+ this.log(
14018
+ "[conveyor-agent] scheduled wakeup deadline passed with no turn \u2014 reporting idle again\n"
14019
+ );
14020
+ this.onChange?.();
14021
+ }
14022
+ };
14023
+
13787
14024
  // src/runner/live-children.ts
13788
14025
  function findLiveChild(sources) {
13789
14026
  for (const source of sources) {
@@ -13851,6 +14088,13 @@ var SessionRunner = class _SessionRunner {
13851
14088
  backgroundWork = new BackgroundWorkTracker({
13852
14089
  onChange: () => this.refreshLoopStatus()
13853
14090
  });
14091
+ /** An armed `ScheduleWakeup` (the dynamic `/loop` mode): the turn ended, but
14092
+ * the agent asked to be re-invoked at a deadline. Same liveness contract as
14093
+ * `backgroundWork` — while it reports pending, heartbeats go out non-idle so
14094
+ * the reconciler cannot sleep the pod before the wakeup fires. */
14095
+ scheduledWakeup = new ScheduledWakeupTracker({
14096
+ onChange: () => this.refreshLoopStatus()
14097
+ });
13854
14098
  /** Boot supervisor handle, set post-construction once it's known (it is
13855
14099
  * started alongside connect(), before the runner itself exists in some
13856
14100
  * call sites) — notified when the core loop goes live so it can release
@@ -13895,6 +14139,7 @@ var SessionRunner = class _SessionRunner {
13895
14139
  },
13896
14140
  onIdleTimeout: () => {
13897
14141
  if (this.deferShutdownForLiveChild("idle")) return;
14142
+ if (this.deferShutdownForScheduledWakeup("idle")) return;
13898
14143
  if (this.deferShutdownForBackgroundWork()) return;
13899
14144
  process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
13900
14145
  this.stopped = true;
@@ -13907,6 +14152,7 @@ var SessionRunner = class _SessionRunner {
13907
14152
  },
13908
14153
  onDormantTimeout: () => {
13909
14154
  if (this.deferShutdownForLiveChild("dormant")) return;
14155
+ if (this.deferShutdownForScheduledWakeup("dormant")) return;
13910
14156
  process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
13911
14157
  this.stopped = true;
13912
14158
  this.queryBridge?.stop();
@@ -13966,6 +14212,50 @@ var SessionRunner = class _SessionRunner {
13966
14212
  const label = timer === "idle" ? "Idle" : "Dormant idle";
13967
14213
  process.stderr.write(
13968
14214
  `[conveyor-agent] ${label} timeout deferred: ${live.kind} session ${live.sessionId} active
14215
+ `
14216
+ );
14217
+ if (timer === "idle") {
14218
+ this.lifecycle.startIdleTimer(recheckMs);
14219
+ } else {
14220
+ this.dormantDeadline = Date.now() + recheckMs;
14221
+ this.lifecycle.startDormantTimer(recheckMs);
14222
+ }
14223
+ return true;
14224
+ }
14225
+ /**
14226
+ * Suppress an idle or dormant shutdown while a `ScheduleWakeup` is armed.
14227
+ *
14228
+ * Both timers matter. The idle timer (30 min) kills a loop that paces itself
14229
+ * slower than that, and the dormant timer (60 min after a `completed` event)
14230
+ * kills a one-hour arm — the agent declares completion at the end of every
14231
+ * tick, so a dynamic loop sits dormant by construction.
14232
+ *
14233
+ * The bound is the WAKEUP'S OWN deadline, not `BACKGROUND_WORK_MAX_MS`: the
14234
+ * tool clamps `delaySeconds` to one hour, which already exceeds that 45-min
14235
+ * cap, so reusing it would shut the pod down before the longest legal arm
14236
+ * fired. Once the deadline passes the tracker expires it and this returns
14237
+ * false, so a lost timer cannot pin a pod.
14238
+ */
14239
+ deferShutdownForScheduledWakeup(timer) {
14240
+ let deadline;
14241
+ try {
14242
+ deadline = this.scheduledWakeup.pendingDeadline();
14243
+ } catch (err) {
14244
+ process.stderr.write(
14245
+ `[conveyor-agent] Scheduled-wakeup probe failed, not deferring: ${err}
14246
+ `
14247
+ );
14248
+ return false;
14249
+ }
14250
+ if (deadline === null) return false;
14251
+ const remainingMs = Math.max(0, deadline - Date.now());
14252
+ const recheckMs = Math.min(
14253
+ Math.max(remainingMs, 1),
14254
+ Math.max(this.lifecycle.config.idleTimeoutMs, 1)
14255
+ );
14256
+ const label = timer === "idle" ? "Idle" : "Dormant idle";
14257
+ process.stderr.write(
14258
+ `[conveyor-agent] ${label} timeout deferred: scheduled wakeup armed (${Math.round(remainingMs / 1e3)}s remaining)
13969
14259
  `
13970
14260
  );
13971
14261
  if (timer === "idle") {
@@ -14895,6 +15185,7 @@ var SessionRunner = class _SessionRunner {
14895
15185
  });
14896
15186
  this.portDiscovery.stop();
14897
15187
  this.backgroundWork.clear();
15188
+ this.scheduledWakeup.clear();
14898
15189
  this.loopLag.stop();
14899
15190
  this.lifecycle.destroy();
14900
15191
  this.connection.disconnect();
@@ -15135,6 +15426,7 @@ ${outcome.failures.join("\n")}
15135
15426
  if (this.questionPendingSince !== null && Date.now() - this.questionPendingSince < resolveQuestionGraceMs() + QUESTION_HOLD_SLACK_MS) {
15136
15427
  return "waiting";
15137
15428
  }
15429
+ if (this.scheduledWakeup.hasPending()) return "waiting";
15138
15430
  return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
15139
15431
  }
15140
15432
  // ── Question grace hold ────────────────────────────────────────────
@@ -15205,11 +15497,12 @@ ${outcome.failures.join("\n")}
15205
15497
  this.loopLag.setStatus(loopStatus);
15206
15498
  return loopStatus;
15207
15499
  }
15208
- /** One `tool_use` from the turn stream — registers a background launch when
15209
- * the tool starts work that outlives the turn. */
15500
+ /** One `tool_use` from the turn stream — registers work that outlives the
15501
+ * turn: a background launch, or an armed `ScheduleWakeup`. */
15210
15502
  noteToolUseForBackgroundWork(event) {
15211
15503
  if (event.type !== "tool_use" || typeof event.tool !== "string") return;
15212
15504
  this.backgroundWork.noteToolUse(event.tool, event.input);
15505
+ this.scheduledWakeup.noteToolUse(event.tool, event.input);
15213
15506
  }
15214
15507
  async setState(status) {
15215
15508
  if (status === "running") {
@@ -15250,6 +15543,7 @@ ${outcome.failures.join("\n")}
15250
15543
  /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
15251
15544
  * prefilled PTY callback already emitted `running` before it reaches here. */
15252
15545
  beginRunningEpisode() {
15546
+ this.scheduledWakeup.clear();
15253
15547
  this.activeEpisodeStartedAt = Date.now();
15254
15548
  this.backgroundDeferStartedAt = null;
15255
15549
  this.backgroundWakeFiredThisIdle = false;
@@ -15261,6 +15555,7 @@ ${outcome.failures.join("\n")}
15261
15555
  this.clearQuestionHold();
15262
15556
  this.portDiscovery.stop();
15263
15557
  this.backgroundWork.clear();
15558
+ this.scheduledWakeup.clear();
15264
15559
  this.loopLag.stop();
15265
15560
  this.lifecycle.destroy();
15266
15561
  try {
@@ -8,7 +8,7 @@ import {
8
8
  readWorkspaceBytes,
9
9
  statWorkspacePath,
10
10
  workspacePathExists
11
- } from "./chunk-I7QYTA2V.js";
11
+ } from "./chunk-YNI2HFZE.js";
12
12
  import {
13
13
  reportBootMilestone
14
14
  } from "./chunk-Q4FQOJ7D.js";
@@ -26,6 +26,9 @@ function buildSpawnArgs(input) {
26
26
  args.push("--permission-mode", "plan");
27
27
  }
28
28
  args.push("--settings", input.settingsPath);
29
+ if (input.disallowedTools && input.disallowedTools.length > 0) {
30
+ args.push("--disallowed-tools", input.disallowedTools.join(","));
31
+ }
29
32
  if (input.appendSystemPrompt) {
30
33
  args.push("--append-system-prompt", input.appendSystemPrompt);
31
34
  }
@@ -1806,6 +1806,13 @@ var TurnEndToolCallSchema = z22.object({
1806
1806
  output: z22.string().optional(),
1807
1807
  timestamp: z22.string().optional()
1808
1808
  }).passthrough();
1809
+ var AgentTurnUsageSchema = z22.object({
1810
+ inputTokens: z22.number(),
1811
+ outputTokens: z22.number(),
1812
+ cacheReadInputTokens: z22.number(),
1813
+ cacheCreationInputTokens: z22.number(),
1814
+ totalTokens: z22.number()
1815
+ });
1809
1816
  var KnownAgentEventSchema = z22.discriminatedUnion("type", [
1810
1817
  // ── Lifecycle / connection ────────────────────────────────────────────
1811
1818
  z22.object({
@@ -1845,7 +1852,11 @@ var KnownAgentEventSchema = z22.discriminatedUnion("type", [
1845
1852
  z22.object({
1846
1853
  type: z22.literal("completed"),
1847
1854
  summary: z22.string().optional(),
1848
- durationMs: z22.number().optional()
1855
+ durationMs: z22.number().optional(),
1856
+ /** Tokens this turn consumed, summed from the transcript's assistant
1857
+ * records. Optional: an older agent build sends none, and the server
1858
+ * then records nothing rather than zeroing the session's totals. */
1859
+ usage: AgentTurnUsageSchema.optional()
1849
1860
  }).passthrough(),
1850
1861
  z22.object({ type: z22.literal("error"), message: z22.string() }).passthrough(),
1851
1862
  z22.object({ type: z22.literal("agent_typing_start") }).passthrough(),
@@ -3352,6 +3363,10 @@ var CreateProjectDecisionRequestSchema = z11.object({
3352
3363
  sessionId: z11.string().optional(),
3353
3364
  tagNames: z11.array(z11.string().min(1)).max(10).optional(),
3354
3365
  raisedByPr: z11.number().int().positive().optional(),
3366
+ /** Slug or id of the card that raised the question. A link back, not a block. */
3367
+ raisedByCard: z11.string().min(1).optional(),
3368
+ /** Absolute deadline (ISO 8601). Wins over `deadlineHours` when both are sent. */
3369
+ deadlineAt: z11.string().datetime({ offset: true }).optional(),
3355
3370
  requestingUserId: z11.string().optional()
3356
3371
  });
3357
3372
  var ListProjectDecisionsRequestSchema = z11.object({
@@ -3387,7 +3402,16 @@ var ResolveProjectDecisionRequestSchema = z11.object({
3387
3402
  var UpdateProjectDecisionRequestSchema = z11.object({
3388
3403
  projectId: z11.string(),
3389
3404
  decision: z11.string().min(1),
3405
+ question: z11.string().min(1).max(200).optional(),
3406
+ summary: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
3407
+ context: z11.string().min(1).optional(),
3408
+ /** Accepted only while nobody has voted. */
3409
+ options: optionsInput.optional(),
3410
+ defaultOption: z11.number().int().min(1).max(DECISION_OPTIONS_MAX).optional(),
3411
+ tagNames: z11.array(z11.string().min(1)).max(10).optional(),
3390
3412
  deadlineHours,
3413
+ /** Absolute deadline (ISO 8601). Wins over `deadlineHours` when both are sent. */
3414
+ deadlineAt: z11.string().datetime({ offset: true }).optional(),
3391
3415
  addImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3392
3416
  removeImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3393
3417
  status: z11.enum(["Cancelled", "Open"]).optional(),
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-LBB7DXOC.js";
10
+ } from "./chunk-IWWBG5FI.js";
11
11
  import {
12
12
  PtyHarness,
13
13
  SessionRunner,
@@ -25,13 +25,13 @@ import {
25
25
  runUsageProbe,
26
26
  sampleKeyUsage,
27
27
  usesNativeUsageReporting
28
- } from "./chunk-XJSW42TM.js";
28
+ } from "./chunk-HW6SHIL7.js";
29
29
  import "./chunk-37J5MMQT.js";
30
30
  import {
31
31
  inheritedEnv,
32
32
  resolvePtySpawn,
33
33
  sessionTempBase
34
- } from "./chunk-W5INK3NE.js";
34
+ } from "./chunk-WCJ6HRVU.js";
35
35
  import {
36
36
  AgentConnection,
37
37
  CodespacePortVisibility,
@@ -44,7 +44,7 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-I7QYTA2V.js";
47
+ } from "./chunk-YNI2HFZE.js";
48
48
  import "./chunk-Q4FQOJ7D.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-372R6E4C.js";
@@ -1205,7 +1205,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1205
1205
 
1206
1206
  // src/cli.ts
1207
1207
  if (process.argv[2] === "boot") {
1208
- const { runBoot } = await import("./boot-S73ZNWBJ.js");
1208
+ const { runBoot } = await import("./boot-65PTSADA.js");
1209
1209
  process.exit(await runBoot(process.argv.slice(3)));
1210
1210
  }
1211
1211
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1307,7 +1307,7 @@ process.on("unhandledRejection", (reason) => {
1307
1307
  process.exit(1);
1308
1308
  });
1309
1309
  if (process.env.CONVEYOR_MODE === "workbench") {
1310
- const { startWorkbenchServer } = await import("./server-L5CQ5EJB.js");
1310
+ const { startWorkbenchServer } = await import("./server-E6IYHSMN.js");
1311
1311
  const { oomWatchdogOptionsFromEnv } = await import("./oom-watchdog-PAC5OJJG.js");
1312
1312
  const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QBCYO4GI.js");
1313
1313
  const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
@@ -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-ODH57W6I.js");
1461
+ const { runServingSession } = await import("./serve-boot-THPACDVI.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.d.ts CHANGED
@@ -732,6 +732,11 @@ declare class SessionRunner {
732
732
  * it reports pending, heartbeats go out non-idle so the workspace activity
733
733
  * clock keeps being bumped and the reconciler can't sleep the pod mid-gate. */
734
734
  private readonly backgroundWork;
735
+ /** An armed `ScheduleWakeup` (the dynamic `/loop` mode): the turn ended, but
736
+ * the agent asked to be re-invoked at a deadline. Same liveness contract as
737
+ * `backgroundWork` — while it reports pending, heartbeats go out non-idle so
738
+ * the reconciler cannot sleep the pod before the wakeup fires. */
739
+ private readonly scheduledWakeup;
735
740
  /** Boot supervisor handle, set post-construction once it's known (it is
736
741
  * started alongside connect(), before the runner itself exists in some
737
742
  * call sites) — notified when the core loop goes live so it can release
@@ -782,6 +787,21 @@ declare class SessionRunner {
782
787
  * live build costs work.
783
788
  */
784
789
  private deferShutdownForLiveChild;
790
+ /**
791
+ * Suppress an idle or dormant shutdown while a `ScheduleWakeup` is armed.
792
+ *
793
+ * Both timers matter. The idle timer (30 min) kills a loop that paces itself
794
+ * slower than that, and the dormant timer (60 min after a `completed` event)
795
+ * kills a one-hour arm — the agent declares completion at the end of every
796
+ * tick, so a dynamic loop sits dormant by construction.
797
+ *
798
+ * The bound is the WAKEUP'S OWN deadline, not `BACKGROUND_WORK_MAX_MS`: the
799
+ * tool clamps `delaySeconds` to one hour, which already exceeds that 45-min
800
+ * cap, so reusing it would shut the pod down before the longest legal arm
801
+ * fired. Once the deadline passes the tracker expires it and this returns
802
+ * false, so a lost timer cannot pin a pod.
803
+ */
804
+ private deferShutdownForScheduledWakeup;
785
805
  /** Epoch ms of the first idle-shutdown deferral for background work in the
786
806
  * current idle episode; null when nothing has deferred. Reset when a turn
787
807
  * runs, so each idle episode gets its own bounded hold. */
@@ -1101,8 +1121,8 @@ declare class SessionRunner {
1101
1121
  /** Mirror the resolved status into the loop-lag buffer so the starvation-proof
1102
1122
  * heartbeat worker reports it too. Returns what it wrote. */
1103
1123
  private refreshLoopStatus;
1104
- /** One `tool_use` from the turn stream — registers a background launch when
1105
- * the tool starts work that outlives the turn. */
1124
+ /** One `tool_use` from the turn stream — registers work that outlives the
1125
+ * turn: a background launch, or an armed `ScheduleWakeup`. */
1106
1126
  private noteToolUseForBackgroundWork;
1107
1127
  private setState;
1108
1128
  /**
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-XJSW42TM.js";
4
+ } from "./chunk-HW6SHIL7.js";
5
5
  import "./chunk-37J5MMQT.js";
6
- import "./chunk-W5INK3NE.js";
6
+ import "./chunk-WCJ6HRVU.js";
7
7
  import {
8
8
  AgentConnection,
9
9
  GIT_TIMEOUT_MS,
@@ -17,7 +17,7 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-I7QYTA2V.js";
20
+ } from "./chunk-YNI2HFZE.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-LBB7DXOC.js";
3
+ } from "./chunk-IWWBG5FI.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-I7QYTA2V.js";
14
+ } from "./chunk-YNI2HFZE.js";
15
15
  import "./chunk-Q4FQOJ7D.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startWorkbenchServer
3
- } from "./chunk-EIHZRSYI.js";
4
- import "./chunk-W5INK3NE.js";
3
+ } from "./chunk-EITUTXXJ.js";
4
+ import "./chunk-WCJ6HRVU.js";
5
5
  import "./chunk-W4LZ7R6Z.js";
6
6
  import "./chunk-372R6E4C.js";
7
7
  import "./chunk-SQM2BQ7H.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.23",
3
+ "version": "11.0.24",
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",
@@ -157,6 +157,18 @@ and the order to verify in. Write it last, when you know which parts were hard.
157
157
  It is not optional and it is not a summary. It is the only channel your context
158
158
  has across the handoff, because handoffs carry the card and never the session.
159
159
 
160
+ **Deliverables beyond the plan go on the card.** A diagram, an HTML report, a
161
+ screenshot, or a data export the team should keep is attached with
162
+ `mcp__conveyor__upload_attachment` — never published as a Claude artifact, which
163
+ is an off-platform link the card cannot render. Write the file under `/tmp/`
164
+ first (a Planner session may write there; the repo checkout stays read-only) and
165
+ pass the caption so the file shows in the card thread rather than only in the
166
+ Files grid. The caption field is spelled differently per surface, and both
167
+ surfaces accept either name: in a pod pass `path` + `title`; on the local MCP
168
+ pass `taskId` + `path` + `comment` (or `title`). An HTML deliverable must be
169
+ self-contained — inline CSS, no `<script>`, no external assets — because the
170
+ card serves it under `sandbox; default-src 'none'`.
171
+
160
172
  Size it: 1 SP default, 2 multi-file, 3 complex patterns/design, 5 hard. Larger
161
173
  → propose a pack (parent + `mcp__conveyor__create_subtask` children) instead of
162
174
  one mega-card. **Child→child ordering rides `create_subtask`/`update_subtask`'s