@rallycry/conveyor-agent 11.0.23 → 11.0.25

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,
@@ -8,7 +8,7 @@ import {
8
8
  readWorkspaceBytes,
9
9
  statWorkspacePath,
10
10
  workspacePathExists
11
- } from "./chunk-I7QYTA2V.js";
11
+ } from "./chunk-BZZ4B37Q.js";
12
12
  import {
13
13
  reportBootMilestone
14
14
  } from "./chunk-Q4FQOJ7D.js";
@@ -1446,14 +1446,14 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1446
1446
  }
1447
1447
  };
1448
1448
 
1449
- // ../shared/dist/chunk-6NPOGQPP.js
1449
+ // ../shared/dist/chunk-U3HGG5NX.js
1450
1450
  import { z } from "zod";
1451
1451
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
1452
- var DEFAULT_OPUS_MODEL = "claude-opus-5";
1452
+ var DEFAULT_OPUS_MODEL = "claude-opus-5-5";
1453
1453
  var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
1454
1454
  var FABLE_MODEL = "claude-fable-5-1";
1455
1455
  var PREVIOUS_SONNET_MODEL = "claude-sonnet-4-6";
1456
- var PREVIOUS_OPUS_MODEL = "claude-opus-4-8";
1456
+ var PREVIOUS_OPUS_MODEL = "claude-opus-5";
1457
1457
  var PTY_STREAM_PORT_BASE = 7420;
1458
1458
  var PTY_STREAM_PORT_ATTEMPTS = 8;
1459
1459
  function encodePtyStreamFrame(frame) {
@@ -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(),
@@ -3296,6 +3307,10 @@ var CancelDecisionRequestSchema = z10.object({
3296
3307
  /** Why it is being dropped. Posted to the decision thread. */
3297
3308
  reason: z10.string().max(DECISION_RESOLUTION_MAX).optional()
3298
3309
  });
3310
+ var BulkDecisionIdsRequestSchema = z10.object({
3311
+ projectId: z10.string().cuid("Invalid project ID"),
3312
+ decisionIds: z10.array(z10.string().cuid()).min(1).max(200)
3313
+ });
3299
3314
  var ReopenDecisionRequestSchema = z10.object({
3300
3315
  decisionId: decisionIdSchema,
3301
3316
  deadlineHours: z10.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional()
@@ -3352,6 +3367,10 @@ var CreateProjectDecisionRequestSchema = z11.object({
3352
3367
  sessionId: z11.string().optional(),
3353
3368
  tagNames: z11.array(z11.string().min(1)).max(10).optional(),
3354
3369
  raisedByPr: z11.number().int().positive().optional(),
3370
+ /** Slug or id of the card that raised the question. A link back, not a block. */
3371
+ raisedByCard: z11.string().min(1).optional(),
3372
+ /** Absolute deadline (ISO 8601). Wins over `deadlineHours` when both are sent. */
3373
+ deadlineAt: z11.string().datetime({ offset: true }).optional(),
3355
3374
  requestingUserId: z11.string().optional()
3356
3375
  });
3357
3376
  var ListProjectDecisionsRequestSchema = z11.object({
@@ -3387,7 +3406,16 @@ var ResolveProjectDecisionRequestSchema = z11.object({
3387
3406
  var UpdateProjectDecisionRequestSchema = z11.object({
3388
3407
  projectId: z11.string(),
3389
3408
  decision: z11.string().min(1),
3409
+ question: z11.string().min(1).max(200).optional(),
3410
+ summary: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
3411
+ context: z11.string().min(1).optional(),
3412
+ /** Accepted only while nobody has voted. */
3413
+ options: optionsInput.optional(),
3414
+ defaultOption: z11.number().int().min(1).max(DECISION_OPTIONS_MAX).optional(),
3415
+ tagNames: z11.array(z11.string().min(1)).max(10).optional(),
3390
3416
  deadlineHours,
3417
+ /** Absolute deadline (ISO 8601). Wins over `deadlineHours` when both are sent. */
3418
+ deadlineAt: z11.string().datetime({ offset: true }).optional(),
3391
3419
  addImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3392
3420
  removeImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3393
3421
  status: z11.enum(["Cancelled", "Open"]).optional(),
@@ -3548,8 +3576,8 @@ function anthropicEntry(model, label, inputPerMillion, outputPerMillion, opts =
3548
3576
  };
3549
3577
  }
3550
3578
  var ANTHROPIC_CATALOG = [
3551
- anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5 Latest", 5, 25),
3552
- anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 4.8", 5, 25),
3579
+ anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5.5 Latest", 4, 20),
3580
+ anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 5", 5, 25),
3553
3581
  anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest", 3, 15),
3554
3582
  anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 4.6", 3, 15),
3555
3583
  // The Haiku line (4.5 and older) predates the tuning surface and 400s on it.
@@ -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-BZZ4B37Q.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);
@@ -4903,6 +4912,9 @@ var CodexTuiAdapter = class {
4903
4912
  }
4904
4913
  const config = {
4905
4914
  cli_auth_credentials_store: "file",
4915
+ // The pod image owns CLI upgrades. An interactive update would try to
4916
+ // replace the root-owned global package from the conveyor user account.
4917
+ check_for_update_on_startup: false,
4906
4918
  sqlite_home: codexSqliteHome(this.env),
4907
4919
  approval_policy: "never",
4908
4920
  // Always full access, plan mode included. Codex's own read-only mode is
@@ -5641,7 +5653,8 @@ function buildDiscoveryPrompt(context, runnerMode) {
5641
5653
  ] : [],
5642
5654
  ``,
5643
5655
  `### 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).`,
5656
+ `- 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).`,
5657
+ `- 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
5658
  `- A plan file on disk is NOT the plan. Only \`update_task\` saves it to the card.`,
5646
5659
  `- **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
5660
  `- ExitPlanMode does NOT start building. It parks this session for human review; the team decides when to build.`,
@@ -6340,7 +6353,7 @@ Working rules:`,
6340
6353
  `- 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
6354
  `- 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
6355
  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.`,
6356
+ 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
6357
  `
6345
6358
  Git:`,
6346
6359
  `- 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 +7515,11 @@ var uploadAttachmentContract = defineToolContract({
7502
7515
  title: f.optional(
7503
7516
  f.string({ desc: "Short caption posted with the file (defaults to the file name)" })
7504
7517
  ),
7518
+ comment: f.optional(
7519
+ f.string({
7520
+ desc: "Alias of `title` (the external conveyor-mcp surface names this field `comment`). Provide at most one of the two."
7521
+ })
7522
+ ),
7505
7523
  tags: attachmentTags
7506
7524
  }
7507
7525
  },
@@ -7514,6 +7532,11 @@ var uploadAttachmentContract = defineToolContract({
7514
7532
  comment: f.optional(
7515
7533
  f.string({ desc: "When set, also posts the attachment to the task chat with this text" })
7516
7534
  ),
7535
+ title: f.optional(
7536
+ f.string({
7537
+ desc: "Alias of `comment` (the in-pod agent surface names this field `title`). Provide at most one of the two."
7538
+ })
7539
+ ),
7517
7540
  tags: attachmentTags,
7518
7541
  mimeType: f.optional(
7519
7542
  f.string({ desc: "Override the mime type inferred from the file extension" })
@@ -7573,7 +7596,7 @@ var agentOption = f.object(
7573
7596
  var createDecisionContract = defineToolContract({
7574
7597
  name: "create_decision",
7575
7598
  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.`,
7599
+ 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
7600
  fields: {
7578
7601
  question: f.string({
7579
7602
  min: 1,
@@ -7610,6 +7633,16 @@ var createDecisionContract = defineToolContract({
7610
7633
  ),
7611
7634
  raised_by_pr: f.optional(
7612
7635
  f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7636
+ ),
7637
+ raised_by_card: f.optional(
7638
+ f.string({
7639
+ 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."
7640
+ })
7641
+ ),
7642
+ deadline_at: f.optional(
7643
+ f.string({
7644
+ 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."
7645
+ })
7613
7646
  )
7614
7647
  }
7615
7648
  },
@@ -7633,6 +7666,16 @@ var createDecisionContract = defineToolContract({
7633
7666
  tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
7634
7667
  raisedByPr: f.optional(
7635
7668
  f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7669
+ ),
7670
+ raisedByCard: f.optional(
7671
+ f.string({
7672
+ desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impactedCards to block a card."
7673
+ })
7674
+ ),
7675
+ deadlineAt: f.optional(
7676
+ f.string({
7677
+ desc: "Absolute deadline (ISO 8601). Wins over deadlineHours when both are given; must be in the future and within 720 hours."
7678
+ })
7636
7679
  )
7637
7680
  }
7638
7681
  }
@@ -7640,7 +7683,7 @@ var createDecisionContract = defineToolContract({
7640
7683
  var listDecisionsContract = defineToolContract({
7641
7684
  name: "list_decisions",
7642
7685
  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.`,
7686
+ 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
7687
  fields: {
7645
7688
  status: f.optional(
7646
7689
  f.enum(STATUS_LABELS, {
@@ -7672,7 +7715,7 @@ var listDecisionsContract = defineToolContract({
7672
7715
  var getDecisionContract = defineToolContract({
7673
7716
  name: "get_decision",
7674
7717
  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.",
7718
+ 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
7719
  fields: {
7677
7720
  decision: f.string({ min: 1, desc: "Decision slug or id" })
7678
7721
  }
@@ -7737,10 +7780,32 @@ var resolveDecisionContract = defineToolContract({
7737
7780
  var updateDecisionContract = defineToolContract({
7738
7781
  name: "update_decision",
7739
7782
  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.",
7783
+ 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
7784
  fields: {
7742
7785
  decision: f.string({ min: 1, desc: "Decision slug or id" }),
7786
+ question: f.optional(f.string({ min: 1, desc: "New question, one plain sentence" })),
7787
+ summary: f.optional(
7788
+ f.string({ desc: cardDescriptionDesc("New one- or two-sentence summary") })
7789
+ ),
7790
+ context: f.optional(f.string({ min: 1, desc: "New markdown background" })),
7791
+ options: f.optional(
7792
+ f.array(agentOption, {
7793
+ min: 2,
7794
+ 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."
7795
+ })
7796
+ ),
7797
+ default_option: f.optional(
7798
+ f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
7799
+ ),
7800
+ tag_names: f.optional(
7801
+ f.array(f.string(), { desc: "Replacement tag names (replaces the current set)" })
7802
+ ),
7743
7803
  deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
7804
+ deadline_at: f.optional(
7805
+ f.string({
7806
+ desc: "New absolute deadline (ISO 8601). Wins over deadline_hours; must be in the future and within 720 hours."
7807
+ })
7808
+ ),
7744
7809
  add_impacted_cards: f.optional(
7745
7810
  f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7746
7811
  ),
@@ -7755,13 +7820,29 @@ var updateDecisionContract = defineToolContract({
7755
7820
  }
7756
7821
  },
7757
7822
  mcp: {
7758
- description: "Change an open Conveyor decision: extend the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
7823
+ 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
7824
  fields: {
7760
7825
  projectId: mcpProjectId,
7761
7826
  decision: f.string({ desc: "Decision slug or id" }),
7827
+ question: f.optional(f.string({ desc: "New question" })),
7828
+ summary: f.optional(f.string({ desc: cardDescriptionDesc("New summary") })),
7829
+ context: f.optional(f.string({ desc: "New markdown background" })),
7830
+ options: f.optional(
7831
+ f.array(agentOption, {
7832
+ min: 2,
7833
+ desc: "Replacement options. Accepted only while the decision has no votes. Send defaultOption with it: the old default no longer exists."
7834
+ })
7835
+ ),
7836
+ defaultOption: f.optional(
7837
+ f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
7838
+ ),
7839
+ tagNames: f.optional(f.array(f.string(), { desc: "Replacement tag names" })),
7762
7840
  deadlineHours: f.optional(
7763
7841
  f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
7764
7842
  ),
7843
+ deadlineAt: f.optional(
7844
+ f.string({ desc: "New absolute deadline (ISO 8601). Wins over deadlineHours." })
7845
+ ),
7765
7846
  addImpactedCards: f.optional(
7766
7847
  f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7767
7848
  ),
@@ -8506,10 +8587,11 @@ function buildReadTaskChatTool(connection) {
8506
8587
  taskId: task_id
8507
8588
  });
8508
8589
  return textResult(JSON.stringify(messages, null, 2));
8509
- } catch {
8590
+ } catch (error) {
8510
8591
  return textResult(
8511
8592
  JSON.stringify({
8512
- note: "Could not fetch live chat. Chat history was provided in the initial context."
8593
+ error: `Failed to read chat: ${error instanceof Error ? error.message : "Unknown error"}`,
8594
+ note: "Usually transient \u2014 retry. Chat history up to this session's start was provided in the initial context."
8513
8595
  })
8514
8596
  );
8515
8597
  }
@@ -9293,7 +9375,8 @@ Paste this into the PR description to show it inline:
9293
9375
  ${snippet}`;
9294
9376
  }
9295
9377
  function buildUploadAttachmentTool(connection, config) {
9296
- return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
9378
+ return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
9379
+ const caption = title ?? comment;
9297
9380
  try {
9298
9381
  const filePath = isAbsolute(path) ? path : join16(config.workspaceDir, path);
9299
9382
  const mimeType = inferMimeType(filePath);
@@ -9329,11 +9412,11 @@ function buildUploadAttachmentTool(connection, config) {
9329
9412
  const result = await connection.call("confirmFileUpload", {
9330
9413
  sessionId: connection.sessionId,
9331
9414
  fileId,
9332
- title,
9415
+ title: caption,
9333
9416
  tags
9334
9417
  });
9335
9418
  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)}`
9419
+ `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
9420
  );
9338
9421
  } catch (error) {
9339
9422
  return textResult(
@@ -10765,14 +10848,19 @@ function raiseAndVoteTools(connection, projectId) {
10765
10848
  includeCurrentCard: input.include_current_card,
10766
10849
  sessionId: connection.sessionId,
10767
10850
  tagNames: input.tag_names,
10768
- raisedByPr: input.raised_by_pr
10851
+ raisedByPr: input.raised_by_pr,
10852
+ raisedByCard: input.raised_by_card,
10853
+ deadlineAt: input.deadline_at
10769
10854
  });
10770
- const refused = res.refused.length > 0 ? `
10855
+ const { impactedLinked, refused, ...detail } = res;
10856
+ const refusedLine = refused.length > 0 ? `
10771
10857
 
10772
- Not linked: ${res.refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
10858
+ Not linked: ${refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
10773
10859
  return textResult(
10774
10860
  `Raised decision ${res.slug} \u2014 ${res.url}
10775
- Blocking ${res.impactedLinked.length} card(s).${refused}`
10861
+ Blocking ${impactedLinked.length} card(s).${refusedLine}
10862
+
10863
+ ${JSON.stringify(detail, null, 2)}`
10776
10864
  );
10777
10865
  } catch (error) {
10778
10866
  return errText5("Failed to raise the decision", error);
@@ -10817,7 +10905,14 @@ function settleTools(connection, projectId) {
10817
10905
  const res = await connection.call("updateProjectDecision", {
10818
10906
  projectId,
10819
10907
  decision: input.decision,
10908
+ question: input.question,
10909
+ summary: input.summary,
10910
+ context: input.context,
10911
+ options: input.options,
10912
+ defaultOption: input.default_option,
10913
+ tagNames: input.tag_names,
10820
10914
  deadlineHours: input.deadline_hours,
10915
+ deadlineAt: input.deadline_at,
10821
10916
  addImpactedCards: input.add_impacted_cards,
10822
10917
  removeImpactedCards: input.remove_impacted_cards,
10823
10918
  status: input.status
@@ -11216,6 +11311,26 @@ function safeVoid(promise, context) {
11216
11311
  });
11217
11312
  }
11218
11313
  }
11314
+ function addTurnUsage(total, usage) {
11315
+ const next = {
11316
+ inputTokens: total.inputTokens + (usage.input_tokens ?? 0),
11317
+ outputTokens: total.outputTokens + (usage.output_tokens ?? 0),
11318
+ cacheReadInputTokens: total.cacheReadInputTokens + (usage.cache_read_input_tokens ?? 0),
11319
+ cacheCreationInputTokens: total.cacheCreationInputTokens + (usage.cache_creation_input_tokens ?? 0),
11320
+ totalTokens: 0
11321
+ };
11322
+ next.totalTokens = next.inputTokens + next.outputTokens + next.cacheReadInputTokens + next.cacheCreationInputTokens;
11323
+ return next;
11324
+ }
11325
+ function emptyTurnUsage() {
11326
+ return {
11327
+ inputTokens: 0,
11328
+ outputTokens: 0,
11329
+ cacheReadInputTokens: 0,
11330
+ cacheCreationInputTokens: 0,
11331
+ totalTokens: 0
11332
+ };
11333
+ }
11219
11334
  function epochSecondsToISO(value) {
11220
11335
  if (typeof value === "string") return value;
11221
11336
  if (typeof value !== "number" || value <= 0) return void 0;
@@ -11297,11 +11412,16 @@ function emitContextUpdate(modelUsage, host, context, lastAssistantUsage) {
11297
11412
  });
11298
11413
  }
11299
11414
  }
11300
- function handleSuccessResult(event, host, context, startTime, lastAssistantUsage) {
11415
+ function handleSuccessResult(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11301
11416
  const durationMs = Date.now() - startTime;
11302
11417
  const summary = event.result || "Task completed.";
11303
11418
  const retriable = isRetriableMessage(summary);
11304
- host.connection.sendEvent({ type: "completed", summary, durationMs });
11419
+ host.connection.sendEvent({
11420
+ type: "completed",
11421
+ summary,
11422
+ durationMs,
11423
+ ...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
11424
+ });
11305
11425
  const { modelUsage } = event;
11306
11426
  if (modelUsage && typeof modelUsage === "object") {
11307
11427
  emitContextUpdate(modelUsage, host, context, lastAssistantUsage);
@@ -11322,7 +11442,7 @@ function handleErrorResult(event, host) {
11322
11442
  host.connection.sendEvent({ type: "error", message: errorMsg });
11323
11443
  return { retriable };
11324
11444
  }
11325
- function handleResultEvent(event, host, context, startTime, lastAssistantUsage) {
11445
+ function handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11326
11446
  const resultSummary = event.subtype === "success" ? event.result : event.errors.join(", ");
11327
11447
  if (event.subtype === "success") {
11328
11448
  const result2 = handleSuccessResult(
@@ -11330,15 +11450,16 @@ function handleResultEvent(event, host, context, startTime, lastAssistantUsage)
11330
11450
  host,
11331
11451
  context,
11332
11452
  startTime,
11333
- lastAssistantUsage
11453
+ lastAssistantUsage,
11454
+ turnUsage
11334
11455
  );
11335
11456
  return { ...result2, resultSummary };
11336
11457
  }
11337
11458
  const result = handleErrorResult(event, host);
11338
11459
  return { ...result, resultSummary };
11339
11460
  }
11340
- async function emitResultEvent(event, host, context, startTime, lastAssistantUsage) {
11341
- const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage);
11461
+ async function emitResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
11462
+ const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage);
11342
11463
  const durationMs = Date.now() - startTime;
11343
11464
  if (event.subtype === "success") {
11344
11465
  const successEvent = event;
@@ -11346,7 +11467,8 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
11346
11467
  await host.callbacks.onEvent({
11347
11468
  type: "completed",
11348
11469
  summary,
11349
- durationMs
11470
+ durationMs,
11471
+ ...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
11350
11472
  });
11351
11473
  } else if (!result.staleSession) {
11352
11474
  const errorEvent = event;
@@ -11452,13 +11574,20 @@ async function handleAssistantCase(event, host, turnToolCalls) {
11452
11574
  const msgUsage = event.message.usage;
11453
11575
  return msgUsage ?? void 0;
11454
11576
  }
11455
- async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage) {
11577
+ async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage, turnUsage) {
11456
11578
  let stoppedTyping = false;
11457
11579
  if (isTyping) {
11458
11580
  host.connection.sendTypingStop();
11459
11581
  stoppedTyping = true;
11460
11582
  }
11461
- const resultInfo = await emitResultEvent(event, host, context, startTime, lastAssistantUsage);
11583
+ const resultInfo = await emitResultEvent(
11584
+ event,
11585
+ host,
11586
+ context,
11587
+ startTime,
11588
+ lastAssistantUsage,
11589
+ turnUsage
11590
+ );
11462
11591
  return {
11463
11592
  retriable: resultInfo.retriable,
11464
11593
  resultSummary: resultInfo.resultSummary,
@@ -11562,7 +11691,10 @@ async function processAssistantCase(event, host, state) {
11562
11691
  state.isTyping = true;
11563
11692
  }
11564
11693
  const usage = await handleAssistantCase(event, host, state.turnToolCalls);
11565
- if (usage) state.lastAssistantUsage = usage;
11694
+ if (usage) {
11695
+ state.lastAssistantUsage = usage;
11696
+ state.turnUsage = addTurnUsage(state.turnUsage, usage);
11697
+ }
11566
11698
  if (!state.sawApiError || event.isApiError && !state.terminalApiError) {
11567
11699
  const fullText = event.message.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
11568
11700
  if (API_ERROR_PATTERN.test(fullText)) {
@@ -11581,7 +11713,8 @@ async function processResultCase(event, host, context, startTime, state) {
11581
11713
  context,
11582
11714
  startTime,
11583
11715
  state.isTyping,
11584
- state.lastAssistantUsage
11716
+ state.lastAssistantUsage,
11717
+ state.turnUsage
11585
11718
  );
11586
11719
  if (info.stoppedTyping) state.isTyping = false;
11587
11720
  state.retriable = info.retriable;
@@ -11614,6 +11747,7 @@ async function processEvents(events, context, host) {
11614
11747
  staleSession: void 0,
11615
11748
  authError: void 0,
11616
11749
  lastAssistantUsage: void 0,
11750
+ turnUsage: emptyTurnUsage(),
11617
11751
  turnToolCalls: [],
11618
11752
  questionPending: false
11619
11753
  };
@@ -11693,6 +11827,9 @@ function fallbackResetIso(rateLimitType, now = Date.now()) {
11693
11827
  return new Date(now + (weekly ? TWENTY_FOUR_HOURS_MS : FIVE_HOURS_MS)).toISOString();
11694
11828
  }
11695
11829
 
11830
+ // src/execution/tool-access.ts
11831
+ import { isAbsolute as isAbsolute3, posix } from "path";
11832
+
11696
11833
  // src/execution/task-property-utils.ts
11697
11834
  function collectMissingProps(taskProps) {
11698
11835
  const missing = [];
@@ -11835,6 +11972,12 @@ function isPlanFile(input) {
11835
11972
  const filePath = String(input.file_path ?? input.path ?? "");
11836
11973
  return filePath.includes(".claude/plans/");
11837
11974
  }
11975
+ function isScratchPath(input) {
11976
+ const filePath = String(input.file_path ?? input.path ?? "");
11977
+ if (!isAbsolute3(filePath)) return false;
11978
+ return posix.normalize(filePath).startsWith("/tmp/");
11979
+ }
11980
+ var ARTIFACT_DENIED = "Publish deliverables on the card with upload_attachment; the Artifact tool is not available in Conveyor sessions.";
11838
11981
  var READ_ONLY_BLOCKED_BASH = [
11839
11982
  { name: "git commit", re: /\bgit\s+(?:-\S+\s+)*commit\b/ },
11840
11983
  { name: "git push", re: /\bgit\s+(?:-\S+\s+)*push\b/ },
@@ -11860,12 +12003,12 @@ function handleReadOnlyToolAccess(toolName, input) {
11860
12003
  return { behavior: "deny", message: CHANNEL_POST_DENIED };
11861
12004
  }
11862
12005
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
11863
- if (isPlanFile(input)) {
12006
+ if (isPlanFile(input) || isScratchPath(input)) {
11864
12007
  return { behavior: "allow", updatedInput: input };
11865
12008
  }
11866
12009
  return {
11867
12010
  behavior: "deny",
11868
- message: "Discovery mode is read-only. File writes are restricted to plan files."
12011
+ 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
12012
  };
11870
12013
  }
11871
12014
  if (toolName === "Bash") {
@@ -12024,7 +12167,7 @@ var DENIAL_FORCE_STOP_THRESHOLD = 8;
12024
12167
  function handleDenialEscalation(host, consecutiveDenials) {
12025
12168
  if (consecutiveDenials === DENIAL_WARNING_THRESHOLD) {
12026
12169
  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.`
12170
+ `\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
12171
  );
12029
12172
  }
12030
12173
  if (consecutiveDenials >= DENIAL_FORCE_STOP_THRESHOLD) {
@@ -12070,6 +12213,9 @@ function checkToolLoop(host, toolName, input) {
12070
12213
  return { behavior: "deny", message: buildRepeatLoopMessage(repeats, isHeavyGateActive()) };
12071
12214
  }
12072
12215
  function resolveToolAccess(host, toolName, input) {
12216
+ if (toolName === "Artifact") {
12217
+ return { behavior: "deny", message: ARTIFACT_DENIED };
12218
+ }
12073
12219
  switch (host.agentMode) {
12074
12220
  case "discovery":
12075
12221
  case "help":
@@ -13784,6 +13930,100 @@ var BackgroundWorkTracker = class {
13784
13930
  }
13785
13931
  };
13786
13932
 
13933
+ // src/runner/scheduled-wakeup.ts
13934
+ var SCHEDULE_WAKEUP_TOOL = "schedulewakeup";
13935
+ var SCHEDULED_WAKEUP_MIN_DELAY_SECONDS = 60;
13936
+ var SCHEDULED_WAKEUP_MAX_DELAY_SECONDS = 3600;
13937
+ var SCHEDULED_WAKEUP_GRACE_MS = 2 * 60 * 1e3;
13938
+ function asRecord3(input) {
13939
+ if (typeof input === "string") {
13940
+ try {
13941
+ const parsed = JSON.parse(input);
13942
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
13943
+ } catch {
13944
+ return null;
13945
+ }
13946
+ }
13947
+ return typeof input === "object" && input !== null ? input : null;
13948
+ }
13949
+ function parseScheduledWakeup(tool2, input, now = Date.now()) {
13950
+ if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return null;
13951
+ const record = asRecord3(input);
13952
+ if (!record) return null;
13953
+ if (record.stop === true) return null;
13954
+ const raw = record.delaySeconds;
13955
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
13956
+ const delaySeconds = Math.min(
13957
+ SCHEDULED_WAKEUP_MAX_DELAY_SECONDS,
13958
+ Math.max(SCHEDULED_WAKEUP_MIN_DELAY_SECONDS, raw)
13959
+ );
13960
+ return { delaySeconds, deadline: now + delaySeconds * 1e3 + SCHEDULED_WAKEUP_GRACE_MS };
13961
+ }
13962
+ function isScheduledWakeupStop(tool2, input) {
13963
+ if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return false;
13964
+ return asRecord3(input)?.stop === true;
13965
+ }
13966
+ var ScheduledWakeupTracker = class {
13967
+ /** Absolute expiry (epoch ms) of the armed wakeup; null when none. */
13968
+ deadline = null;
13969
+ onChange;
13970
+ log;
13971
+ constructor(options = {}) {
13972
+ this.onChange = options.onChange;
13973
+ this.log = options.log ?? ((message) => process.stderr.write(message));
13974
+ }
13975
+ /**
13976
+ * Feed one `tool_use` from the turn stream. Returns true when it changed the
13977
+ * armed state (test/diagnostic signal; callers can ignore it).
13978
+ */
13979
+ noteToolUse(tool2, input, now = Date.now()) {
13980
+ if (isScheduledWakeupStop(tool2, input)) {
13981
+ if (this.deadline === null) return false;
13982
+ this.deadline = null;
13983
+ this.log("[conveyor-agent] scheduled wakeup stopped \u2014 heartbeats may report idle again\n");
13984
+ this.onChange?.();
13985
+ return true;
13986
+ }
13987
+ const arm = parseScheduledWakeup(tool2, input, now);
13988
+ if (!arm) return false;
13989
+ this.deadline = arm.deadline;
13990
+ this.log(
13991
+ `[conveyor-agent] scheduled wakeup armed (${arm.delaySeconds}s) \u2014 heartbeats stay non-idle until it fires
13992
+ `
13993
+ );
13994
+ this.onChange?.();
13995
+ return true;
13996
+ }
13997
+ /** Is a non-expired wakeup armed? */
13998
+ hasPending(now = Date.now()) {
13999
+ this.prune(now);
14000
+ return this.deadline !== null;
14001
+ }
14002
+ /** Absolute expiry of the armed wakeup, or null when none is armed. Callers
14003
+ * bound their own deferrals by this rather than by a fixed cap — a one-hour
14004
+ * arm outlives `BACKGROUND_WORK_MAX_MS`. */
14005
+ pendingDeadline(now = Date.now()) {
14006
+ this.prune(now);
14007
+ return this.deadline;
14008
+ }
14009
+ /** Drop the arm (a turn started, or the session is shutting down). */
14010
+ clear() {
14011
+ if (this.deadline === null) return;
14012
+ this.deadline = null;
14013
+ this.onChange?.();
14014
+ }
14015
+ /** Expire a deadline that has passed, loudly — the wakeup should have fired
14016
+ * a turn by now, so a tripped expiry is worth seeing in the log. */
14017
+ prune(now) {
14018
+ if (this.deadline === null || this.deadline > now) return;
14019
+ this.deadline = null;
14020
+ this.log(
14021
+ "[conveyor-agent] scheduled wakeup deadline passed with no turn \u2014 reporting idle again\n"
14022
+ );
14023
+ this.onChange?.();
14024
+ }
14025
+ };
14026
+
13787
14027
  // src/runner/live-children.ts
13788
14028
  function findLiveChild(sources) {
13789
14029
  for (const source of sources) {
@@ -13851,6 +14091,13 @@ var SessionRunner = class _SessionRunner {
13851
14091
  backgroundWork = new BackgroundWorkTracker({
13852
14092
  onChange: () => this.refreshLoopStatus()
13853
14093
  });
14094
+ /** An armed `ScheduleWakeup` (the dynamic `/loop` mode): the turn ended, but
14095
+ * the agent asked to be re-invoked at a deadline. Same liveness contract as
14096
+ * `backgroundWork` — while it reports pending, heartbeats go out non-idle so
14097
+ * the reconciler cannot sleep the pod before the wakeup fires. */
14098
+ scheduledWakeup = new ScheduledWakeupTracker({
14099
+ onChange: () => this.refreshLoopStatus()
14100
+ });
13854
14101
  /** Boot supervisor handle, set post-construction once it's known (it is
13855
14102
  * started alongside connect(), before the runner itself exists in some
13856
14103
  * call sites) — notified when the core loop goes live so it can release
@@ -13895,6 +14142,7 @@ var SessionRunner = class _SessionRunner {
13895
14142
  },
13896
14143
  onIdleTimeout: () => {
13897
14144
  if (this.deferShutdownForLiveChild("idle")) return;
14145
+ if (this.deferShutdownForScheduledWakeup("idle")) return;
13898
14146
  if (this.deferShutdownForBackgroundWork()) return;
13899
14147
  process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
13900
14148
  this.stopped = true;
@@ -13907,6 +14155,7 @@ var SessionRunner = class _SessionRunner {
13907
14155
  },
13908
14156
  onDormantTimeout: () => {
13909
14157
  if (this.deferShutdownForLiveChild("dormant")) return;
14158
+ if (this.deferShutdownForScheduledWakeup("dormant")) return;
13910
14159
  process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
13911
14160
  this.stopped = true;
13912
14161
  this.queryBridge?.stop();
@@ -13966,6 +14215,50 @@ var SessionRunner = class _SessionRunner {
13966
14215
  const label = timer === "idle" ? "Idle" : "Dormant idle";
13967
14216
  process.stderr.write(
13968
14217
  `[conveyor-agent] ${label} timeout deferred: ${live.kind} session ${live.sessionId} active
14218
+ `
14219
+ );
14220
+ if (timer === "idle") {
14221
+ this.lifecycle.startIdleTimer(recheckMs);
14222
+ } else {
14223
+ this.dormantDeadline = Date.now() + recheckMs;
14224
+ this.lifecycle.startDormantTimer(recheckMs);
14225
+ }
14226
+ return true;
14227
+ }
14228
+ /**
14229
+ * Suppress an idle or dormant shutdown while a `ScheduleWakeup` is armed.
14230
+ *
14231
+ * Both timers matter. The idle timer (30 min) kills a loop that paces itself
14232
+ * slower than that, and the dormant timer (60 min after a `completed` event)
14233
+ * kills a one-hour arm — the agent declares completion at the end of every
14234
+ * tick, so a dynamic loop sits dormant by construction.
14235
+ *
14236
+ * The bound is the WAKEUP'S OWN deadline, not `BACKGROUND_WORK_MAX_MS`: the
14237
+ * tool clamps `delaySeconds` to one hour, which already exceeds that 45-min
14238
+ * cap, so reusing it would shut the pod down before the longest legal arm
14239
+ * fired. Once the deadline passes the tracker expires it and this returns
14240
+ * false, so a lost timer cannot pin a pod.
14241
+ */
14242
+ deferShutdownForScheduledWakeup(timer) {
14243
+ let deadline;
14244
+ try {
14245
+ deadline = this.scheduledWakeup.pendingDeadline();
14246
+ } catch (err) {
14247
+ process.stderr.write(
14248
+ `[conveyor-agent] Scheduled-wakeup probe failed, not deferring: ${err}
14249
+ `
14250
+ );
14251
+ return false;
14252
+ }
14253
+ if (deadline === null) return false;
14254
+ const remainingMs = Math.max(0, deadline - Date.now());
14255
+ const recheckMs = Math.min(
14256
+ Math.max(remainingMs, 1),
14257
+ Math.max(this.lifecycle.config.idleTimeoutMs, 1)
14258
+ );
14259
+ const label = timer === "idle" ? "Idle" : "Dormant idle";
14260
+ process.stderr.write(
14261
+ `[conveyor-agent] ${label} timeout deferred: scheduled wakeup armed (${Math.round(remainingMs / 1e3)}s remaining)
13969
14262
  `
13970
14263
  );
13971
14264
  if (timer === "idle") {
@@ -14895,6 +15188,7 @@ var SessionRunner = class _SessionRunner {
14895
15188
  });
14896
15189
  this.portDiscovery.stop();
14897
15190
  this.backgroundWork.clear();
15191
+ this.scheduledWakeup.clear();
14898
15192
  this.loopLag.stop();
14899
15193
  this.lifecycle.destroy();
14900
15194
  this.connection.disconnect();
@@ -15135,6 +15429,7 @@ ${outcome.failures.join("\n")}
15135
15429
  if (this.questionPendingSince !== null && Date.now() - this.questionPendingSince < resolveQuestionGraceMs() + QUESTION_HOLD_SLACK_MS) {
15136
15430
  return "waiting";
15137
15431
  }
15432
+ if (this.scheduledWakeup.hasPending()) return "waiting";
15138
15433
  return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
15139
15434
  }
15140
15435
  // ── Question grace hold ────────────────────────────────────────────
@@ -15205,11 +15500,12 @@ ${outcome.failures.join("\n")}
15205
15500
  this.loopLag.setStatus(loopStatus);
15206
15501
  return loopStatus;
15207
15502
  }
15208
- /** One `tool_use` from the turn stream — registers a background launch when
15209
- * the tool starts work that outlives the turn. */
15503
+ /** One `tool_use` from the turn stream — registers work that outlives the
15504
+ * turn: a background launch, or an armed `ScheduleWakeup`. */
15210
15505
  noteToolUseForBackgroundWork(event) {
15211
15506
  if (event.type !== "tool_use" || typeof event.tool !== "string") return;
15212
15507
  this.backgroundWork.noteToolUse(event.tool, event.input);
15508
+ this.scheduledWakeup.noteToolUse(event.tool, event.input);
15213
15509
  }
15214
15510
  async setState(status) {
15215
15511
  if (status === "running") {
@@ -15250,6 +15546,7 @@ ${outcome.failures.join("\n")}
15250
15546
  /** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
15251
15547
  * prefilled PTY callback already emitted `running` before it reaches here. */
15252
15548
  beginRunningEpisode() {
15549
+ this.scheduledWakeup.clear();
15253
15550
  this.activeEpisodeStartedAt = Date.now();
15254
15551
  this.backgroundDeferStartedAt = null;
15255
15552
  this.backgroundWakeFiredThisIdle = false;
@@ -15261,6 +15558,7 @@ ${outcome.failures.join("\n")}
15261
15558
  this.clearQuestionHold();
15262
15559
  this.portDiscovery.stop();
15263
15560
  this.backgroundWork.clear();
15561
+ this.scheduledWakeup.clear();
15264
15562
  this.loopLag.stop();
15265
15563
  this.lifecycle.destroy();
15266
15564
  try {
@@ -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
  }
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-7WO6NTOS.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-RLROQPPT.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-BZZ4B37Q.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-T7MNWASE.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-RLROQPPT.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-BZZ4B37Q.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-7WO6NTOS.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-BZZ4B37Q.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.25",
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",
@@ -38,7 +38,7 @@
38
38
  "typecheck": "tsgo --noEmit"
39
39
  },
40
40
  "dependencies": {
41
- "@anthropic-ai/claude-agent-sdk": "^0.3.257",
41
+ "@anthropic-ai/claude-agent-sdk": "^0.3.280",
42
42
  "@modelcontextprotocol/sdk": "^1.30.0",
43
43
  "node-pty": "^1.0.0",
44
44
  "socket.io-client": "^4.8.3",
@@ -49,6 +49,6 @@
49
49
  "@project/shared": "workspace:*",
50
50
  "tsup": "^8.0.0",
51
51
  "typescript": "^5.3.0",
52
- "vitest": "^4.0.17"
52
+ "vitest": "^5.0.1"
53
53
  }
54
54
  }
@@ -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