@schoolai/shipyard 3.12.0 → 3.13.0-rc.20260615.0

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.
Files changed (34) hide show
  1. package/dist/capability-detector-worker.js +4 -4
  2. package/dist/{chunk-3NML6IJM.js → chunk-7MVFHLYV.js} +2 -2
  3. package/dist/{chunk-GMMRLPGI.js → chunk-BUAEJNUG.js} +78 -13
  4. package/dist/{chunk-GMMRLPGI.js.map → chunk-BUAEJNUG.js.map} +1 -1
  5. package/dist/{chunk-HUTIDJIT.js → chunk-IWBDVGD2.js} +634 -29
  6. package/dist/chunk-IWBDVGD2.js.map +1 -0
  7. package/dist/{chunk-OCCM6UXW.js → chunk-KUPHN3ZN.js} +2 -2
  8. package/dist/{chunk-LUKN75OT.js → chunk-M3WBYTB3.js} +4 -2
  9. package/dist/chunk-M3WBYTB3.js.map +1 -0
  10. package/dist/{chunk-KJAEED3H.js → chunk-MR6HRO7R.js} +3 -3
  11. package/dist/{chunk-Q6JQK2DR.js → chunk-OX3UY44R.js} +4 -4
  12. package/dist/{chunk-LXFMCDAZ.js → chunk-RCEAMZVG.js} +2 -2
  13. package/dist/cursor-runner.js +3 -3
  14. package/dist/electron-utility.js +2 -2
  15. package/dist/index.js +5 -5
  16. package/dist/{login-2DJVKACB.js → login-HRR3T4SZ.js} +3 -3
  17. package/dist/{plan-backfill-S7FKJVOP.js → plan-backfill-QNJUWOYP.js} +4 -4
  18. package/dist/{roi-PTTYX6XY.js → roi-HNJRISLW.js} +11 -12
  19. package/dist/roi-HNJRISLW.js.map +1 -0
  20. package/dist/{serve-2N7BIGRO.js → serve-3N7IHREI.js} +718 -268
  21. package/dist/{serve-2N7BIGRO.js.map → serve-3N7IHREI.js.map} +1 -1
  22. package/dist/{start-GKHRXDGA.js → start-UPNI6OOF.js} +6 -6
  23. package/package.json +1 -1
  24. package/dist/chunk-HUTIDJIT.js.map +0 -1
  25. package/dist/chunk-LUKN75OT.js.map +0 -1
  26. package/dist/roi-PTTYX6XY.js.map +0 -1
  27. /package/dist/{chunk-3NML6IJM.js.map → chunk-7MVFHLYV.js.map} +0 -0
  28. /package/dist/{chunk-OCCM6UXW.js.map → chunk-KUPHN3ZN.js.map} +0 -0
  29. /package/dist/{chunk-KJAEED3H.js.map → chunk-MR6HRO7R.js.map} +0 -0
  30. /package/dist/{chunk-Q6JQK2DR.js.map → chunk-OX3UY44R.js.map} +0 -0
  31. /package/dist/{chunk-LXFMCDAZ.js.map → chunk-RCEAMZVG.js.map} +0 -0
  32. /package/dist/{login-2DJVKACB.js.map → login-HRR3T4SZ.js.map} +0 -0
  33. /package/dist/{plan-backfill-S7FKJVOP.js.map → plan-backfill-QNJUWOYP.js.map} +0 -0
  34. /package/dist/{start-GKHRXDGA.js.map → start-UPNI6OOF.js.map} +0 -0
@@ -12,7 +12,7 @@ import {
12
12
  guardedSubscribe,
13
13
  makeInitialAttemptState,
14
14
  shutdownFileWatcherGuard
15
- } from "./chunk-3NML6IJM.js";
15
+ } from "./chunk-7MVFHLYV.js";
16
16
  import {
17
17
  TRAILER_SCHEMA_VERSION,
18
18
  appendTrailerToMessage,
@@ -39,13 +39,13 @@ import {
39
39
  startPlanBackfillForDaemon,
40
40
  sync,
41
41
  validatePeerId
42
- } from "./chunk-GMMRLPGI.js";
42
+ } from "./chunk-BUAEJNUG.js";
43
43
  import {
44
44
  loadAuthToken
45
45
  } from "./chunk-TFWDJCUJ.js";
46
46
  import {
47
47
  getDaemonVersion
48
- } from "./chunk-KJAEED3H.js";
48
+ } from "./chunk-MR6HRO7R.js";
49
49
  import {
50
50
  collectPriorCrashDetail,
51
51
  getRecentWasmPanicMessages
@@ -167,7 +167,7 @@ import {
167
167
  translateWarningNotification,
168
168
  unresolveReviewThread,
169
169
  writeCursorApiKeyToVault
170
- } from "./chunk-Q6JQK2DR.js";
170
+ } from "./chunk-OX3UY44R.js";
171
171
  import {
172
172
  buildNodeSpawnEnv,
173
173
  nodeSpawnEnvOverlay,
@@ -226,7 +226,7 @@ import {
226
226
  createAssetResolver,
227
227
  toSdkContent,
228
228
  toSdkPermissionMode
229
- } from "./chunk-LXFMCDAZ.js";
229
+ } from "./chunk-RCEAMZVG.js";
230
230
  import {
231
231
  AGENT_IDS,
232
232
  AGENT_SYSTEM_CLAUDE_CODE,
@@ -369,6 +369,7 @@ import {
369
369
  isDiffEmpty,
370
370
  isPendingAgentParticipantId,
371
371
  isResourceResolutionMeta,
372
+ maybeCompressFrame,
372
373
  migrateAnnotationStore,
373
374
  migrateCredentialsVaultStore,
374
375
  migrateDiffCommentStore,
@@ -407,7 +408,7 @@ import {
407
408
  unsafeAssertSourceId,
408
409
  vizFileExtension,
409
410
  withPreferredRuntimeAuthMethod
410
- } from "./chunk-HUTIDJIT.js";
411
+ } from "./chunk-IWBDVGD2.js";
411
412
  import {
412
413
  AuthGitHubCallbackRequestSchema,
413
414
  AuthGitHubCallbackResponseSchema,
@@ -420,6 +421,7 @@ import {
420
421
  BrowserCodeExchangeRequestSchema,
421
422
  BrowserCodeExchangeResponseSchema,
422
423
  BrowserCodeResponseSchema,
424
+ CONTROL_COMPRESSION_MIN_VERSION,
423
425
  CollabCreateRequestSchema,
424
426
  CollabCreateResponseSchema,
425
427
  CollabRoomConnection,
@@ -452,7 +454,7 @@ import {
452
454
  VaultKeyPutRequestSchema,
453
455
  VaultKeyPutResponseSchema,
454
456
  classifyClaudeCodeCompatibility
455
- } from "./chunk-LUKN75OT.js";
457
+ } from "./chunk-M3WBYTB3.js";
456
458
  import {
457
459
  external_exports as external_exports2
458
460
  } from "./chunk-EHQITHQX.js";
@@ -5762,7 +5764,7 @@ function nanoid(size = 21) {
5762
5764
  }
5763
5765
 
5764
5766
  // src/services/bootstrap/signaling.ts
5765
- var DAEMON_NPM_VERSION = true ? "3.12.0" : "unknown";
5767
+ var DAEMON_NPM_VERSION = true ? "3.13.0" : "unknown";
5766
5768
  function createDaemonSignaling(config) {
5767
5769
  const agentId = config.agentId ?? nanoid();
5768
5770
  function send(msg) {
@@ -13295,6 +13297,8 @@ var ROLE_PERMISSIONS = {
13295
13297
  "rewind_preview_request",
13296
13298
  "fetch_older",
13297
13299
  "permission_response",
13300
+ "permission_response_codex",
13301
+ "permission_response_cursor",
13298
13302
  "update_settings",
13299
13303
  "request_mcp_toggle",
13300
13304
  "request_skill_toggle",
@@ -13332,6 +13336,7 @@ var ROLE_PERMISSIONS = {
13332
13336
  "delete_annotation",
13333
13337
  "mark_annotation_delivered",
13334
13338
  "request_annotation_snapshot",
13339
+ "request_viz_content_batch",
13335
13340
  "request_deliverables_snapshot",
13336
13341
  "request_task_state",
13337
13342
  "create_thread",
@@ -13403,6 +13408,7 @@ var ROLE_PERMISSIONS = {
13403
13408
  "delete_annotation",
13404
13409
  "mark_annotation_delivered",
13405
13410
  "request_annotation_snapshot",
13411
+ "request_viz_content_batch",
13406
13412
  "request_deliverables_snapshot",
13407
13413
  "request_task_state",
13408
13414
  "create_thread",
@@ -13426,6 +13432,7 @@ var ROLE_PERMISSIONS = {
13426
13432
  "join_collab_room",
13427
13433
  "leave_collab_room",
13428
13434
  "request_annotation_snapshot",
13435
+ "request_viz_content_batch",
13429
13436
  "request_deliverables_snapshot",
13430
13437
  "request_task_state",
13431
13438
  "list_threads",
@@ -13437,6 +13444,7 @@ var ROLE_PERMISSIONS = {
13437
13444
  "request_capabilities",
13438
13445
  "request_pr_data",
13439
13446
  "request_task_state",
13447
+ "request_viz_content_batch",
13440
13448
  "join_collab_room",
13441
13449
  "leave_collab_room",
13442
13450
  "presence_update"
@@ -13474,6 +13482,7 @@ var TASK_SCOPED_CONTROL_TYPES = /* @__PURE__ */ new Set([
13474
13482
  "delete_annotation",
13475
13483
  "mark_annotation_delivered",
13476
13484
  "request_annotation_snapshot",
13485
+ "request_viz_content_batch",
13477
13486
  "request_deliverables_snapshot",
13478
13487
  "request_task_state",
13479
13488
  "create_thread",
@@ -14786,6 +14795,7 @@ function routeMessage(msg, callbacks, sendControl, log) {
14786
14795
  callbacks.onUpdateThreadSettings(msg.taskId, msg.threadId, msg.settings);
14787
14796
  break;
14788
14797
  case "request_capabilities":
14798
+ callbacks.onPeerProtocolVersion(msg.protocolVersion ?? 0);
14789
14799
  callbacks.onRequestCapabilities({ force: msg.force });
14790
14800
  break;
14791
14801
  case "request_skills":
@@ -14994,6 +15004,9 @@ function routeMessage(msg, callbacks, sendControl, log) {
14994
15004
  case "request_annotation_snapshot":
14995
15005
  callbacks.onRequestAnnotationSnapshot(msg.taskId);
14996
15006
  break;
15007
+ case "request_viz_content_batch":
15008
+ callbacks.onRequestVizContentBatch(msg.taskId);
15009
+ break;
14997
15010
  case "request_deliverables_snapshot":
14998
15011
  callbacks.onRequestDeliverablesSnapshot(msg.taskId);
14999
15012
  break;
@@ -16295,6 +16308,8 @@ function dedupKeyForMessage(msg) {
16295
16308
  return JSON.stringify({ type: "plugin_data_response", requestId: msg.requestId });
16296
16309
  case "presence_state":
16297
16310
  return JSON.stringify({ type: "presence_state" });
16311
+ case "capabilities":
16312
+ return JSON.stringify({ type: "capabilities" });
16298
16313
  default:
16299
16314
  return void 0;
16300
16315
  }
@@ -19332,8 +19347,9 @@ function createSkillsTools(ctx) {
19332
19347
  "To use one, call load_skill with its name. Skills you already reach natively are omitted here",
19333
19348
  "(invoke those the normal way). Returns each skill's name, one-line description, and origin.",
19334
19349
  "",
19335
- "WHEN: before starting work, to see what curated capabilities exist; or when a native skill",
19336
- "mention came back empty (the skill is likely library-only \u2014 look for it here)."
19350
+ "WHEN: before starting domain-specific work, to see whether the team already wrote down the",
19351
+ "conventions for it (check skills first, then dive in); or when a native skill mention came",
19352
+ "back empty (the skill is likely library-only \u2014 look for it here)."
19337
19353
  ].join("\n"),
19338
19354
  inputSchema: {},
19339
19355
  handler: async () => {
@@ -19462,12 +19478,14 @@ var COMMENTS_PROMPT_DOC = [
19462
19478
  var VISUALIZE_PROMPT_DOC = [
19463
19479
  "Call `read_me` before your first visualization in a session. It returns the theme, color palette, and constraints. Skipping it leads to broken colors (e.g. invisible text on dark backgrounds). Pass the `modules` parameter for your use case: chart, interactive, diagram, art, slides, review, table, recap, or plan.",
19464
19480
  "",
19465
- "**When to visualize proactively** \u2014 don't wait to be asked:",
19481
+ "**When a picture beats a paragraph** \u2014 don't wait to be asked, but produce one when it changes what a reviewer understands, not by reflex:",
19466
19482
  "- Architecture or system design \u2192 Mermaid diagram",
19467
19483
  "- UI work or layout discussion \u2192 HTML mockup",
19468
19484
  "- Data modeling or performance \u2192 chart via Chart.js or D3 in HTML",
19469
19485
  "- State machines or workflows \u2192 Mermaid stateDiagram",
19470
19486
  "",
19487
+ "When prose already carries the idea clearly, skip the diagram \u2014 one sharp explanation beats a chart nobody needed.",
19488
+ "",
19471
19489
  "**Two tools:** `visualize` creates a file-backed visualization that renders inline immediately (returns slug + file path). `present({source:'slug', slug, target})` shows an existing visualization: `target:'inline'` returns a snapshot in conversation, `target:'canvas'` places a live element that auto-updates when the file changes."
19472
19490
  ].join("\n");
19473
19491
  var PRESENT_PROMPT_DOC = [
@@ -19516,16 +19534,16 @@ var TASK_TOOL_PROMPT_DOC = [
19516
19534
  'The `task` MCP tool covers three actions: `send_message` and `cancel` for sibling tasks (by ID \u2014 drive a spawned task with new instructions or stop it), and `snapshot` for your CURRENT task. Call `task({action:"snapshot"})` to read a live markdown view of your own todos, branch, cost, PR state, and recent deliverables \u2014 the same shape `create_task` returns for spawned siblings.'
19517
19535
  ].join("\n");
19518
19536
  var REGISTER_DELIVERABLE_PROMPT_DOC = [
19519
- "Use `register_deliverable` to record concrete artifacts the agent produces during a task \u2014 screenshots, videos, log files, markdown summaries, HTML dashboards, structured JSON results, AX-tree dumps, DOM snapshots, or any other proof of completion. Reviewers see these in the conversation dock's Deliverables tab and can open each one in a full-screen viewer.",
19537
+ "Use `register_deliverable` to attach proof a reviewer can't get from the diff alone \u2014 a screenshot of the UI after your change, a green test-run log, a benchmark number, an AX-tree dump. Reviewers open these in the conversation dock's Deliverables tab. This is the \"don't ship blind\" half made concrete: the diff shows what changed; the deliverable shows it works.",
19520
19538
  "",
19521
- "**When to call:** any time you produce a verifiable artifact a reviewer would want to see \u2014 a Playwright screenshot proving a UI change works, a test-run log proving the suite passed, a markdown summary of what was decided. The point of Shipyard is velocity *with understanding*; deliverables are the understanding layer.",
19539
+ "**When to call:** right after a change whose effect a reviewer can't verify by reading the diff \u2014 a UI tweak (attach the rendered screenshot), a fix you proved with a test (attach the passing run), a perf claim (attach the numbers). **When NOT to call:** when the diff *is* the proof. A pure refactor, a rename, a config edit \u2014 the change verifies itself; registering a deliverable just costs the reviewer attention.",
19522
19540
  "",
19523
19541
  "**Scoping:** when the task has structured todos (visible in `<task-list>`), scope the deliverable to the matching `todoItemId` (bare number, no `#`). For task-spanning artifacts, leave `todoItemId` null. For thread-scoped evidence, set `threadId`.",
19524
19542
  "",
19525
19543
  "**Required fields:** `kind` (one of `screenshot`, `video`, `log`, `markdown`, `html`, `json`, `ax-tree`, `dom-snapshot`, `other`), `title`, and exactly one of `assetId` or `filePath`."
19526
19544
  ].join("\n");
19527
19545
  var SKILLS_PROMPT_DOC = [
19528
- "Shipyard curates a set of skills for this task \u2014 reusable instructions for common workflows. Call `list_skills` to see what's available, then `load_skill({ name })` to retrieve a skill's full instructions (and a directory path for any sibling files it references). Some skills are also reachable through your own `/name` mentions; for anything in `list_skills`, `load_skill` always works."
19546
+ "Shipyard curates a set of skills for this task \u2014 reusable, expert instructions for specific workflows. **Before starting domain-specific work, check skills first:** call `list_skills` to see what's available, then `load_skill({ name })` to pull a skill's full instructions (and a directory path for any sibling files it references) before you dive in. A relevant skill saves you re-deriving conventions the team already wrote down. Some skills are also reachable through your own `/name` mentions; for anything in `list_skills`, `load_skill` always works."
19529
19547
  ].join("\n");
19530
19548
  var HARNESS_TOOL_REGISTRY = [
19531
19549
  /** Comments — gate: all. */
@@ -19700,17 +19718,51 @@ var CURSOR_TOOLS = buildToolsBlock(
19700
19718
  function sharedIntro(shellWord) {
19701
19719
  return `# Shipyard
19702
19720
 
19703
- Shipyard is a collaborative workspace. Your teammates see everything you produce \u2014 code diffs, comments, visualizations, tasks \u2014 in real time. They review your changes, leave feedback, and contribute to the work plan alongside you. You are a pair programmer, not a tool.
19721
+ You're a pair programmer on this team. Your job has two halves, and they matter equally:
19722
+
19723
+ - **Ship fast** \u2014 do the work: write the code, run it, fix it, keep moving.
19724
+ - **Don't ship blind** \u2014 make your work legible *as you go*, so the people reviewing it see not just *what* changed but *why*, and can accept it with confidence.
19725
+
19726
+ Everything Shipyard gives you \u2014 the diff, comments, visualizations, deliverables, the plan, the canvas \u2014 exists to serve the second half. You're not narrating for its own sake; you're keeping the people you work with in the loop at the speed you're working. **A change nobody can review isn't done.**
19727
+
19728
+ Your teammates see everything you produce in real time, and they can act on it \u2014 edit the plan alongside you, comment on a diff hunk, add a task to the list. Treat what they do as direct input.
19729
+
19730
+ Before anything else, the few rules that don't bend:
19731
+
19732
+ - **Never launch browser tabs.** Don't use \`open\`, \`xdg-open\`, or ${shellWord} commands to open a URL \u2014 Shipyard renders everything the user needs to see.
19733
+ - **Confirm before anything outward-facing, irreversible, or tied to an identity** \u2014 publishing, posting, anything that consumes a token or attributes work to an account. Ask first.
19734
+ - **Rejection does not revert your code.** When a reviewer rejects, your changes stay on disk. Wait for their feedback before reattempting \u2014 don't undo or redo on your own.
19735
+
19736
+ ## Make your work legible
19737
+
19738
+ After you do something, close the gap between what you did and what a reviewer can see \u2014 and reach for the tool only when there's a real gap:
19739
+
19740
+ - **The reasoning behind a non-obvious change** belongs where they'll read it: a diff comment (\`add_comment\`) on the hunk. If the change speaks for itself, say nothing.
19741
+ - **A design, a data model, a flow, a comparison** is faster to grasp as a picture than a paragraph \u2014 a visualization (\`visualize\`, then \`present\`).
19742
+ - **Proof that something works that the diff can't show** \u2014 the UI after your change, a green test run, a performance number \u2014 is a deliverable (\`register_deliverable\`). When the diff *is* the proof, skip it.
19743
+ - **Multi-step work** becomes a task list the team watches and adds to \u2014 keep it current with your task-list tool (see Live State Resources for the exact verb your runtime uses).
19744
+
19745
+ You don't need to be asked, and you don't need permission \u2014 this is how the job gets done. But each artifact costs a reviewer a moment of attention, so produce the one that changes what they understand. One sharp comment beats three diagrams. When in doubt, don't.
19746
+
19747
+ You cannot preview your own visualizations \u2014 they render client-side only. Follow the constraints from \`read_me\` exactly.
19704
19748
 
19705
- **How collaborators see your work** \u2014 your output renders as a reviewable artifact: response text, code diffs, inline comments (from \`add_comment\`), and visualizations (from \`visualize\` + \`present\`). Explain *why* you made each change \u2014 reviewers see the diff.
19749
+ ## Choosing the right tool
19706
19750
 
19707
- **How you receive collaborator input** \u2014 see "Live State Resources" below. Every aspect of the task (todos, branch state, canvas, plan, comments) is delivered as a structured resource block, not as ad-hoc events.
19751
+ A few boundaries are easy to get wrong \u2014 prefer the more specific option:
19708
19752
 
19709
- Never use \`open\`, \`xdg-open\`, or ${shellWord} commands to launch browser tabs \u2014 Shipyard renders everything the user needs to see.
19753
+ - **Isolated or parallel work on its own branch** \u2192 a sibling task (\`create_task\`, which gets its own git worktree). A subagent (your runtime's subagent tool \u2014 see the agent-specific notes) shares *this* worktree and returns findings here \u2014 it's for read/research fan-out, not isolated build work.
19754
+ - **Sharing a preview** \u2192 \`publish\` to send it outside the session; \`present\` to show it inside.
19755
+ - **A recurring dashboard the user will return to** \u2192 a plugin. A one-off chart \u2192 a visualization.
19710
19756
 
19711
- Rejection does not revert code changes. Wait for the reviewer's feedback before reattempting.
19757
+ These are preferences, not commands \u2014 you decide.
19712
19758
 
19713
- You cannot preview your own visualizations \u2014 they render client-side only. Follow the constraints from \`read_me\` exactly.`;
19759
+ ## The plan is a shared editor, not a document you publish
19760
+
19761
+ When you propose a plan, it opens on the canvas as a **live collaborative editor** \u2014 rich text over a CRDT, with multiplayer cursors. Your teammates can **edit the plan text directly**, alongside your comments and theirs. So:
19762
+
19763
+ - Write the plan to be *edited*, not just approved \u2014 clear sections a human can revise in place.
19764
+ - Between turns, re-read the \`<plan>\` resource: human edits to the plan text are intent, the same as a task they added or a comment they left. Build on them; don't overwrite them.
19765
+ - Comments and direct edits are two channels into the same document \u2014 answer the comments, absorb the edits.`;
19714
19766
  }
19715
19767
  var CLAUDE_LIVE_STATE = `## Live State Resources
19716
19768
 
@@ -19777,9 +19829,11 @@ When liveness flips back to \`bound\`, the resource re-emits on your next turn.
19777
19829
 
19778
19830
  When you call ExitPlanMode, your plan is published to the Shipyard canvas for collaborative review. You will receive a deny response \u2014 this is expected. Stay in plan mode and wait for reviewer feedback. The \`<plan>\` resource carries the current plan markdown, embedded \`<${XML_TAGS.COMMENT}>\` children for plan-anchored comments, and a \`<review>\` element when reviewers have approved or requested changes.
19779
19831
 
19832
+ **The plan is a live editor, not a static post.** It renders as a collaborative rich-text editor over a CRDT \u2014 your teammates can edit the plan text directly, not just comment on it. So the markdown in a later \`<plan>\` resource may differ from what you wrote: treat their edits as intent, the same as a task they added or a comment they left. Build on what they changed; don't silently revert it. Comments and direct edits are two channels into the same document.
19833
+
19780
19834
  **While in plan mode** \u2014 place supporting visualizations on the canvas using \`present({source:'slug', slug, target:'canvas'})\`. Architecture diagrams, data models, and flow charts alongside the plan text help reviewers evaluate your design.
19781
19835
 
19782
- Do not revise the plan unless reviewers request changes. When the \`<review decision="approve">\` element appears, call ExitPlanMode again to begin implementation.
19836
+ Do not revise the plan unless reviewers request changes \u2014 but absorbing a human's direct edit to the plan text is not "revising," it's accepting their contribution. When the \`<review decision="approve">\` element appears, call ExitPlanMode again to begin implementation.
19783
19837
 
19784
19838
  ### \`<${XML_TAGS.DEV_SERVERS}>\`
19785
19839
 
@@ -19807,11 +19861,19 @@ Prefer background execution to keep your main thread responsive to collaborators
19807
19861
  **Bash tool** \u2014 Use \`run_in_background: true\` for commands that may take more than 10 seconds: builds, installs, test suites, large git operations, network requests. Use \`TaskOutput\` to read results when notified. Keep short commands (git status, ls, cat) in the foreground.
19808
19862
 
19809
19863
  **Why this matters:** Foreground operations block everything. The only recovery from a stuck foreground operation is stopping the entire agent. Background-first execution avoids this and keeps you available for collaborator feedback.`;
19864
+ var SHARED_CLOSING = `## The rules that don't bend
19865
+
19866
+ A reminder, because these are the ones that cost the most when missed:
19867
+
19868
+ - **Never open browser tabs** \u2014 Shipyard renders everything; no \`open\` / \`xdg-open\`.
19869
+ - **Confirm before anything outward-facing, irreversible, or identity-tied** \u2014 publishing, posting, spending a token. Ask first.
19870
+ - **Rejection does not revert your code** \u2014 your changes stay on disk; wait for the reviewer's feedback before reattempting.`;
19810
19871
  var SHIPYARD_SYSTEM_PROMPT = [
19811
19872
  sharedIntro("shell"),
19812
19873
  CLAUDE_TOOLS,
19813
19874
  CLAUDE_LIVE_STATE,
19814
- CLAUDE_BACKGROUND
19875
+ CLAUDE_BACKGROUND,
19876
+ SHARED_CLOSING
19815
19877
  ].join("\n\n").trim();
19816
19878
  function buildCollabSystemPrompt(participants) {
19817
19879
  const roster = participants.map((p2) => `${p2.name} (${p2.role})`).join(", ");
@@ -19865,6 +19927,8 @@ When you are in plan-collaboration mode, publish your plan by wrapping it in a \
19865
19927
 
19866
19928
  The \`<plan>\` resource carries the current plan markdown, embedded \`<${XML_TAGS.COMMENT}>\` children for plan-anchored comments, and a \`<review>\` element when reviewers have approved or requested changes. Reviewer feedback arrives as \`<${XML_TAGS.COMMENT}>\` children of \`<plan>\` \u2014 reply to every one with \`add_comment\` \`mode: "reply"\` and \`parentCommentId\` set to the comment's \`id\`, same as on every other surface.
19867
19929
 
19930
+ **The plan is a live editor, not a static post.** Once published, it renders as a collaborative rich-text editor over a CRDT \u2014 your teammates can edit the plan text directly, not just comment. The markdown in a later \`<plan>\` resource may therefore differ from what you proposed: treat their edits as intent, the same as a comment they left. When you emit your next \`<proposed_plan>\` block, build on what they changed \u2014 carry their edits forward rather than reverting to your prior wording.
19931
+
19868
19932
  **Iteration semantics** \u2014 to revise the plan, emit a fresh \`<proposed_plan>\` block in your next turn. Each new block is a complete replacement, not a patch \u2014 include everything the plan should contain. Emit at most one \`<proposed_plan>\` block per turn, and only when the plan is decision-complete.
19869
19933
 
19870
19934
  **Refining with the reviewer** \u2014 use \`request_user_input\` to ask multiple-choice questions during refinement (preferences, tradeoffs, and locked-in assumptions that materially change the plan). Don't ask questions you can answer from the repo \u2014 explore first via \`shell\` reads, then ask only when ambiguity is genuinely about intent.
@@ -19901,7 +19965,8 @@ var SHIPYARD_CODEX_SYSTEM_PROMPT = [
19901
19965
  sharedIntro("shell"),
19902
19966
  CODEX_TOOLS,
19903
19967
  CODEX_LIVE_STATE,
19904
- CODEX_NOTES
19968
+ CODEX_NOTES,
19969
+ SHARED_CLOSING
19905
19970
  ].join("\n\n").trim();
19906
19971
  var CURSOR_LIVE_STATE = `## Live State Resources
19907
19972
 
@@ -19937,6 +20002,8 @@ Authoritative snapshot of what's on the shared canvas. Each \`<element>\` carrie
19937
20002
 
19938
20003
  The \`<plan>\` resource carries the current plan markdown, embedded \`<${XML_TAGS.COMMENT}>\` children for plan-anchored comments, and a \`<review>\` element when reviewers have approved or requested changes. Reviewer feedback arrives as \`<${XML_TAGS.COMMENT}>\` children of \`<plan>\` \u2014 reply to every one with \`add_comment\` \`mode: "reply"\` and \`parentCommentId\` set to the comment's \`id\`, same as on every other surface.
19939
20004
 
20005
+ The plan text is a live collaborative editor \u2014 your teammates can edit it directly, not just comment. If the plan markdown in a later \`<plan>\` resource differs from what you remember, that is a human edit: treat it as intent, the same as a comment they left, and build on it rather than reverting it.
20006
+
19940
20007
  ### \`<${XML_TAGS.DEV_SERVERS}>\` and \`<${XML_TAGS.TERMINALS}>\`
19941
20008
 
19942
20009
  Task-scoped snapshots of running processes. Use \`<${XML_TAGS.DEV_SERVERS}>\` to check whether a dev server is running before suggesting starting one. Use \`<${XML_TAGS.TERMINALS}>\` to find log files for active PTY sessions \u2014 when you need current terminal state, read the file at \`log="..."\` directly via \`Shell\` (e.g. \`tail -n 100\`), do not infer from possibly-stale \`<recent-output>\`.
@@ -19960,7 +20027,8 @@ var SHIPYARD_CURSOR_SYSTEM_PROMPT = [
19960
20027
  sharedIntro("`Shell`"),
19961
20028
  CURSOR_TOOLS,
19962
20029
  CURSOR_LIVE_STATE,
19963
- CURSOR_NOTES
20030
+ CURSOR_NOTES,
20031
+ SHARED_CLOSING
19964
20032
  ].join("\n\n").trim();
19965
20033
 
19966
20034
  // src/services/mcp/visibility-migrations.ts
@@ -22316,7 +22384,13 @@ function dispatchSpawn(decision, shell) {
22316
22384
  return shell.spawnClaude();
22317
22385
  case "spawn-cursor": {
22318
22386
  const sub = shell.spawnCursor();
22319
- if (shell.initialContent.length > 0) sub.pushMessage(shell.initialContent);
22387
+ const firstTurnBlocks = shell.initialContent.length;
22388
+ if (firstTurnBlocks > 0) sub.pushMessage(shell.initialContent);
22389
+ shell.log({
22390
+ event: "cursor_spawn_first_turn",
22391
+ firstTurnPushed: firstTurnBlocks > 0,
22392
+ firstTurnBlocks
22393
+ });
22320
22394
  return sub;
22321
22395
  }
22322
22396
  case "reject-not-installed":
@@ -23080,11 +23154,12 @@ var RATCHET_THRESHOLDS = [10, 100, 1e3];
23080
23154
  var RATCHET_WINDOW_MS = 6e4;
23081
23155
  function createBestEffortSender(deps) {
23082
23156
  const dropsCounter = new RollingCounter(RATCHET_WINDOW_MS, RATCHET_THRESHOLDS);
23083
- const { guardedChannel, logAdapter, controlPeerId } = deps;
23157
+ const { guardedChannel, logAdapter, controlPeerId, compress } = deps;
23084
23158
  return {
23085
23159
  send: (msg) => {
23086
23160
  const json = JSON.stringify(msg);
23087
- const result = guardedChannel.send(json, "best-effort");
23161
+ const out = compress(json);
23162
+ const result = guardedChannel.send(out, "best-effort");
23088
23163
  if (result !== "dropped" && result !== "drain-pending") return;
23089
23164
  const now = Date.now();
23090
23165
  const { thresholdCrossed } = dropsCounter.record(now);
@@ -26406,6 +26481,61 @@ function describeError(err3) {
26406
26481
  // src/services/channels/control-channel/task-callbacks.ts
26407
26482
  var TASK_INDEX_SNAPSHOT_MAX_BYTES = 9e5;
26408
26483
  var TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES = 1048576;
26484
+ function buildSlimTaskIndex(tasks) {
26485
+ const slimTasks = {};
26486
+ for (const [taskId, record] of Object.entries(tasks)) {
26487
+ slimTasks[taskId] = toTaskIndexEntry(record);
26488
+ }
26489
+ return slimTasks;
26490
+ }
26491
+ function measureJsonBytes(value) {
26492
+ try {
26493
+ return Buffer.byteLength(JSON.stringify(value), "utf-8");
26494
+ } catch {
26495
+ return 0;
26496
+ }
26497
+ }
26498
+ function reportOversizedTaskIndexSnapshot(slimTasks, bytes, logAdapter) {
26499
+ if (bytes <= TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES) return;
26500
+ const taskCount = Object.keys(slimTasks).length;
26501
+ logAdapter({
26502
+ event: "task_index_snapshot_oversized",
26503
+ level: "error",
26504
+ bytes,
26505
+ taskCount,
26506
+ limitBytes: TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES
26507
+ });
26508
+ telemeter("task_index_snapshot_oversized", {
26509
+ bytes,
26510
+ taskCount,
26511
+ limitBytes: TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES
26512
+ });
26513
+ }
26514
+ function emitTaskIndexSnapshot(args) {
26515
+ const { tasks, version, partial, nextCursor, nextCursorId, totalCount } = args.result;
26516
+ const slimTasks = buildSlimTaskIndex(tasks);
26517
+ const message = {
26518
+ type: "task_index_snapshot",
26519
+ tasks: slimTasks,
26520
+ version,
26521
+ partial,
26522
+ nextCursor,
26523
+ nextCursorId,
26524
+ totalCount
26525
+ };
26526
+ const bytes = measureJsonBytes(message);
26527
+ args.controlHandler.sendControl(message);
26528
+ reportOversizedTaskIndexSnapshot(slimTasks, bytes, args.logAdapter);
26529
+ args.logAdapter({
26530
+ event: "perf_task_index_snapshot_emit_done",
26531
+ taskCount: Object.keys(slimTasks).length,
26532
+ totalCount,
26533
+ partial: partial === true,
26534
+ cursorRequested: args.cursorRequested,
26535
+ bytes,
26536
+ durationMs: Date.now() - args.startedAt
26537
+ });
26538
+ }
26409
26539
  function buildTaskCallbacks(deps) {
26410
26540
  const { daemon, getControlHandler, logAdapter, publishedArtifactStore, previewStateStore } = deps;
26411
26541
  const taskIndexInflightTtlMs = 250;
@@ -26490,7 +26620,8 @@ function buildTaskCallbacks(deps) {
26490
26620
  limit,
26491
26621
  sinceLastActivityAt: opts.sinceLastActivityAt ?? null,
26492
26622
  sinceLastId: opts.sinceLastId ?? null,
26493
- maxBytes: TASK_INDEX_SNAPSHOT_MAX_BYTES
26623
+ maxBytes: TASK_INDEX_SNAPSHOT_MAX_BYTES,
26624
+ includeTaskIds: cursorRequested ? [] : "favoriteTasks"
26494
26625
  });
26495
26626
  const cached = taskIndexInflightCache.get(fingerprint);
26496
26627
  const now = Date.now();
@@ -26502,80 +26633,54 @@ function buildTaskCallbacks(deps) {
26502
26633
  });
26503
26634
  return;
26504
26635
  }
26505
- logAdapter({
26506
- event: "perf_task_index_snapshot_emit_start",
26507
- cursorRequested,
26508
- activeOnly: opts.activeOnly === true
26509
- });
26510
- const resultPromise = daemon.taskStateStore.listTasksPaginated({
26511
- activeOnly: opts.activeOnly,
26512
- limit,
26513
- sinceLastActivityAt: opts.sinceLastActivityAt,
26514
- sinceLastId: opts.sinceLastId,
26515
- maxBytes: TASK_INDEX_SNAPSHOT_MAX_BYTES
26516
- });
26517
- taskIndexInflightCache.set(fingerprint, {
26518
- promise: resultPromise,
26519
- expiresAt: now + taskIndexInflightTtlMs * 4
26636
+ const includeTaskIdsPromise = cursorRequested ? Promise.resolve(void 0) : daemon.userSettingsStore.getSettings().then((settings) => settings.favoriteTasks).catch((err3) => {
26637
+ logAdapter({
26638
+ event: "task_index_favorites_load_failed",
26639
+ error: err3 instanceof Error ? err3.message : String(err3)
26640
+ });
26641
+ return void 0;
26520
26642
  });
26521
- resultPromise.then(({ tasks, version, partial, nextCursor, nextCursorId, totalCount }) => {
26522
- const slimTasks = {};
26523
- for (const [taskId, record] of Object.entries(tasks)) {
26524
- slimTasks[taskId] = toTaskIndexEntry(record);
26525
- }
26526
- const message = {
26527
- type: "task_index_snapshot",
26528
- tasks: slimTasks,
26529
- version,
26530
- partial,
26531
- nextCursor,
26532
- nextCursorId,
26533
- totalCount
26534
- };
26535
- let bytes = 0;
26536
- try {
26537
- bytes = Buffer.byteLength(JSON.stringify(message), "utf-8");
26538
- } catch {
26539
- }
26540
- getControlHandler().sendControl(message);
26541
- if (bytes > TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES) {
26542
- const taskCount = Object.keys(slimTasks).length;
26543
- logAdapter({
26544
- event: "task_index_snapshot_oversized",
26545
- level: "error",
26546
- bytes,
26547
- taskCount,
26548
- limitBytes: TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES
26549
- });
26550
- telemeter("task_index_snapshot_oversized", {
26551
- bytes,
26552
- taskCount,
26553
- limitBytes: TASK_INDEX_SNAPSHOT_OVERSIZE_BYTES
26554
- });
26555
- }
26643
+ const requestPromise = includeTaskIdsPromise.then((includeTaskIds) => {
26556
26644
  logAdapter({
26557
- event: "perf_task_index_snapshot_emit_done",
26558
- taskCount: Object.keys(slimTasks).length,
26559
- totalCount,
26560
- partial: partial === true,
26645
+ event: "perf_task_index_snapshot_emit_start",
26561
26646
  cursorRequested,
26562
- bytes,
26563
- durationMs: Date.now() - startedAt
26647
+ activeOnly: opts.activeOnly === true
26648
+ });
26649
+ return daemon.taskStateStore.listTasksPaginated({
26650
+ activeOnly: opts.activeOnly,
26651
+ limit,
26652
+ sinceLastActivityAt: opts.sinceLastActivityAt,
26653
+ sinceLastId: opts.sinceLastId,
26654
+ includeTaskIds,
26655
+ maxBytes: TASK_INDEX_SNAPSHOT_MAX_BYTES
26656
+ });
26657
+ }).then((result) => {
26658
+ emitTaskIndexSnapshot({
26659
+ result,
26660
+ controlHandler: getControlHandler(),
26661
+ logAdapter,
26662
+ cursorRequested,
26663
+ startedAt
26564
26664
  });
26565
26665
  const settled = taskIndexInflightCache.get(fingerprint);
26566
- if (settled?.promise === resultPromise) {
26666
+ if (settled?.promise === requestPromise) {
26567
26667
  settled.expiresAt = Date.now() + taskIndexInflightTtlMs;
26568
26668
  }
26569
26669
  }).catch((err3) => {
26570
26670
  logAdapter({
26571
- event: "task_state_store_list_failed",
26671
+ event: "task_index_request_failed",
26572
26672
  error: err3 instanceof Error ? err3.message : String(err3)
26573
26673
  });
26574
26674
  const failed = taskIndexInflightCache.get(fingerprint);
26575
- if (failed?.promise === resultPromise) {
26675
+ if (failed?.promise === requestPromise) {
26576
26676
  taskIndexInflightCache.delete(fingerprint);
26577
26677
  }
26578
26678
  });
26679
+ taskIndexInflightCache.set(fingerprint, {
26680
+ promise: requestPromise,
26681
+ expiresAt: now + taskIndexInflightTtlMs * 4
26682
+ });
26683
+ void requestPromise;
26579
26684
  },
26580
26685
  onRequestReviewTasks: () => {
26581
26686
  daemon.taskStateStore.listTasks().then((all) => {
@@ -27042,6 +27147,10 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
27042
27147
  chunk: true,
27043
27148
  logAdapter
27044
27149
  });
27150
+ let peerProtocolVersion = 0;
27151
+ function maybeCompress(json) {
27152
+ return maybeCompressFrame(json, peerProtocolVersion >= CONTROL_COMPRESSION_MIN_VERSION);
27153
+ }
27045
27154
  function guardedSend(data) {
27046
27155
  const result = guardedChannel.send(data);
27047
27156
  if (result === "dropped" || result === "drain-pending") {
@@ -27240,6 +27349,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
27240
27349
  daemon.taskManager.applyThreadSettings(taskId, threadId, applied);
27241
27350
  logAdapter({ event: "thread_settings_updated", taskId, threadId, settings: applied });
27242
27351
  },
27352
+ onPeerProtocolVersion: (version) => {
27353
+ peerProtocolVersion = version;
27354
+ },
27243
27355
  onRequestCapabilities: ({ force } = {}) => {
27244
27356
  if (force) {
27245
27357
  void deps.capabilityRefresh.refreshFull("force_request");
@@ -27795,6 +27907,16 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
27795
27907
  });
27796
27908
  });
27797
27909
  },
27910
+ onRequestVizContentBatch: (taskId) => {
27911
+ daemon.attachVizWatcher(taskId);
27912
+ daemon.pushTaskVizContent(taskId, (msg) => controlHandler.sendControl(msg)).catch((err3) => {
27913
+ logAdapter({
27914
+ event: "request_viz_content_batch_push_failed",
27915
+ taskId,
27916
+ error: err3 instanceof Error ? err3.message : String(err3)
27917
+ });
27918
+ });
27919
+ },
27798
27920
  onCreateThread: (opts) => handleCreateThread({
27799
27921
  opts,
27800
27922
  daemon,
@@ -28176,7 +28298,8 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
28176
28298
  receiveCursor: aloReceiveCursor,
28177
28299
  rawSend: (json) => {
28178
28300
  try {
28179
- const result = guardedChannel.send(json);
28301
+ const out = maybeCompress(json);
28302
+ const result = guardedChannel.send(out);
28180
28303
  if (result === "dropped" || result === "drain-pending") {
28181
28304
  const now = Date.now();
28182
28305
  const { thresholdCrossed } = rawSendDropsCounter.record(now);
@@ -28228,7 +28351,16 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
28228
28351
  error: err3 instanceof Error ? err3.message : String(err3)
28229
28352
  });
28230
28353
  });
28231
- const bestEffort = createBestEffortSender({ guardedChannel, logAdapter, controlPeerId });
28354
+ const bestEffort = createBestEffortSender({
28355
+ guardedChannel,
28356
+ logAdapter,
28357
+ controlPeerId,
28358
+ /**
28359
+ * Pass the compression gate so best-effort messages are also compressed for
28360
+ * v139+ peers. `maybeCompress` is a no-op for pre-v139 peers (safe default).
28361
+ */
28362
+ compress: maybeCompress
28363
+ });
28232
28364
  const aloSend = (msg) => {
28233
28365
  if (classifyCriticality(msg) === "best-effort") {
28234
28366
  bestEffort.send(msg);
@@ -28945,57 +29077,74 @@ function handleMessageChannel(opts) {
28945
29077
  }).catch(() => {
28946
29078
  });
28947
29079
  }
28948
- if (msg.maxMessages && msg.sinceSeqNo === 0) {
28949
- const limit = msg.maxMessages;
28950
- store.getMessagesBefore(channelId, Number.POSITIVE_INFINITY, limit).then(({ messages: page }) => {
28951
- const outgoing = {
28952
- type: "catch_up_chunk",
28953
- messages: page,
28954
- cursor: null
28955
- };
28956
- send(JSON.stringify(outgoing));
28957
- finalizeCatchUp();
28958
- logSubscribeDone(page.length);
28959
- }).catch((err3) => {
28960
- flushBufferedMessages();
28961
- sendError(formatError2(err3));
28962
- logSubscribeDone(0);
28963
- });
28964
- } else if (msg.maxMessages) {
28965
- store.getMessagesPaginated(channelId, msg.sinceSeqNo, msg.maxMessages).then(({ messages, nextSeqNo }) => {
28966
- const outgoing = {
28967
- type: "catch_up_chunk",
28968
- messages,
28969
- cursor: nextSeqNo
28970
- };
28971
- send(JSON.stringify(outgoing));
28972
- if (nextSeqNo === null) finalizeCatchUp();
28973
- else flushBufferedMessages();
28974
- logSubscribeDone(messages.length);
28975
- }).catch((err3) => {
28976
- flushBufferedMessages();
28977
- sendError(formatError2(err3));
28978
- logSubscribeDone(0);
28979
- });
28980
- } else {
28981
- store.getMessagesSince(channelId, msg.sinceSeqNo).then((messages) => {
28982
- const outgoing = { type: "catch_up", messages };
28983
- send(JSON.stringify(outgoing));
28984
- finalizeCatchUp();
28985
- logSubscribeDone(messages.length);
28986
- }).catch((err3) => {
28987
- flushBufferedMessages();
28988
- sendError(formatError2(err3));
28989
- logSubscribeDone(0);
28990
- });
28991
- }
29080
+ void taskManager.getTask(taskId).then((task) => {
29081
+ const clearedAtSeqNo = task?.clearedAtSeqNo;
29082
+ const sinceSeqNo = clearedAtSeqNo == null ? msg.sinceSeqNo : Math.max(msg.sinceSeqNo, clearedAtSeqNo + 1);
29083
+ if (msg.maxMessages && msg.sinceSeqNo === 0 && clearedAtSeqNo == null) {
29084
+ const limit = msg.maxMessages;
29085
+ store.getMessagesBefore(channelId, Number.POSITIVE_INFINITY, limit).then(({ messages: page }) => {
29086
+ const outgoing = {
29087
+ type: "catch_up_chunk",
29088
+ messages: page,
29089
+ cursor: null
29090
+ };
29091
+ send(JSON.stringify(outgoing));
29092
+ finalizeCatchUp();
29093
+ logSubscribeDone(page.length);
29094
+ }).catch((err3) => {
29095
+ flushBufferedMessages();
29096
+ sendError(formatError2(err3));
29097
+ logSubscribeDone(0);
29098
+ });
29099
+ } else if (msg.maxMessages) {
29100
+ store.getMessagesPaginated(channelId, sinceSeqNo, msg.maxMessages).then(({ messages, nextSeqNo }) => {
29101
+ const outgoing = {
29102
+ type: "catch_up_chunk",
29103
+ messages,
29104
+ cursor: nextSeqNo
29105
+ };
29106
+ send(JSON.stringify(outgoing));
29107
+ if (nextSeqNo === null) finalizeCatchUp();
29108
+ else flushBufferedMessages();
29109
+ logSubscribeDone(messages.length);
29110
+ }).catch((err3) => {
29111
+ flushBufferedMessages();
29112
+ sendError(formatError2(err3));
29113
+ logSubscribeDone(0);
29114
+ });
29115
+ } else {
29116
+ store.getMessagesSince(channelId, sinceSeqNo).then((messages) => {
29117
+ const outgoing = { type: "catch_up", messages };
29118
+ send(JSON.stringify(outgoing));
29119
+ finalizeCatchUp();
29120
+ logSubscribeDone(messages.length);
29121
+ }).catch((err3) => {
29122
+ flushBufferedMessages();
29123
+ sendError(formatError2(err3));
29124
+ logSubscribeDone(0);
29125
+ });
29126
+ }
29127
+ }).catch((err3) => {
29128
+ flushBufferedMessages();
29129
+ sendError(formatError2(err3));
29130
+ logSubscribeDone(0);
29131
+ });
28992
29132
  }
28993
29133
  function handleFetchOlder(msg) {
28994
- store.getMessagesBefore(channelId, msg.beforeSeqNo, msg.limit).then(({ messages, hasMore }) => {
29134
+ taskManager.getTask(taskId).then((task) => {
29135
+ const clearedAtSeqNo = task?.clearedAtSeqNo ?? -1;
29136
+ return store.getMessagesBefore(channelId, msg.beforeSeqNo, msg.limit).then((page) => ({
29137
+ ...page,
29138
+ clearedAtSeqNo
29139
+ }));
29140
+ }).then(({ messages, hasMore, clearedAtSeqNo }) => {
29141
+ const visibleMessages = messages.filter((m2) => m2.seqNo > clearedAtSeqNo);
29142
+ const oldestVisibleSeqNo = visibleMessages[0]?.seqNo ?? null;
29143
+ const visibleHasMore = visibleMessages.length === messages.length && hasMore && oldestVisibleSeqNo !== null && oldestVisibleSeqNo > clearedAtSeqNo + 1;
28995
29144
  const outgoing = {
28996
29145
  type: "older_messages",
28997
- messages,
28998
- hasMore
29146
+ messages: visibleMessages,
29147
+ hasMore: visibleHasMore
28999
29148
  };
29000
29149
  send(JSON.stringify(outgoing));
29001
29150
  }).catch((err3) => {
@@ -37508,9 +37657,14 @@ ${block.preview}` : `[File: ${block.filename}]`
37508
37657
  }
37509
37658
  }
37510
37659
  }
37660
+ function resolveEntryRole(msg) {
37661
+ if (msg.content.some((block) => block.type === "tool_result")) return "user";
37662
+ return msg.senderKind === "human" ? "user" : "assistant";
37663
+ }
37511
37664
  function portableMessageToEntry(msg, sessionId, parentUuid) {
37512
37665
  const apiContent = msg.content.map(portableBlockToApiContent).filter((c) => c !== null);
37513
- if (msg.senderKind === "human") {
37666
+ const role = resolveEntryRole(msg);
37667
+ if (role === "user") {
37514
37668
  const entry2 = {
37515
37669
  type: "user",
37516
37670
  uuid: msg.messageId,
@@ -38555,7 +38709,11 @@ import { randomUUID as randomUUID12 } from "crypto";
38555
38709
  import { copyFile, rm as fsRm, stat as fsStat3, mkdir as mkdir17 } from "fs/promises";
38556
38710
  import { basename as basename9, extname as extname2, join as join50, resolve as resolve8, sep as sep3 } from "path";
38557
38711
  var TOOL_DESCRIPTION2 = [
38558
- "Register a proof-of-work deliverable for this task. Call this after producing an artifact (screenshot, video, log, markdown document, AX tree, DOM snapshot) so reviewers can see verifiable evidence of what the agent produced.",
38712
+ "Register a proof-of-work deliverable for this task \u2014 proof a reviewer cannot get from the diff alone.",
38713
+ "",
38714
+ "WHEN: right after a change whose effect is not visible in the diff \u2014 a UI change (attach the rendered screenshot), a fix you proved by running tests (attach the passing log), a performance claim (attach the numbers), an accessibility audit (attach the AX tree).",
38715
+ "WHEN NOT: when the diff IS the proof \u2014 a refactor, a rename, a config edit verifies itself. Registering a deliverable then just costs the reviewer attention.",
38716
+ "EXAMPLE: after adding a PATCH route and confirming it returns 200, register the test-run log so the reviewer sees it passed without re-running it.",
38559
38717
  "",
38560
38718
  "SCOPING:",
38561
38719
  " When the current task has a structured todo list (visible in the <task-list> resource), scope the deliverable to the specific todoItemId it proves. This gives reviewers granular evidence per task step.",
@@ -39742,11 +39900,12 @@ var VISUALIZE_DESCRIPTION = [
39742
39900
  "",
39743
39901
  "Provide content inline OR pass file_path to import from disk.",
39744
39902
  "",
39745
- "WHEN to create proactively (don't wait to be asked):",
39903
+ "WHEN a picture beats a paragraph (don't wait to be asked, but produce one when it changes what a reviewer understands \u2014 not by reflex):",
39746
39904
  "- Architecture or system design -> mermaid",
39747
39905
  "- UI work or layout -> html mockup",
39748
39906
  "- Data or performance -> html with Chart.js/D3",
39749
- "- State machines or workflows -> mermaid stateDiagram"
39907
+ "- State machines or workflows -> mermaid stateDiagram",
39908
+ "When prose already carries the idea, skip the diagram."
39750
39909
  ].join("\n");
39751
39910
  var PRESENT_DESCRIPTION = [
39752
39911
  "Display something somewhere.",
@@ -40139,7 +40298,21 @@ async function guardTaskTarget(args, callingTaskId, ctx) {
40139
40298
  }
40140
40299
  async function handleSendMessage(taskId, prompt, title, callingTaskId, ctx) {
40141
40300
  try {
40142
- await ctx.taskManager.handleUserMessage(taskId, [{ type: "text", text: prompt }]);
40301
+ const callingSessionId = ctx.taskManager.getTaskState(callingTaskId)?.sessionId;
40302
+ if (!callingSessionId) {
40303
+ return errorResult("send_message failed: calling task has no active agent session.");
40304
+ }
40305
+ const agentParticipantId = buildAgentParticipantId(callingSessionId, ctx.agentSystem);
40306
+ await ctx.taskManager.handleUserMessage(
40307
+ taskId,
40308
+ [{ type: "text", text: `[From agent \xB7 task ${callingTaskId}]: ${prompt}` }],
40309
+ void 0,
40310
+ void 0,
40311
+ agentParticipantId,
40312
+ void 0,
40313
+ void 0,
40314
+ "agent"
40315
+ );
40143
40316
  } catch (err3) {
40144
40317
  const message = err3 instanceof Error ? err3.message : String(err3);
40145
40318
  logger.error({
@@ -49800,6 +49973,7 @@ function buildHarnessTools(ctx) {
49800
49973
  };
49801
49974
  const taskControlCtx = {
49802
49975
  callingTaskId: taskIdRef,
49976
+ agentSystem: ctx.agentSystem,
49803
49977
  taskManager: ctx.taskManager,
49804
49978
  taskStateStore: ctx.taskStateStore,
49805
49979
  getWorktreeRunner: ctx.getWorktreeRunner,
@@ -52655,6 +52829,7 @@ var McpCoordinator = class {
52655
52829
  }
52656
52830
  this.#timer.cancelAll();
52657
52831
  this.#deps.reauthOrchestrator.cancelAll();
52832
+ this.#deps.broadcaster.dispose();
52658
52833
  await Promise.all([this.#deps.proxyTransport.dispose(), this.#deps.stdioTransport.dispose()]);
52659
52834
  }
52660
52835
  /** ===== private helpers ===== */
@@ -54728,22 +54903,165 @@ function defaultRequestId() {
54728
54903
  return `reauth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
54729
54904
  }
54730
54905
 
54906
+ // src/services/event-batching.ts
54907
+ var EVENT_BATCHING = {
54908
+ /**
54909
+ * Comments: sliding window for review comment bursts.
54910
+ * 20 s idle matches observed user typing cadence in review sessions (#2218).
54911
+ */
54912
+ comments: { idleMs: 2e4, maxWindowMs: 6e4 },
54913
+ /**
54914
+ * Resources: sliding window for file/plan/git/task pushes.
54915
+ *
54916
+ * Inverted in Wave 4 of the unified context-delivery refactor from 20s
54917
+ * to 100ms. The 20s window made agent-triggered updates (TaskCreate /
54918
+ * TaskUpdate, structured-task mutations, etc.) invisible to the agent
54919
+ * for up to 20s after the tool call returned — the agent's next turn
54920
+ * would be reasoning on stale state. 100ms still coalesces within-tick
54921
+ * bursts and absorbs CRDT keystroke storms via each resolver's existing
54922
+ * projection cache (task-metadata, branch, canvas all gate no-op
54923
+ * re-emissions before the timer ever sees them).
54924
+ *
54925
+ * `maxWindowMs` retains the 60s cap as a guardrail for pathological
54926
+ * never-quiet sources.
54927
+ */
54928
+ resources: { idleMs: 100, maxWindowMs: 6e4 },
54929
+ /** Messages: sliding window for collab message micro-batching. */
54930
+ messages: { idleMs: 250, maxWindowMs: 3e4 },
54931
+ /**
54932
+ * MCP broadcaster: coalesce FSM-transition storms during MCP churn.
54933
+ *
54934
+ * Idle 150 ms, max 250 ms — the toggle-feel contract today is ~50 ms via
54935
+ * the direct `pushWorkspaceMcpStateSnapshot` path in `applyWorkspaceMcpToggle`
54936
+ * (which bypasses this debounce entirely). This window only applies to
54937
+ * daemon-internal FSM transitions that fire during background MCP churn
54938
+ * (reconnects, auth probes, plugin reloads). 250 ms max keeps the UI snappy
54939
+ * while collapsing the ~900 sends / 10s burst seen during MCP churn.
54940
+ */
54941
+ mcp: { idleMs: 150, maxWindowMs: 250 }
54942
+ };
54943
+ var MAX_COLLAB_BATCH_SIZE = 4;
54944
+ var SlidingWindowTimer = class {
54945
+ #config;
54946
+ #onFlush;
54947
+ #timer = null;
54948
+ #windowStart = null;
54949
+ #lastScheduleAt = null;
54950
+ #disposed = false;
54951
+ constructor(config, onFlush) {
54952
+ this.#config = config;
54953
+ this.#onFlush = onFlush;
54954
+ }
54955
+ /**
54956
+ * Schedule or reschedule the flush timer.
54957
+ *
54958
+ * - First call in a window: records `windowStart`, starts idle timer.
54959
+ * - Subsequent calls: resets idle timer (sliding window).
54960
+ * - If `maxWindowMs` exceeded: fires flush immediately.
54961
+ */
54962
+ schedule() {
54963
+ if (this.#disposed) return;
54964
+ if (this.#windowStart === null) {
54965
+ this.#windowStart = Date.now();
54966
+ } else if (Date.now() - this.#windowStart >= this.#config.maxWindowMs) {
54967
+ this.#clearTimer();
54968
+ this.#windowStart = null;
54969
+ this.#lastScheduleAt = null;
54970
+ this.#onFlush();
54971
+ return;
54972
+ }
54973
+ this.#clearTimer();
54974
+ this.#lastScheduleAt = Date.now();
54975
+ this.#timer = setTimeout(() => {
54976
+ this.#timer = null;
54977
+ this.#windowStart = null;
54978
+ this.#lastScheduleAt = null;
54979
+ this.#onFlush();
54980
+ }, this.#config.idleMs);
54981
+ }
54982
+ /** Cancel the pending timer without resetting the window. */
54983
+ cancel() {
54984
+ this.#clearTimer();
54985
+ }
54986
+ /** Reset the window start (call after draining the batch). */
54987
+ reset() {
54988
+ this.#windowStart = null;
54989
+ this.#lastScheduleAt = null;
54990
+ }
54991
+ /**
54992
+ * Epoch-ms at which the idle timer will fire, or null if no timer is
54993
+ * scheduled. Tracks the most recent `schedule()` call (sliding window),
54994
+ * not the original windowStart — used by ResourcePushManager to emit
54995
+ * accurate `flushAt` values for all dirty URIs after each timer slide.
54996
+ */
54997
+ get nextFireAt() {
54998
+ if (this.#timer === null || this.#lastScheduleAt === null) return null;
54999
+ return this.#lastScheduleAt + this.#config.idleMs;
55000
+ }
55001
+ /** Permanently shut down. */
55002
+ dispose() {
55003
+ this.#clearTimer();
55004
+ this.#windowStart = null;
55005
+ this.#lastScheduleAt = null;
55006
+ this.#disposed = true;
55007
+ }
55008
+ #clearTimer() {
55009
+ if (this.#timer !== null) {
55010
+ clearTimeout(this.#timer);
55011
+ this.#timer = null;
55012
+ }
55013
+ }
55014
+ };
55015
+
54731
55016
  // src/services/mcp/push/status-broadcaster.ts
54732
55017
  var StatusBroadcaster = class {
54733
55018
  #deps;
55019
+ /** Latest coalesced payload — replaced on every broadcast() call. */
55020
+ #pending = null;
55021
+ #timer;
55022
+ /** Serial flush promise — deferred per Invariant #11 (no sync re-entry). */
55023
+ #flushInProgress = Promise.resolve();
55024
+ #disposed = false;
54734
55025
  constructor(deps) {
54735
55026
  this.#deps = deps;
55027
+ this.#timer = new SlidingWindowTimer(EVENT_BATCHING.mcp, () => {
55028
+ this.#flushInProgress = this.#flushInProgress.then(() => this.#flush());
55029
+ });
54736
55030
  }
54737
55031
  /**
54738
- * Push the entire current snapshot to all peers.
55032
+ * Schedule a coalesced broadcast of the current snapshot to all peers.
54739
55033
  *
54740
55034
  * `disabledNames` is the live workspace VISIBILITY set from the coordinator.
54741
55035
  * Required per-call — without it every FSM transition re-broadcasts
54742
55036
  * `visible:true` and silently clobbers a persisted workspace-disable (the
54743
55037
  * #4203 regression). It arrives per-call rather than at construction so the
54744
55038
  * broadcaster stays a thin push adapter with no visibility-state of its own.
55039
+ *
55040
+ * Multiple calls within the 150ms idle / 250ms max window collapse to one
55041
+ * delivery of the LATEST pair.
54745
55042
  */
54746
55043
  broadcast(servers, disabledNames) {
55044
+ if (this.#disposed) return;
55045
+ this.#pending = { servers, disabledNames };
55046
+ this.#timer.schedule();
55047
+ }
55048
+ /**
55049
+ * Permanently shut down. Cancels the pending timer and discards any
55050
+ * coalesced snapshot — the next channel attach re-pushes fresh MCP state
55051
+ * via `attach-snapshot.ts:78` (`sendMcpStatusSnapshot`), so a discarded
55052
+ * pending snapshot self-heals on reconnect.
55053
+ */
55054
+ dispose() {
55055
+ this.#timer.dispose();
55056
+ this.#pending = null;
55057
+ this.#disposed = true;
55058
+ }
55059
+ #flush() {
55060
+ if (this.#disposed) return Promise.resolve();
55061
+ const snapshot = this.#pending;
55062
+ this.#pending = null;
55063
+ if (snapshot === null) return Promise.resolve();
55064
+ const { servers, disabledNames } = snapshot;
54747
55065
  try {
54748
55066
  this.#deps.send({ type: "mcp_server_status", servers });
54749
55067
  } catch (err3) {
@@ -54762,6 +55080,7 @@ var StatusBroadcaster = class {
54762
55080
  error: err3 instanceof Error ? err3.message : String(err3)
54763
55081
  });
54764
55082
  }
55083
+ return Promise.resolve();
54765
55084
  }
54766
55085
  };
54767
55086
 
@@ -66250,6 +66569,7 @@ var VisualizationFileWatcher = class {
66250
66569
  title: effect.title,
66251
66570
  content: effect.content
66252
66571
  });
66572
+ this.#updateCanvasElementContent(effect);
66253
66573
  break;
66254
66574
  }
66255
66575
  case "persist_registry":
@@ -66468,7 +66788,7 @@ var VisualizationFileWatcher = class {
66468
66788
  title: effect.title,
66469
66789
  content
66470
66790
  });
66471
- const data = { slug: effect.slug, title: effect.title };
66791
+ const data = { slug: effect.slug, title: effect.title, content };
66472
66792
  const nodeId = this.#deps.canvasRepo.createElement(effect.taskId, {
66473
66793
  type: effect.vizType,
66474
66794
  x: position.x,
@@ -66481,6 +66801,23 @@ var VisualizationFileWatcher = class {
66481
66801
  const followUp = this.registry.setCanvasElementId(effect.slug, `${nodeId}`);
66482
66802
  await this.executeEffects(followUp);
66483
66803
  }
66804
+ /**
66805
+ * Mirror the latest viz HTML into the canvas CRDT element's `data.content`
66806
+ * on hot-reload so the inline-content reload fallback (`html-element.tsx`)
66807
+ * stays current. Reads the existing element to preserve `slug`/`title`, then
66808
+ * writes the merged `data` blob. A missing element (evicted/raced) is a
66809
+ * no-op — the `viz_content` control push already covers the live path.
66810
+ */
66811
+ #updateCanvasElementContent(effect) {
66812
+ const elements = this.#deps.canvasRepo.getElements(effect.taskId);
66813
+ const target = elements.find((el) => `${el.id}` === effect.elementId);
66814
+ if (!target) return;
66815
+ const existing = target.data.data;
66816
+ const existingData = typeof existing === "object" && existing !== null && !Array.isArray(existing) ? existing : {};
66817
+ this.#deps.canvasRepo.updateElement(effect.taskId, target.id, {
66818
+ data: { ...existingData, slug: effect.slug, title: effect.title, content: effect.content }
66819
+ });
66820
+ }
66484
66821
  async #handleFileChange(filePath) {
66485
66822
  let content;
66486
66823
  try {
@@ -74257,105 +74594,6 @@ function drainPendingRuntimeSwitch(ctx, taskId) {
74257
74594
  );
74258
74595
  }
74259
74596
 
74260
- // src/services/event-batching.ts
74261
- var EVENT_BATCHING = {
74262
- /**
74263
- * Comments: sliding window for review comment bursts.
74264
- * 20 s idle matches observed user typing cadence in review sessions (#2218).
74265
- */
74266
- comments: { idleMs: 2e4, maxWindowMs: 6e4 },
74267
- /**
74268
- * Resources: sliding window for file/plan/git/task pushes.
74269
- *
74270
- * Inverted in Wave 4 of the unified context-delivery refactor from 20s
74271
- * to 100ms. The 20s window made agent-triggered updates (TaskCreate /
74272
- * TaskUpdate, structured-task mutations, etc.) invisible to the agent
74273
- * for up to 20s after the tool call returned — the agent's next turn
74274
- * would be reasoning on stale state. 100ms still coalesces within-tick
74275
- * bursts and absorbs CRDT keystroke storms via each resolver's existing
74276
- * projection cache (task-metadata, branch, canvas all gate no-op
74277
- * re-emissions before the timer ever sees them).
74278
- *
74279
- * `maxWindowMs` retains the 60s cap as a guardrail for pathological
74280
- * never-quiet sources.
74281
- */
74282
- resources: { idleMs: 100, maxWindowMs: 6e4 },
74283
- /** Messages: sliding window for collab message micro-batching. */
74284
- messages: { idleMs: 250, maxWindowMs: 3e4 }
74285
- };
74286
- var MAX_COLLAB_BATCH_SIZE = 4;
74287
- var SlidingWindowTimer = class {
74288
- #config;
74289
- #onFlush;
74290
- #timer = null;
74291
- #windowStart = null;
74292
- #lastScheduleAt = null;
74293
- #disposed = false;
74294
- constructor(config, onFlush) {
74295
- this.#config = config;
74296
- this.#onFlush = onFlush;
74297
- }
74298
- /**
74299
- * Schedule or reschedule the flush timer.
74300
- *
74301
- * - First call in a window: records `windowStart`, starts idle timer.
74302
- * - Subsequent calls: resets idle timer (sliding window).
74303
- * - If `maxWindowMs` exceeded: fires flush immediately.
74304
- */
74305
- schedule() {
74306
- if (this.#disposed) return;
74307
- if (this.#windowStart === null) {
74308
- this.#windowStart = Date.now();
74309
- } else if (Date.now() - this.#windowStart >= this.#config.maxWindowMs) {
74310
- this.#clearTimer();
74311
- this.#windowStart = null;
74312
- this.#lastScheduleAt = null;
74313
- this.#onFlush();
74314
- return;
74315
- }
74316
- this.#clearTimer();
74317
- this.#lastScheduleAt = Date.now();
74318
- this.#timer = setTimeout(() => {
74319
- this.#timer = null;
74320
- this.#windowStart = null;
74321
- this.#lastScheduleAt = null;
74322
- this.#onFlush();
74323
- }, this.#config.idleMs);
74324
- }
74325
- /** Cancel the pending timer without resetting the window. */
74326
- cancel() {
74327
- this.#clearTimer();
74328
- }
74329
- /** Reset the window start (call after draining the batch). */
74330
- reset() {
74331
- this.#windowStart = null;
74332
- this.#lastScheduleAt = null;
74333
- }
74334
- /**
74335
- * Epoch-ms at which the idle timer will fire, or null if no timer is
74336
- * scheduled. Tracks the most recent `schedule()` call (sliding window),
74337
- * not the original windowStart — used by ResourcePushManager to emit
74338
- * accurate `flushAt` values for all dirty URIs after each timer slide.
74339
- */
74340
- get nextFireAt() {
74341
- if (this.#timer === null || this.#lastScheduleAt === null) return null;
74342
- return this.#lastScheduleAt + this.#config.idleMs;
74343
- }
74344
- /** Permanently shut down. */
74345
- dispose() {
74346
- this.#clearTimer();
74347
- this.#windowStart = null;
74348
- this.#lastScheduleAt = null;
74349
- this.#disposed = true;
74350
- }
74351
- #clearTimer() {
74352
- if (this.#timer !== null) {
74353
- clearTimeout(this.#timer);
74354
- this.#timer = null;
74355
- }
74356
- }
74357
- };
74358
-
74359
74597
  // src/services/collab/collab-message-queue.ts
74360
74598
  var UNKNOWN_PARTICIPANT = "__unknown__";
74361
74599
  function formatCollabBatch(batch) {
@@ -78346,6 +78584,8 @@ var Thread = class {
78346
78584
  #sendControlMessage;
78347
78585
  #streamDelta = new ThreadStreamDeltaForwarder();
78348
78586
  #latestSettings = {};
78587
+ #lastApiUsage = null;
78588
+ #lastTurnResult = null;
78349
78589
  /**
78350
78590
  * Last values pushed to the live subprocess. Reset to undefined whenever
78351
78591
  * a fresh subprocess is attached (settings get applied via spawn options
@@ -79227,6 +79467,7 @@ var Thread = class {
79227
79467
  return;
79228
79468
  }
79229
79469
  if (event.type === "api_usage_snapshot") {
79470
+ this.#lastApiUsage = event;
79230
79471
  this.#callbacks.onSubprocessEvent?.(event);
79231
79472
  return;
79232
79473
  }
@@ -79365,6 +79606,7 @@ var Thread = class {
79365
79606
  this.#recovery.resetRetriesOnTurnComplete();
79366
79607
  this.#cursorRecovery.reset();
79367
79608
  this.#rateLimitFlap.noteRecoveredOnTurnComplete((event.result.outputTokens ?? 0) > 0);
79609
+ this.#emitThreadTurnStats(event.result);
79368
79610
  this.#manager.notifyTurnComplete();
79369
79611
  break;
79370
79612
  case "sdk_error":
@@ -79498,6 +79740,27 @@ var Thread = class {
79498
79740
  }
79499
79741
  this.#callbacks.onSubprocessEvent?.(event);
79500
79742
  }
79743
+ #emitThreadTurnStats(result) {
79744
+ if (!this.#config.permissionThreadId) return;
79745
+ const prevResult = this.#lastTurnResult;
79746
+ this.#lastTurnResult = result;
79747
+ const stats = buildTurnStatsMessage(
79748
+ this.#config.permissionTaskId ?? this.#config.threadId,
79749
+ result,
79750
+ null,
79751
+ this.#lastApiUsage,
79752
+ prevResult,
79753
+ void 0,
79754
+ this.#latestSettings.model ?? void 0,
79755
+ null
79756
+ );
79757
+ this.#lastApiUsage = null;
79758
+ if (stats.type !== "turn_stats") return;
79759
+ this.#sendControlMessage?.({
79760
+ ...stats,
79761
+ threadId: this.#config.permissionThreadId
79762
+ });
79763
+ }
79501
79764
  };
79502
79765
 
79503
79766
  // src/services/plan/plan-handler.ts
@@ -79511,7 +79774,12 @@ import { dirname as dirname34, join as join72 } from "path";
79511
79774
  var SETTLE_BUDGET_MS = 2e3;
79512
79775
  async function ensurePlanDocRebuilt(deps) {
79513
79776
  try {
79514
- if (await settleAndCheckContent(deps)) return { kind: "has-content" };
79777
+ const verdict = await settleAndCheckContent(deps);
79778
+ if (verdict === "has-content") return { kind: "has-content" };
79779
+ if (verdict === "storage-error") {
79780
+ deps.log({ event: "plan_lazy_rebuild_storage_error", taskId: deps.taskId });
79781
+ return { kind: "storage-error" };
79782
+ }
79515
79783
  const sources = await gatherBackfillSources(
79516
79784
  deps.taskRecord,
79517
79785
  deps.taskId,
@@ -79553,7 +79821,9 @@ async function settleAndCheckContent(deps) {
79553
79821
  await sync(doc).waitForSync({ kind: "storage", timeout: SETTLE_BUDGET_MS });
79554
79822
  } catch {
79555
79823
  }
79556
- return deps.planRepo.getContent(deps.taskId).trim() !== "";
79824
+ if (deps.planRepo.getContent(deps.taskId).trim() !== "") return "has-content";
79825
+ const onDisk = await (deps.hasOnDiskDoc?.() ?? deps.planRepo.hasOnDiskDoc(deps.taskId));
79826
+ return onDisk === true ? "storage-error" : "absent";
79557
79827
  }
79558
79828
 
79559
79829
  // src/services/plan/plan-detection-policy.ts
@@ -79622,6 +79892,21 @@ var PlanFileBridge = class _PlanFileBridge {
79622
79892
  #disposed = false;
79623
79893
  #hasAttachedContent = false;
79624
79894
  #writeBackInProgress = null;
79895
+ /**
79896
+ * Pending FM-8 re-attach scheduled after a recoverable wasm panic. Cancelled
79897
+ * on dispose so a torn-down bridge never re-attaches against a dead task.
79898
+ */
79899
+ #reattachTimer = null;
79900
+ /**
79901
+ * One-shot loop guard: true while a panic-recovery re-attach is scheduled or
79902
+ * running. The re-attach itself calls `updateContent`, which can panic again
79903
+ * (on-disk chunks still poisoned). Without this gate that second panic would
79904
+ * schedule a third re-attach, and so on — a hot recovery loop. Reset only when
79905
+ * the re-attach finishes, so a single bounded retry fires per poisoning and a
79906
+ * persistently-poisoned doc falls back to the next real file-change event (the
79907
+ * prior recovery cadence), never a spin.
79908
+ */
79909
+ #reattachAfterPanicInFlight = false;
79625
79910
  static WRITE_BACK_DEBOUNCE_MS = 1e3;
79626
79911
  static FILE_READ_DEBOUNCE_MS = 500;
79627
79912
  #lastFileReadTime = 0;
@@ -79664,6 +79949,10 @@ var PlanFileBridge = class _PlanFileBridge {
79664
79949
  */
79665
79950
  async attach(filePath) {
79666
79951
  if (this.#disposed) return;
79952
+ if (this.#reattachTimer) {
79953
+ clearTimeout(this.#reattachTimer);
79954
+ this.#reattachTimer = null;
79955
+ }
79667
79956
  void this.#dirWatcher?.unsubscribe();
79668
79957
  this.#dirWatcher = null;
79669
79958
  this.#crdtUnsub?.();
@@ -79905,10 +80194,46 @@ var PlanFileBridge = class _PlanFileBridge {
79905
80194
  callSite: "plan-file-bridge.updateContent",
79906
80195
  errorMessage: err3 instanceof Error ? err3.message : String(err3)
79907
80196
  });
80197
+ this.#scheduleReattachAfterPanic();
79908
80198
  }
79909
80199
  return err3;
79910
80200
  }
79911
80201
  }
80202
+ /**
80203
+ * FM-8: re-attach to the current file after a recoverable wasm panic so the
80204
+ * fresh (post-eviction) CRDT is repopulated immediately. One-shot per
80205
+ * poisoning via `#reattachAfterPanicInFlight` — a re-attach that panics again
80206
+ * does NOT reschedule; recovery then falls back to the next real file-change
80207
+ * event. No-op when disposed, when there is no file to re-read, or when a
80208
+ * re-attach is already pending/running.
80209
+ */
80210
+ #scheduleReattachAfterPanic() {
80211
+ if (this.#disposed || !this.#filePath || this.#reattachAfterPanicInFlight) return;
80212
+ this.#reattachAfterPanicInFlight = true;
80213
+ const filePath = this.#filePath;
80214
+ this.#reattachTimer = setTimeout(() => {
80215
+ this.#reattachTimer = null;
80216
+ if (this.#disposed) {
80217
+ this.#reattachAfterPanicInFlight = false;
80218
+ return;
80219
+ }
80220
+ this.#config.log({
80221
+ event: "plan_bridge_reattach_after_panic",
80222
+ taskId: this.#config.taskId,
80223
+ filePath
80224
+ });
80225
+ this.attach(filePath).catch((err3) => {
80226
+ this.#config.log({
80227
+ event: "plan_bridge_reattach_after_panic_failed",
80228
+ taskId: this.#config.taskId,
80229
+ filePath,
80230
+ error: err3 instanceof Error ? err3.message : String(err3)
80231
+ });
80232
+ }).finally(() => {
80233
+ this.#reattachAfterPanicInFlight = false;
80234
+ });
80235
+ }, 0);
80236
+ }
79912
80237
  /**
79913
80238
  * Subscribe to CRDT changes (from browser edits arriving via WebRTC).
79914
80239
  * Debounced write-back to the plan file on disk.
@@ -80016,6 +80341,10 @@ var PlanFileBridge = class _PlanFileBridge {
80016
80341
  clearTimeout(this.#writeBackTimer);
80017
80342
  this.#writeBackTimer = null;
80018
80343
  }
80344
+ if (this.#reattachTimer) {
80345
+ clearTimeout(this.#reattachTimer);
80346
+ this.#reattachTimer = null;
80347
+ }
80019
80348
  }
80020
80349
  /**
80021
80350
  * Test-only: seed internal state so `flushWriteBack` can be exercised
@@ -84597,6 +84926,7 @@ function dispatchToCompactionSM(event, profile, now) {
84597
84926
  // src/services/task/orchestrator/task-event-dispatch.ts
84598
84927
  var AUTO_COMPACTION_CANCEL_DELAY_MS = 3e3;
84599
84928
  var AUTO_COMPACTION_REARM_DELTA = 0.05;
84929
+ var TASK_SNAPSHOT_URI_PREFIX2 = "shipyard://task-snapshot/";
84600
84930
  var AUTO_COMPACTION_MESSAGE_CEILING = 3e3;
84601
84931
  function decideAutoCompaction(params) {
84602
84932
  if (params.threshold === null) return { kind: "noop" };
@@ -84830,9 +85160,21 @@ function handleToolResultEcho(ctx, event) {
84830
85160
  ctx.resolveStructuredTaskCreates(event.content);
84831
85161
  ctx.handleWorktreeToolResults(event.content);
84832
85162
  ctx.handleSubagentToolResults(event.content);
85163
+ trackTaskSnapshotLinksFromToolResults(ctx, event.content);
84833
85164
  ctx.trackPlanFileCreation(event.content);
84834
85165
  ctx.triggerCommitScan();
84835
85166
  }
85167
+ function trackTaskSnapshotLinksFromToolResults(ctx, content) {
85168
+ for (const block of content) {
85169
+ if (block.type !== "tool_result" || typeof block.content !== "string") continue;
85170
+ if (!block.content.includes(TASK_SNAPSHOT_URI_PREFIX2)) continue;
85171
+ for (const uri of extractToolResultResourceUris(block.content)) {
85172
+ if (uri.startsWith(TASK_SNAPSHOT_URI_PREFIX2)) {
85173
+ ctx.trackTaskSnapshotResource(uri);
85174
+ }
85175
+ }
85176
+ }
85177
+ }
84836
85178
  function resolveCompactionEvents(ctx) {
84837
85179
  const parsed = AgentIdSchema.safeParse(ctx.getRuntimeId());
84838
85180
  if (!parsed.success) return null;
@@ -87164,6 +87506,12 @@ function applyDepEdgeAdditions(merged, depEdges) {
87164
87506
  }
87165
87507
  }
87166
87508
  }
87509
+ function findOverlayTask(overlay, taskId) {
87510
+ return overlay?.userTasks.find((task) => task.id === taskId);
87511
+ }
87512
+ function getStructuredTaskForUpdate(tasks, overlay, taskId) {
87513
+ return tasks.get(taskId) ?? findOverlayTask(overlay, taskId);
87514
+ }
87167
87515
  function applyCapabilitiesOverrides(merged, overrides) {
87168
87516
  if (!overrides) return;
87169
87517
  for (const [id, caps] of Object.entries(overrides)) {
@@ -87487,22 +87835,16 @@ var StructuredTaskTracker = class {
87487
87835
  }
87488
87836
  return false;
87489
87837
  }
87490
- const existing = this.#structuredTasks.get(taskId);
87838
+ const existing = getStructuredTaskForUpdate(
87839
+ this.#structuredTasks,
87840
+ this.#currentOverlay,
87841
+ taskId
87842
+ );
87491
87843
  if (existing) {
87492
87844
  const prevStatus = existing.status;
87493
87845
  const updated = applyStructuredTaskChanges(existing, changes);
87494
87846
  this.#structuredTasks.set(taskId, updated);
87495
- if (prevStatus !== "in_progress" && updated.status === "in_progress") {
87496
- const caps = this.#resolveEffectiveCaps(taskId, updated);
87497
- this.#maybeFireItemActivated(taskId, caps);
87498
- }
87499
- return true;
87500
- }
87501
- const overlayTask = this.#currentOverlay?.userTasks.find((t) => t.id === taskId);
87502
- if (overlayTask) {
87503
- const prevStatus = overlayTask.status;
87504
- const updated = applyStructuredTaskChanges(overlayTask, changes);
87505
- this.#structuredTasks.set(taskId, updated);
87847
+ this.#applyReciprocalDependencyChanges(taskId, changes);
87506
87848
  if (prevStatus !== "in_progress" && updated.status === "in_progress") {
87507
87849
  const caps = this.#resolveEffectiveCaps(taskId, updated);
87508
87850
  this.#maybeFireItemActivated(taskId, caps);
@@ -87516,6 +87858,36 @@ var StructuredTaskTracker = class {
87516
87858
  });
87517
87859
  return false;
87518
87860
  }
87861
+ #applyReciprocalDependencyChanges(taskId, changes) {
87862
+ for (const blockedTaskId of changes.addBlocks ?? []) {
87863
+ const blockedTask = getStructuredTaskForUpdate(
87864
+ this.#structuredTasks,
87865
+ this.#currentOverlay,
87866
+ blockedTaskId
87867
+ );
87868
+ if (blockedTask && !blockedTask.blockedBy.includes(taskId)) {
87869
+ this.#structuredTasks.set(blockedTaskId, {
87870
+ ...blockedTask,
87871
+ blockedBy: [...blockedTask.blockedBy, taskId],
87872
+ updatedAt: Date.now()
87873
+ });
87874
+ }
87875
+ }
87876
+ for (const prerequisiteTaskId of changes.addBlockedBy ?? []) {
87877
+ const prerequisiteTask = getStructuredTaskForUpdate(
87878
+ this.#structuredTasks,
87879
+ this.#currentOverlay,
87880
+ prerequisiteTaskId
87881
+ );
87882
+ if (prerequisiteTask && !prerequisiteTask.blocks.includes(taskId)) {
87883
+ this.#structuredTasks.set(prerequisiteTaskId, {
87884
+ ...prerequisiteTask,
87885
+ blocks: [...prerequisiteTask.blocks, taskId],
87886
+ updatedAt: Date.now()
87887
+ });
87888
+ }
87889
+ }
87890
+ }
87519
87891
  resolveStructuredTaskCreates(content) {
87520
87892
  this.#deps.log({
87521
87893
  event: "debug_resolve_creates",
@@ -88971,6 +89343,10 @@ var UsageTracker = class {
88971
89343
  };
88972
89344
 
88973
89345
  // src/services/task/orchestrator/task.ts
89346
+ var WORKTREE_ESCAPE_WARNING = "Working directory is now outside the Shipyard-managed worktree \u2014 the Changes tab will be empty until you return (cd back, or ExitWorktree).";
89347
+ function isOutsideTaskWorktreeRoot(newCwd, originalCwd) {
89348
+ return originalCwd != null && newCwd !== originalCwd && !newCwd.startsWith(`${originalCwd}/`);
89349
+ }
88974
89350
  var Task = class {
88975
89351
  #deps;
88976
89352
  #threads = /* @__PURE__ */ new Map();
@@ -88984,6 +89360,7 @@ var Task = class {
88984
89360
  /** Task-level state */
88985
89361
  #cwd;
88986
89362
  #originalCwd;
89363
+ #cwdEscapeWarningActive = false;
88987
89364
  #latestSettings = {};
88988
89365
  #pendingMessageMeta = [];
88989
89366
  #pendingWorktreeToolUseIds = /* @__PURE__ */ new Set();
@@ -90469,8 +90846,9 @@ var Task = class {
90469
90846
  return ack;
90470
90847
  }
90471
90848
  /**
90472
- * Clear the session: stop the agent, wipe conversation history, and reset to cold_idle.
90473
- * The next user message will start a completely fresh session.
90849
+ * Clear visible session state and reset to cold_idle. The JSONL transcript
90850
+ * stays on disk; subscribers hide prior rows via TaskRecord.clearedAtSeqNo.
90851
+ * The next user message will start a completely fresh SDK session.
90474
90852
  */
90475
90853
  clearSession() {
90476
90854
  this.#deps.log({ event: "clear_session", taskId: this.#deps.taskId });
@@ -90499,11 +90877,18 @@ var Task = class {
90499
90877
  this.#getPrimary().enqueueAsync(async () => {
90500
90878
  this.#compaction.reset();
90501
90879
  await this.#sideThreads.dispose();
90880
+ let clearedAtSeqNo = -1;
90881
+ let markerPersisted = false;
90502
90882
  try {
90503
- await this.#deps.store.truncateAfter(this.#deps.channelId, -1);
90883
+ const messages = await this.#deps.store.getMessages(this.#deps.channelId);
90884
+ clearedAtSeqNo = messages.at(-1)?.seqNo ?? -1;
90885
+ await this.#deps.taskStateStore.setClearedAtSeqNo(this.#deps.taskId, clearedAtSeqNo, {
90886
+ broadcast: false
90887
+ });
90888
+ markerPersisted = true;
90504
90889
  } catch (err3) {
90505
90890
  this.#deps.log({
90506
- event: "clear_session_truncate_failed",
90891
+ event: "clear_session_marker_failed",
90507
90892
  taskId: this.#deps.taskId,
90508
90893
  error: err3 instanceof Error ? err3.message : String(err3)
90509
90894
  });
@@ -90517,9 +90902,11 @@ var Task = class {
90517
90902
  error: err3 instanceof Error ? err3.message : String(err3)
90518
90903
  });
90519
90904
  }
90905
+ if (!markerPersisted) return;
90520
90906
  this.#broadcastToAllPeers?.({
90521
90907
  type: "session_cleared",
90522
- taskId: this.#deps.taskId
90908
+ taskId: this.#deps.taskId,
90909
+ clearedAtSeqNo
90523
90910
  });
90524
90911
  });
90525
90912
  }
@@ -91009,6 +91396,26 @@ var Task = class {
91009
91396
  prevCwd: prevCwd ?? null,
91010
91397
  originalCwd: this.#originalCwd ?? null
91011
91398
  });
91399
+ this.#applyCwdEscapeWarning(newCwd);
91400
+ }
91401
+ #applyCwdEscapeWarning(newCwd) {
91402
+ const originalCwd = this.#originalCwd;
91403
+ const escaped = isOutsideTaskWorktreeRoot(newCwd, originalCwd);
91404
+ if (escaped) {
91405
+ if (this.#cwdEscapeWarningActive) return;
91406
+ this.#cwdEscapeWarningActive = true;
91407
+ this.#deps.log({
91408
+ event: "worktree_escaped_task_root",
91409
+ taskId: this.#deps.taskId,
91410
+ newCwd,
91411
+ originalCwd: originalCwd ?? null
91412
+ });
91413
+ this.#handleTaskError(WORKTREE_ESCAPE_WARNING, "sdk_error");
91414
+ return;
91415
+ }
91416
+ if (!this.#cwdEscapeWarningActive) return;
91417
+ this.#cwdEscapeWarningActive = false;
91418
+ this.#broadcastToAllPeers?.({ type: "task_error_cleared", taskId: this.#deps.taskId });
91012
91419
  }
91013
91420
  #trackPlanFileFromToolUse(content) {
91014
91421
  trackPlanFileFromToolUse(content, (path5) => this.#planHandler.trackCreatedFile(path5));
@@ -91211,6 +91618,7 @@ var Task = class {
91211
91618
  this.#pushManager.track(buildDevServersUri(this.#deps.taskId));
91212
91619
  this.#pushManager.track(buildTerminalsUri(this.#deps.taskId));
91213
91620
  },
91621
+ trackTaskSnapshotResource: (uri) => this.#pushManager.track(uri),
91214
91622
  pushManagerTurnStart: () => this.#pushManager.onTurnStart(),
91215
91623
  pushManagerTurnEnd: () => this.#pushManager.onTurnEnd(),
91216
91624
  broadcastSessionMcps: (mcpServers) => this.#broadcastSessionMcps(mcpServers),
@@ -91929,7 +92337,7 @@ async function persistUserMessageAtChokepoint(args) {
91929
92337
  messageId: randomUUID24(),
91930
92338
  channelId: args.channelId,
91931
92339
  participantId: args.participantId ?? buildHumanParticipantId(args.userId),
91932
- senderKind: "human",
92340
+ senderKind: args.senderKind ?? "human",
91933
92341
  content: stripBlobResourcesForStorage(args.content),
91934
92342
  timestamp: Date.now(),
91935
92343
  ...persistedCorrelationId !== void 0 && { correlationId: persistedCorrelationId },
@@ -93416,7 +93824,7 @@ var TaskManager = class {
93416
93824
  });
93417
93825
  return task;
93418
93826
  }
93419
- async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName, correlationId) {
93827
+ async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName, correlationId, senderKind = "human") {
93420
93828
  const task = await this.#ensureTaskForUserMessage(taskId);
93421
93829
  if (cwd && cwd !== task.cwd) {
93422
93830
  if (task.mode === "review" && !task.cwd) {
@@ -93446,6 +93854,7 @@ var TaskManager = class {
93446
93854
  settings: normalizedSettings,
93447
93855
  latestSettings: task.orchestrator.latestSettings,
93448
93856
  participantId,
93857
+ senderKind,
93449
93858
  correlationId
93450
93859
  });
93451
93860
  task.orchestrator.handleUserMessage(
@@ -94061,20 +94470,21 @@ function fingerprintSkillRows(rows) {
94061
94470
  }
94062
94471
 
94063
94472
  // src/services/task-snapshot-resource-resolver.ts
94064
- var TASK_SNAPSHOT_URI_PREFIX2 = "shipyard://task-snapshot/";
94473
+ var TASK_SNAPSHOT_URI_PREFIX3 = "shipyard://task-snapshot/";
94065
94474
  var PREVIEW_CHARS = 240;
94475
+ var HUMAN_STEERING_LIMIT = 3;
94066
94476
  var TASK_ID_SEGMENT_PATTERN = /^[A-Za-z0-9_-]+$/;
94067
94477
  function buildTaskSnapshotResourceUri(taskId) {
94068
94478
  if (!TASK_ID_SEGMENT_PATTERN.test(taskId)) {
94069
94479
  throw new Error(`Invalid taskId for task-snapshot URI: ${taskId}`);
94070
94480
  }
94071
- return `${TASK_SNAPSHOT_URI_PREFIX2}${taskId}`;
94481
+ return `${TASK_SNAPSHOT_URI_PREFIX3}${taskId}`;
94072
94482
  }
94073
94483
  function parseTaskSnapshotResourceUri(uri) {
94074
- if (!uri.startsWith(TASK_SNAPSHOT_URI_PREFIX2)) {
94484
+ if (!uri.startsWith(TASK_SNAPSHOT_URI_PREFIX3)) {
94075
94485
  throw new Error(`Invalid task-snapshot URI: ${uri}`);
94076
94486
  }
94077
- const taskId = uri.slice(TASK_SNAPSHOT_URI_PREFIX2.length);
94487
+ const taskId = uri.slice(TASK_SNAPSHOT_URI_PREFIX3.length);
94078
94488
  if (!TASK_ID_SEGMENT_PATTERN.test(taskId)) {
94079
94489
  throw new Error(`Invalid task-snapshot URI: ${uri}`);
94080
94490
  }
@@ -94112,9 +94522,23 @@ function renderTaskSnapshot(ctx) {
94112
94522
  lines.push(`| planFilePath | ${ctx.planFilePath ?? "(none)"} |`);
94113
94523
  lines.push(`| lastAssistantTurnPreview | ${ctx.lastAssistantTurnPreview ?? "(none)"} |`);
94114
94524
  lines.push(`| deliverableCount | ${ctx.deliverables.length} |`);
94525
+ lines.push(...renderHumanSteeringSection(ctx.humanSteering));
94526
+ lines.push(...renderApprovedPlanSection(ctx.approvedPlanSummary));
94115
94527
  lines.push(...renderDeliverablesSection(ctx.deliverables));
94116
94528
  return lines.join("\n");
94117
94529
  }
94530
+ function renderHumanSteeringSection(steering) {
94531
+ if (steering.length === 0) return [];
94532
+ const out = ["", "## Human steering", ""];
94533
+ for (const item of steering) {
94534
+ out.push(`- ${formatTimestamp(item.timestamp)}: ${escapeListItem(item.text)}`);
94535
+ }
94536
+ return out;
94537
+ }
94538
+ function renderApprovedPlanSection(summary) {
94539
+ if (!summary) return [];
94540
+ return ["", "## Approved plan", "", escapeListItem(summary)];
94541
+ }
94118
94542
  function renderDeliverablesSection(deliverables) {
94119
94543
  if (deliverables.length === 0) return [];
94120
94544
  const out = [
@@ -94135,6 +94559,9 @@ function renderDeliverablesSection(deliverables) {
94135
94559
  function escapeCell(text) {
94136
94560
  return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
94137
94561
  }
94562
+ function escapeListItem(text) {
94563
+ return text.replace(/\s+/g, " ").trim();
94564
+ }
94138
94565
  function extractTaskSnapshotDigest(ctx) {
94139
94566
  const r = ctx.record;
94140
94567
  const ci = ctx.ci ? `${ctx.ci.passing}/${ctx.ci.failing}/${ctx.ci.pending}` : "";
@@ -94169,6 +94596,8 @@ function extractTaskSnapshotDigest(ctx) {
94169
94596
  ctx.awaitingPlanApproval,
94170
94597
  ctx.planFilePath,
94171
94598
  ctx.lastAssistantTurnPreview,
94599
+ ctx.humanSteering.map((m2) => `${m2.seqNo}:${m2.text}`).join(","),
94600
+ ctx.approvedPlanSummary,
94172
94601
  /**
94173
94602
  * Deliverable digest: count + ordered (id, kind) tuples joined to a
94174
94603
  * single string so the digest array stays primitive-typed. Registration
@@ -94248,7 +94677,25 @@ function findLastAssistantPreview(messages) {
94248
94677
  }
94249
94678
  return null;
94250
94679
  }
94680
+ function findRecentHumanSteering(messages, opts) {
94681
+ const humanMessages = messages.filter((m2) => m2.senderKind === "human");
94682
+ const steering = opts.hasEarlierMessages ? humanMessages : humanMessages.slice(1);
94683
+ return steering.map((m2) => {
94684
+ const text = collectMessageText(m2);
94685
+ if (!text) return null;
94686
+ return { seqNo: m2.seqNo, timestamp: m2.timestamp, text: previewSlice(text) };
94687
+ }).filter((m2) => m2 !== null).slice(-HUMAN_STEERING_LIMIT);
94688
+ }
94689
+ function summarizeApprovedPlan(markdown) {
94690
+ if (!markdown) return null;
94691
+ const firstMeaningfulLine = markdown.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
94692
+ if (!firstMeaningfulLine) return null;
94693
+ return previewSlice(firstMeaningfulLine.replace(/^#{1,6}\s+/, ""));
94694
+ }
94251
94695
  function collectAgentText(m2) {
94696
+ return collectMessageText(m2);
94697
+ }
94698
+ function collectMessageText(m2) {
94252
94699
  const parts = [];
94253
94700
  for (const block of m2.content) {
94254
94701
  if (block.type === "text" && block.text) parts.push(block.text);
@@ -94396,6 +94843,8 @@ function wireTaskSnapshotResource(deps) {
94396
94843
  mergeable: prSummary.mergeable,
94397
94844
  branchName,
94398
94845
  lastAssistantTurnPreview: findLastAssistantPreview(tail.messages),
94846
+ humanSteering: findRecentHumanSteering(tail.messages, { hasEarlierMessages: tail.hasMore }),
94847
+ approvedPlanSummary: planDetection?.approved === true ? summarizeApprovedPlan(planDetection.originalMarkdown) : null,
94399
94848
  deliverables
94400
94849
  };
94401
94850
  }
@@ -95464,6 +95913,7 @@ async function createDaemon(deps) {
95464
95913
  return dispatchSpawn(decision, {
95465
95914
  onEvent,
95466
95915
  initialContent: initialContent ?? [],
95916
+ log: deps.log,
95467
95917
  spawnClaude,
95468
95918
  spawnCursor,
95469
95919
  spawnCodex: () => {
@@ -100175,4 +100625,4 @@ export {
100175
100625
  decideWorkspaceScope,
100176
100626
  serve
100177
100627
  };
100178
- //# sourceMappingURL=serve-2N7BIGRO.js.map
100628
+ //# sourceMappingURL=serve-3N7IHREI.js.map