@schoolai/shipyard 3.2.0-rc.20260420.1 → 3.2.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.
@@ -46,7 +46,7 @@ import {
46
46
  VaultKeyPutRequestSchema,
47
47
  VaultKeyPutResponseSchema,
48
48
  classifyClaudeCodeCompatibility
49
- } from "./chunk-MEIJOKPG.js";
49
+ } from "./chunk-5SJBSLGT.js";
50
50
  import "./chunk-EHQITHQX.js";
51
51
  import {
52
52
  loadAuthToken
@@ -25381,7 +25381,7 @@ var FEATURE_WORK = {
25381
25381
  item(FEATURE_WORK_ID, 1, "Research the task", "Map the surface area before writing code. Identify critical files, existing patterns, edge cases, and gotchas. For larger tasks, spawn parallel Shipyard threads to explore independent sub-areas. Pin key findings on Shipyard's canvas so they stay visible during implementation.", []),
25382
25382
  item(FEATURE_WORK_ID, 2, "Plan the approach", "Iterate on an approach on Shipyard's canvas with diagrams, state machines, or data flow so architectural decisions persist through the work. For risky or test-heavy work, write a test spec up front to drive red-green-refactor.", [1]),
25383
25383
  item(FEATURE_WORK_ID, 3, "Sequence implementation", "Break the plan into parallelizable subtasks with explicit dependencies wired between them using TaskUpdate's addBlockedBy / addBlocks. Identify what can be done by parallel Shipyard threads versus what must happen sequentially.", [2]),
25384
- item(FEATURE_WORK_ID, 4, "Implementation", "Replace this placeholder with concrete implementation subtasks via TaskCreate. The new tasks should be blocked by (3) and should block (5). Delete this placeholder after inserting the real work.", [3]),
25384
+ item(FEATURE_WORK_ID, 4, "Implementation \u2014 REPLACE this placeholder", "This is a placeholder, not the work itself. Before touching code, call TaskCreate to insert 2\u2013N concrete subtasks that describe what you'll actually build (file-by-file if useful). Each new task should be blocked by 'Sequence implementation' and should block 'QA'. Then mark this placeholder completed. Shipping without doing this means the work never appeared in the plan.", [3]),
25385
25385
  item(FEATURE_WORK_ID, 5, "QA", "Multi-pass review and fix until convergence. Parallel reviewers catch different issue classes: type holes, missed edge cases, race conditions, pattern drift. Use Shipyard's add_comment on diff hunks where reviewer judgment is useful. Keep running passes until no new issues surface.", [4]),
25386
25386
  item(FEATURE_WORK_ID, 6, "Ship", `Commit the work with a meaningful message, open a PR whose description explains the "why," push, and verify CI passes. Check pre-commit hooks and formatters run clean. Open the result in Shipyard's PR panel.`, [5]),
25387
25387
  item(FEATURE_WORK_ID, 7, "Tend PR", "Monitor CI checks and reviewer feedback in Shipyard's PR panel; fix failures and respond to each comment even if no code change is needed. Shepherd through merge.", [6])
@@ -25397,7 +25397,7 @@ var QUICK_TASK = {
25397
25397
  items: [
25398
25398
  item(QUICK_TASK_ID, 1, "Research the task", "Lightweight investigation: read the relevant files, understand the surrounding code, check for similar prior changes. Do not go deep unless the scope warrants it.", []),
25399
25399
  item(QUICK_TASK_ID, 2, "Sequence implementation", "Break into concrete subtasks with dependencies via TaskCreate and TaskUpdate's addBlockedBy. Even for small work, explicit steps help catch missed pieces.", [1]),
25400
- item(QUICK_TASK_ID, 3, "Implementation", "Replace this placeholder with the real subtasks via TaskCreate. Tasks blocked by (2), blocking (4). Delete this placeholder after inserting the real work.", [2]),
25400
+ item(QUICK_TASK_ID, 3, "Implementation \u2014 REPLACE this placeholder", "This is a placeholder, not the work itself. Call TaskCreate to insert the real subtasks describing what you'll actually build. Each new task should be blocked by 'Sequence implementation' and block 'QA'. Mark this placeholder completed once replaced.", [2]),
25401
25401
  item(QUICK_TASK_ID, 4, "QA", "Quick review pass: type safety, pattern consistency, obvious issues. Use add_comment on hunks where feedback is needed. Skip multi-pass convergence if the change is trivial.", [3]),
25402
25402
  item(QUICK_TASK_ID, 5, "Ship", "Commit, open PR, push, verify CI. Open the result in Shipyard's PR panel.", [4]),
25403
25403
  item(QUICK_TASK_ID, 6, "Tend PR", "Monitor and shepherd to merge via Shipyard's PR panel. Respond to reviewer comments promptly.", [5])
@@ -25426,7 +25426,7 @@ var PARALLEL_REFACTOR = {
25426
25426
  description: "Refactor work that can fan out into independent parallel groups. Map the scope, spawn threads, integrate, and ship as one coherent PR (or stacked PRs).",
25427
25427
  items: [
25428
25428
  item(PARALLEL_REFACTOR_ID, 1, "Map the scope", "Understand which files, patterns, and call sites the refactor touches. Sketch group boundaries on Shipyard's canvas so parallel workers see the same source of truth. Each group should be self-contained \u2014 no cross-group file modifications.", []),
25429
- item(PARALLEL_REFACTOR_ID, 2, "Parallel implementation", "Replace this placeholder with 2-4 independent groups via TaskCreate, each spawned as a Shipyard thread. Groups blocked by (1), all blocking (3).", [1]),
25429
+ item(PARALLEL_REFACTOR_ID, 2, "Parallel implementation \u2014 REPLACE with 2\u20134 parallel groups", "Call TaskCreate 2\u20134 times to insert independent parallel groups. Each group runs on its own thread via the Agent tool. Groups blocked by 'Map the scope', all block 'Integration pass'. Delete this placeholder after inserting the real work.", [1]),
25430
25430
  item(PARALLEL_REFACTOR_ID, 3, "Integration pass", "Resolve cross-group issues: type errors at interfaces, import ordering, pattern consistency. Rerun type checks across the whole codebase. Track any surfaced issues on Shipyard's canvas.", [2]),
25431
25431
  item(PARALLEL_REFACTOR_ID, 4, "QA", "Multi-pass review with add_comment annotations. Refactors have high regression risk; extra scrutiny on callers of changed signatures.", [3]),
25432
25432
  item(PARALLEL_REFACTOR_ID, 5, "Ship", "Commit per group or per theme. Stack if the refactor is large enough to warrant separate reviewable units. Open in Shipyard's PR panel.", [4]),
@@ -27288,7 +27288,7 @@ var TaskRecordSchema = external_exports.object({
27288
27288
  lastActivityAt: external_exports.number().default(0),
27289
27289
  appliedTemplateId: external_exports.string().optional()
27290
27290
  });
27291
- var TASK_STORE_VERSION = 9;
27291
+ var TASK_STORE_VERSION = 10;
27292
27292
  var TaskStoreSchema = external_exports.object({
27293
27293
  schemaVersion: external_exports.number(),
27294
27294
  tasks: external_exports.record(external_exports.string(), TaskRecordSchema)
@@ -27302,8 +27302,26 @@ function migrateTaskStore(raw) {
27302
27302
  if (version < 5) {
27303
27303
  return { schemaVersion: TASK_STORE_VERSION, tasks: {} };
27304
27304
  }
27305
+ if (version < 10) {
27306
+ backfillLastActivityAt(prop(raw, "tasks"));
27307
+ }
27305
27308
  return TaskStoreSchema.parse({ ...raw, schemaVersion: TASK_STORE_VERSION });
27306
27309
  }
27310
+ function backfillLastActivityAt(tasksRaw) {
27311
+ if (typeof tasksRaw !== "object" || tasksRaw === null)
27312
+ return;
27313
+ for (const record of Object.values(tasksRaw)) {
27314
+ if (typeof record !== "object" || record === null)
27315
+ continue;
27316
+ const existing = prop(record, "lastActivityAt");
27317
+ if (typeof existing === "number" && existing !== 0)
27318
+ continue;
27319
+ const updated = prop(record, "updatedAt");
27320
+ const created = prop(record, "createdAt");
27321
+ const fallback = (typeof updated === "number" ? updated : 0) || (typeof created === "number" ? created : 0) || 0;
27322
+ Object.assign(record, { lastActivityAt: fallback });
27323
+ }
27324
+ }
27307
27325
  var TemplateItemSchema = external_exports.object({
27308
27326
  id: external_exports.string(),
27309
27327
  content: external_exports.string(),
@@ -27656,7 +27674,8 @@ var PermissionRequestPayloadSchema = external_exports.object({
27656
27674
  description: external_exports.string().optional(),
27657
27675
  agentId: external_exports.string(),
27658
27676
  ownerDisplayName: external_exports.string().optional(),
27659
- threadId: external_exports.string().optional()
27677
+ threadId: external_exports.string().optional(),
27678
+ targetParticipantId: external_exports.string().optional()
27660
27679
  });
27661
27680
  var CapabilitiesPayloadSchema = external_exports.object({
27662
27681
  models: external_exports.array(external_exports.object({
@@ -27838,7 +27857,8 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
27838
27857
  channelId: external_exports.string(),
27839
27858
  title: external_exports.string(),
27840
27859
  cwd: external_exports.string(),
27841
- mode: external_exports.enum(taskModes).default("task")
27860
+ mode: external_exports.enum(taskModes).default("task"),
27861
+ templateId: external_exports.string().optional()
27842
27862
  }),
27843
27863
  external_exports.object({
27844
27864
  type: external_exports.literal("promote_task"),
@@ -28030,6 +28050,13 @@ var RateLimitInfoSchema = external_exports.object({
28030
28050
  });
28031
28051
  var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("type", [
28032
28052
  external_exports.object({ type: external_exports.literal("permission_request") }).merge(PermissionRequestPayloadSchema),
28053
+ external_exports.object({
28054
+ type: external_exports.literal("permission_resolved"),
28055
+ taskId: external_exports.string(),
28056
+ toolUseId: external_exports.string(),
28057
+ decision: PermissionDecisionSchema,
28058
+ threadId: external_exports.string().optional()
28059
+ }),
28033
28060
  external_exports.object({ type: external_exports.literal("capabilities"), capabilities: CapabilitiesPayloadSchema }),
28034
28061
  external_exports.object({
28035
28062
  type: external_exports.literal("rate_limit_info"),
@@ -31542,6 +31569,15 @@ function humanParticipantId(userId) {
31542
31569
  }
31543
31570
  function createPeerRoleRegistry() {
31544
31571
  const peers = /* @__PURE__ */ new Map();
31572
+ const listeners = /* @__PURE__ */ new Set();
31573
+ function notify(peerId) {
31574
+ for (const listener of listeners) {
31575
+ try {
31576
+ listener(peerId);
31577
+ } catch {
31578
+ }
31579
+ }
31580
+ }
31545
31581
  return {
31546
31582
  registerPersonalPeer(machineId, userId, displayName) {
31547
31583
  peers.set(machineId, {
@@ -31551,6 +31587,7 @@ function createPeerRoleRegistry() {
31551
31587
  displayName,
31552
31588
  taskId: null
31553
31589
  });
31590
+ notify(machineId);
31554
31591
  },
31555
31592
  registerCollabPeer(peerId, role, userId, displayName, taskId) {
31556
31593
  peers.set(peerId, {
@@ -31560,6 +31597,7 @@ function createPeerRoleRegistry() {
31560
31597
  displayName,
31561
31598
  taskId
31562
31599
  });
31600
+ notify(peerId);
31563
31601
  },
31564
31602
  unregisterPeer(peerId) {
31565
31603
  peers.delete(peerId);
@@ -31580,6 +31618,15 @@ function createPeerRoleRegistry() {
31580
31618
  const entry = peers.get(peerId);
31581
31619
  if (entry) return entry.participantId;
31582
31620
  return humanParticipantId("unknown");
31621
+ },
31622
+ isRegistered(peerId) {
31623
+ return peers.has(peerId);
31624
+ },
31625
+ onRegister(listener) {
31626
+ listeners.add(listener);
31627
+ return () => {
31628
+ listeners.delete(listener);
31629
+ };
31583
31630
  }
31584
31631
  };
31585
31632
  }
@@ -46954,6 +47001,14 @@ var VisualizationFileWatcher = class {
46954
47001
  content = "";
46955
47002
  }
46956
47003
  }
47004
+ this.#sendControlMessage?.({
47005
+ type: "viz_content",
47006
+ taskId: effect.taskId,
47007
+ slug: effect.slug,
47008
+ vizType: effect.vizType,
47009
+ title: effect.title,
47010
+ content
47011
+ });
46957
47012
  const data = { slug: effect.slug, title: effect.title };
46958
47013
  const nodeId = this.#deps.canvasRepo.createElement(effect.taskId, {
46959
47014
  type: effect.vizType,
@@ -46964,14 +47019,6 @@ var VisualizationFileWatcher = class {
46964
47019
  zIndex: position.zIndex,
46965
47020
  data
46966
47021
  });
46967
- this.#sendControlMessage?.({
46968
- type: "viz_content",
46969
- taskId: effect.taskId,
46970
- slug: effect.slug,
46971
- vizType: effect.vizType,
46972
- title: effect.title,
46973
- content
46974
- });
46975
47022
  const followUp = this.registry.setCanvasElementId(effect.slug, `${nodeId}`);
46976
47023
  await this.executeEffects(followUp);
46977
47024
  }
@@ -47165,6 +47212,142 @@ var VisualizationRegistry = class {
47165
47212
  }
47166
47213
  };
47167
47214
 
47215
+ // src/services/loro-awareness-sampler.ts
47216
+ var DEFAULT_SAMPLE_INTERVAL_MS = 6e4;
47217
+ function planAwarenessSnapshot(inputs) {
47218
+ const events = [];
47219
+ for (const doc3 of inputs.docs) {
47220
+ const ourVersionBytes = safeEncodeVV(doc3.currentVersion);
47221
+ if (ourVersionBytes === null) continue;
47222
+ const ourVersion = bytesToBase64(ourVersionBytes);
47223
+ for (const peer of inputs.peers) {
47224
+ const awareness = peer.docSyncStates.get(doc3.docId);
47225
+ if (!awareness) continue;
47226
+ if (awareness.status !== "synced") continue;
47227
+ const theirVersion = awareness.lastKnownVersion;
47228
+ const theirBytes = safeEncodeVV(theirVersion);
47229
+ const theirCachedVersion = theirBytes === null ? null : bytesToBase64(theirBytes);
47230
+ const matchStatus = computeMatchStatus(doc3.currentVersion, theirVersion);
47231
+ events.push({
47232
+ event: "loro_awareness_snapshot",
47233
+ docId: doc3.docId,
47234
+ ourVersion,
47235
+ peerId: String(peer.peerId),
47236
+ theirCachedVersion,
47237
+ matchStatus
47238
+ });
47239
+ }
47240
+ }
47241
+ return events;
47242
+ }
47243
+ function computeMatchStatus(ours, theirs) {
47244
+ try {
47245
+ const cmp2 = ours.compare(theirs);
47246
+ if (cmp2 === void 0) return "concurrent";
47247
+ if (cmp2 === 0) return "equal";
47248
+ if (cmp2 === 1) return "we-ahead";
47249
+ if (cmp2 === -1) return "they-ahead";
47250
+ return "unknown";
47251
+ } catch {
47252
+ return "unknown";
47253
+ }
47254
+ }
47255
+ function safeEncodeVV(vv) {
47256
+ try {
47257
+ return vv.encode();
47258
+ } catch {
47259
+ return null;
47260
+ }
47261
+ }
47262
+ function bytesToBase64(bytes) {
47263
+ return Buffer.from(bytes).toString("base64");
47264
+ }
47265
+ function createLoroAwarenessSampler(deps) {
47266
+ let timer = null;
47267
+ const intervalMs = deps.intervalMs ?? DEFAULT_SAMPLE_INTERVAL_MS;
47268
+ function sampleOnce() {
47269
+ try {
47270
+ const synchronizer = getSynchronizer(deps.getRepo());
47271
+ if (!synchronizer) return;
47272
+ const peers = gatherPeers(synchronizer);
47273
+ const docs = gatherDocs(synchronizer, deps.listTaskIds(), deps.buildDocIdsForTask, deps.log);
47274
+ const events = planAwarenessSnapshot({ peers, docs });
47275
+ const counts = { equal: 0, "we-ahead": 0, "they-ahead": 0, concurrent: 0, unknown: 0 };
47276
+ for (const event of events) {
47277
+ counts[event.matchStatus] += 1;
47278
+ if (event.matchStatus === "equal") continue;
47279
+ deps.log({ ...event });
47280
+ }
47281
+ deps.log({
47282
+ event: "loro_awareness_summary",
47283
+ docCount: docs.length,
47284
+ peerCount: peers.length,
47285
+ pairsChecked: events.length,
47286
+ equalCount: counts.equal,
47287
+ weAheadCount: counts["we-ahead"],
47288
+ theyAheadCount: counts["they-ahead"],
47289
+ concurrentCount: counts.concurrent,
47290
+ unknownCount: counts.unknown
47291
+ });
47292
+ } catch (err) {
47293
+ deps.log({
47294
+ event: "loro_awareness_sample_failed",
47295
+ error: err instanceof Error ? err.message : String(err)
47296
+ });
47297
+ }
47298
+ }
47299
+ return {
47300
+ start() {
47301
+ if (timer) return;
47302
+ timer = setInterval(sampleOnce, intervalMs);
47303
+ timer.unref?.();
47304
+ },
47305
+ stop() {
47306
+ if (timer) {
47307
+ clearInterval(timer);
47308
+ timer = null;
47309
+ }
47310
+ },
47311
+ sampleOnce
47312
+ };
47313
+ }
47314
+ function safeOplogVersion(doc3) {
47315
+ try {
47316
+ return doc3.oplogVersion();
47317
+ } catch {
47318
+ return null;
47319
+ }
47320
+ }
47321
+ function getSynchronizer(repo) {
47322
+ const sync = repo.synchronizer;
47323
+ if (sync !== null && sync !== void 0 && typeof sync === "object" && "getPeers" in sync && "getDocumentState" in sync && typeof sync.getPeers === "function" && typeof sync.getDocumentState === "function") {
47324
+ return sync;
47325
+ }
47326
+ return null;
47327
+ }
47328
+ function gatherPeers(synchronizer) {
47329
+ return synchronizer.getPeers().map((p2) => ({
47330
+ peerId: p2.identity.peerId,
47331
+ docSyncStates: p2.docSyncStates
47332
+ }));
47333
+ }
47334
+ function gatherDocs(synchronizer, taskIds, buildDocIdsForTask, log) {
47335
+ const docs = [];
47336
+ for (const taskId of taskIds) {
47337
+ for (const docId of buildDocIdsForTask(taskId)) {
47338
+ const docState = synchronizer.getDocumentState(docId);
47339
+ if (!docState) continue;
47340
+ const currentVersion = safeOplogVersion(docState.doc);
47341
+ if (!currentVersion) {
47342
+ log({ event: "loro_awareness_doc_skipped", docId, reason: "oplog_version_unavailable" });
47343
+ continue;
47344
+ }
47345
+ docs.push({ docId, currentVersion });
47346
+ }
47347
+ }
47348
+ return docs;
47349
+ }
47350
+
47168
47351
  // src/services/metrics/health-metrics.ts
47169
47352
  var HealthMetrics = class {
47170
47353
  #taskManager;
@@ -69481,7 +69664,7 @@ function classifyAbandoned(task, now, thresholdMs) {
69481
69664
  if (task.abandonedAt != null) return false;
69482
69665
  if (task.taskStartedAt == null) return false;
69483
69666
  if (TERMINAL_STATUSES.has(task.status)) return false;
69484
- const idleMs = now - (task.lastActivityAt || task.updatedAt);
69667
+ const idleMs = now - task.lastActivityAt;
69485
69668
  return idleMs >= thresholdMs;
69486
69669
  }
69487
69670
  async function runAbandonedSweep(deps) {
@@ -82066,6 +82249,7 @@ function createCommentEventInjector(deps) {
82066
82249
  }
82067
82250
 
82068
82251
  // src/services/channels/permission-handler.ts
82252
+ var RESOLVED_REPLAY_LRU_SIZE = 50;
82069
82253
  var HIGH_RISK_TOOLS = /* @__PURE__ */ new Set(["Write", "Bash", "Edit", "NotebookEdit"]);
82070
82254
  var LOW_RISK_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "TodoRead", "TodoWrite"]);
82071
82255
  function classifyToolRisk(toolName, _input) {
@@ -82076,13 +82260,15 @@ function classifyToolRisk(toolName, _input) {
82076
82260
  var PermissionHandler = class {
82077
82261
  #deps;
82078
82262
  #pendingPermissions = /* @__PURE__ */ new Map();
82263
+ /** LRU of recently-resolved permissions. Used for reconnect replay (#2164). */
82264
+ #recentlyResolved = [];
82079
82265
  constructor(deps) {
82080
82266
  this.#deps = deps;
82081
82267
  }
82082
82268
  get pendingCount() {
82083
82269
  return this.#pendingPermissions.size;
82084
82270
  }
82085
- handlePermissionResponse(toolUseId, decision, opts) {
82271
+ handlePermissionResponse(toolUseId, decision, opts, senderParticipantId) {
82086
82272
  const pending = this.#pendingPermissions.get(toolUseId);
82087
82273
  if (!pending) {
82088
82274
  this.#deps.log({
@@ -82092,6 +82278,20 @@ var PermissionHandler = class {
82092
82278
  });
82093
82279
  return;
82094
82280
  }
82281
+ if (pending.targetParticipantId !== void 0 && senderParticipantId !== void 0 && pending.targetParticipantId !== senderParticipantId) {
82282
+ this.#deps.log({
82283
+ event: "permission_response_wrong_target",
82284
+ taskId: this.#deps.taskId,
82285
+ toolUseId,
82286
+ target: pending.targetParticipantId,
82287
+ sender: senderParticipantId
82288
+ });
82289
+ this.#deps.getSendControlMessage()?.({
82290
+ type: "error",
82291
+ error: `permission_response from "${senderParticipantId}" rejected \u2014 target is "${pending.targetParticipantId}"`
82292
+ });
82293
+ return;
82294
+ }
82095
82295
  this.#pendingPermissions.delete(toolUseId);
82096
82296
  const durationMs = Date.now() - pending.requestedAt;
82097
82297
  this.#deps.log({
@@ -82108,6 +82308,19 @@ var PermissionHandler = class {
82108
82308
  durationMs,
82109
82309
  riskLevel: pending.riskLevel
82110
82310
  });
82311
+ const resolvedMsg = {
82312
+ type: "permission_resolved",
82313
+ taskId: this.#deps.taskId,
82314
+ toolUseId,
82315
+ decision,
82316
+ ...this.#deps.threadId !== void 0 && { threadId: this.#deps.threadId }
82317
+ };
82318
+ this.#deps.getSendControlMessage()?.(resolvedMsg);
82319
+ this.#rememberResolved({
82320
+ toolUseId,
82321
+ decision,
82322
+ ...this.#deps.threadId !== void 0 && { threadId: this.#deps.threadId }
82323
+ });
82111
82324
  switch (decision) {
82112
82325
  case "approved":
82113
82326
  pending.resolve({
@@ -82129,6 +82342,12 @@ var PermissionHandler = class {
82129
82342
  }
82130
82343
  }
82131
82344
  }
82345
+ #rememberResolved(entry) {
82346
+ this.#recentlyResolved.push(entry);
82347
+ if (this.#recentlyResolved.length > RESOLVED_REPLAY_LRU_SIZE) {
82348
+ this.#recentlyResolved.shift();
82349
+ }
82350
+ }
82132
82351
  /**
82133
82352
  * Creates a permission request promise for a non-plan tool use.
82134
82353
  * Returns a Promise<PermissionResult> that resolves when the browser
@@ -82161,6 +82380,9 @@ var PermissionHandler = class {
82161
82380
  },
82162
82381
  ...this.#deps.threadId !== void 0 && {
82163
82382
  threadId: this.#deps.threadId
82383
+ },
82384
+ ...this.#deps.ownerParticipantId !== void 0 && {
82385
+ targetParticipantId: this.#deps.ownerParticipantId
82164
82386
  }
82165
82387
  };
82166
82388
  this.#pendingPermissions.set(toolUseId, {
@@ -82169,7 +82391,10 @@ var PermissionHandler = class {
82169
82391
  requestedAt,
82170
82392
  toolName,
82171
82393
  riskLevel,
82172
- request
82394
+ request,
82395
+ ...this.#deps.ownerParticipantId !== void 0 && {
82396
+ targetParticipantId: this.#deps.ownerParticipantId
82397
+ }
82173
82398
  });
82174
82399
  const send = this.#deps.getSendControlMessage();
82175
82400
  if (send) {
@@ -82229,9 +82454,8 @@ var PermissionHandler = class {
82229
82454
  * store-writer dedup in apps/web/src/transport/control-channel/control-channel-store-writers.ts.
82230
82455
  */
82231
82456
  replayPendingToChannel(send) {
82232
- if (this.#pendingPermissions.size === 0) return;
82233
- let sent = 0;
82234
82457
  const currentMainAgentId = this.#deps.getAgentParticipantId();
82458
+ let sent = 0;
82235
82459
  for (const [toolUseId, pending] of this.#pendingPermissions) {
82236
82460
  const request = pending.request.agentId === PENDING_AGENT_PARTICIPANT_ID ? { ...pending.request, agentId: currentMainAgentId } : pending.request;
82237
82461
  try {
@@ -82246,11 +82470,33 @@ var PermissionHandler = class {
82246
82470
  });
82247
82471
  }
82248
82472
  }
82473
+ let resolvedSent = 0;
82474
+ for (const entry of this.#recentlyResolved) {
82475
+ try {
82476
+ send({
82477
+ type: "permission_resolved",
82478
+ taskId: this.#deps.taskId,
82479
+ toolUseId: entry.toolUseId,
82480
+ decision: entry.decision,
82481
+ ...entry.threadId !== void 0 && { threadId: entry.threadId }
82482
+ });
82483
+ resolvedSent++;
82484
+ } catch (err) {
82485
+ this.#deps.log({
82486
+ event: "permission_resolved_replay_send_failed",
82487
+ taskId: this.#deps.taskId,
82488
+ toolUseId: entry.toolUseId,
82489
+ error: err instanceof Error ? err.message : String(err)
82490
+ });
82491
+ }
82492
+ }
82249
82493
  this.#deps.log({
82250
82494
  event: "permission_replay",
82251
82495
  taskId: this.#deps.taskId,
82252
82496
  sent,
82253
- total: this.#pendingPermissions.size
82497
+ total: this.#pendingPermissions.size,
82498
+ resolvedSent,
82499
+ resolvedTotal: this.#recentlyResolved.length
82254
82500
  });
82255
82501
  }
82256
82502
  flushQueuedMessages(send, queue) {
@@ -83213,6 +83459,7 @@ var Thread = class {
83213
83459
  taskId: config2.permissionTaskId ?? config2.threadId,
83214
83460
  threadId: config2.permissionThreadId,
83215
83461
  ownerDisplayName: config2.ownerDisplayName,
83462
+ ownerParticipantId: config2.ownerParticipantId,
83216
83463
  getAgentParticipantId: () => this.#agentParticipantId,
83217
83464
  getSubprocess: () => this.#subprocess,
83218
83465
  getSendControlMessage: () => this.#sendControlMessage,
@@ -83400,8 +83647,13 @@ var Thread = class {
83400
83647
  this.#streamDeltaSinks.delete(sink);
83401
83648
  };
83402
83649
  }
83403
- handlePermissionResponse(toolUseId, decision, opts) {
83404
- this.#permissionHandler.handlePermissionResponse(toolUseId, decision, opts);
83650
+ handlePermissionResponse(toolUseId, decision, opts, senderParticipantId) {
83651
+ this.#permissionHandler.handlePermissionResponse(
83652
+ toolUseId,
83653
+ decision,
83654
+ opts,
83655
+ senderParticipantId
83656
+ );
83405
83657
  }
83406
83658
  /**
83407
83659
  * Wire the canUseTool callback on an adopted (pre-warmed) subprocess.
@@ -85169,12 +85421,15 @@ function buildPatchMessage(uri, current2, name, patch, now) {
85169
85421
  resolvedAt: now,
85170
85422
  patchContent: patch
85171
85423
  };
85424
+ const patchText = uri.startsWith("shipyard://task/") ? `# Task list updated (append-only). Apply this patch to reconstruct current state:
85425
+
85426
+ ${patch}` : patch;
85172
85427
  return {
85173
85428
  kind: "synthetic_update",
85174
85429
  content: [
85175
85430
  {
85176
85431
  type: "resource",
85177
- resource: { ...current2, text: patch },
85432
+ resource: { ...current2, text: patchText },
85178
85433
  _meta: toMetaRecord(meta)
85179
85434
  }
85180
85435
  ],
@@ -85257,9 +85512,21 @@ function parseTaskResourceUri(uri) {
85257
85512
  }
85258
85513
  return taskId;
85259
85514
  }
85260
- var TASK_LIST_PREAMBLE = `This is your live task list, managed via the TaskCreate, TaskUpdate, TaskList, and TaskGet tools. Mark tasks in_progress when you start them and completed when you finish. Use TaskCreate to add tasks that emerge during the work. Wire dependencies via TaskUpdate's addBlockedBy / addBlocks. The list auto-refreshes as you update it.
85515
+ var TASK_LIST_PREAMBLE = `This is your live task list, managed via the TaskCreate, TaskUpdate, TaskList, TaskGet tools. Mark in_progress when you start, completed when you finish.
85516
+
85517
+ **Append-only.** Tasks once created stay in the list \u2014 status and dependencies can be updated, but items are not removed. New tasks are added via TaskCreate.
85518
+
85519
+ When you see an update diff to this resource, treat it as a state refresh: re-read and adjust your plans if a task was inserted, a status changed, or a dependency edge shifted.
85261
85520
 
85262
- This is a suggested workflow, not a rigid script. Merge, split, reorder, or skip steps as the situation demands. Use whatever skills and tools you actually have available \u2014 the goal is the outcome, not following the template.`;
85521
+ This is a suggested workflow, not a rigid script. Merge, split, reorder, or skip steps as the situation demands.`;
85522
+ var SKILL_EXAMPLES_BLOCK = `Some of your steps may map well to skills you have available. Examples:
85523
+
85524
+ - A "research" or "investigate" step might map to a research-style skill if you have one (e.g. /deep-research).
85525
+ - A "ship" or "open PR" step might map to a shipping skill (e.g. /ship).
85526
+ - A "review" or "QA" step might map to a review skill (e.g. /qa).
85527
+
85528
+ Use whatever's actually available. If no skill fits a step, leave it unprefixed.`;
85529
+ var BEFORE_YOU_START_BLOCK = `**Before you start:** Look at your available skills. For each item in this list that clearly maps to a skill you have, call TaskUpdate to prefix its title with that slash-command (e.g. a "research" step might become "/deep-research Research the task" if /deep-research is available). Items with no clear skill fit stay unprefixed \u2014 do not force a match. Placeholder items are still replaced per their description. If a step already carries a slash-command prefix, skip it \u2014 this is idempotent.`;
85263
85530
  function buildBlocksMap(entries) {
85264
85531
  const blocksMap = /* @__PURE__ */ new Map();
85265
85532
  for (const task of entries) {
@@ -85306,6 +85573,12 @@ function formatForAgent(ctx) {
85306
85573
  }
85307
85574
  lines.push(TASK_LIST_PREAMBLE);
85308
85575
  lines.push("");
85576
+ if (appliedTemplate) {
85577
+ lines.push(SKILL_EXAMPLES_BLOCK);
85578
+ lines.push("");
85579
+ lines.push(BEFORE_YOU_START_BLOCK);
85580
+ lines.push("");
85581
+ }
85309
85582
  if (entries.length === 0) {
85310
85583
  lines.push("No tasks yet \u2014 create some with TaskCreate as the work emerges.");
85311
85584
  } else {
@@ -86373,6 +86646,7 @@ var SideThreadRegistry = class {
86373
86646
  forkPointMessageUuid: params.forkPointMessageUuid
86374
86647
  } : { kind: "fresh" },
86375
86648
  humanParticipantId: params.humanParticipantId,
86649
+ ownerParticipantId: this.#deps.ownerParticipantId,
86376
86650
  permissionTaskId: this.#deps.taskId,
86377
86651
  permissionThreadId: params.threadId,
86378
86652
  sendControlMessage: params.sendControlMessage,
@@ -88623,6 +88897,8 @@ var Task = class {
88623
88897
  #broadcastToAllPeers;
88624
88898
  #permissionQueue = [];
88625
88899
  #disposed = false;
88900
+ /** One-shot: awaited before the first #handleBeforeSpawn resolves. Cleared after use. */
88901
+ #hydrationPromise;
88626
88902
  /** Guard: prevents re-entry when stop() triggers further status callbacks. */
88627
88903
  #scheduledAutoCompleted = false;
88628
88904
  #explicitlyStopped = false;
@@ -88672,6 +88948,7 @@ var Task = class {
88672
88948
  this.#costBaseline = deps.costBaseline;
88673
88949
  this.#lastTurnStats = deps.initialTurnStats ?? null;
88674
88950
  this.#tokenCountResult = deps.initialTokenCount ?? null;
88951
+ this.#hydrationPromise = deps.hydrationPromise ?? null;
88675
88952
  this.#telemetry = new TaskTelemetry({
88676
88953
  taskId: deps.taskId,
88677
88954
  metricsCollector: deps.metricsCollector,
@@ -88886,6 +89163,7 @@ var Task = class {
88886
89163
  spawnMode,
88887
89164
  humanParticipantId: deps.humanParticipantId,
88888
89165
  ownerDisplayName: deps.ownerDisplayName,
89166
+ ownerParticipantId: deps.ownerParticipantId,
88889
89167
  permissionTaskId: deps.taskId,
88890
89168
  permissionThreadId: void 0,
88891
89169
  sendControlMessage: deps.sendControlMessage,
@@ -88944,6 +89222,7 @@ var Task = class {
88944
89222
  this.#sideThreads = new SideThreadRegistry({
88945
89223
  taskId: deps.taskId,
88946
89224
  dataDir: deps.dataDir,
89225
+ ownerParticipantId: deps.ownerParticipantId,
88947
89226
  store: deps.store,
88948
89227
  sessionPersistence: deps.sessionPersistence,
88949
89228
  spawnSubprocess: wrappedSpawn,
@@ -89235,8 +89514,8 @@ var Task = class {
89235
89514
  addStreamDeltaSink(sink) {
89236
89515
  return this.#mainThread.addStreamDeltaSink(sink);
89237
89516
  }
89238
- handlePermissionResponse(toolUseId, decision, opts) {
89239
- this.#mainThread.handlePermissionResponse(toolUseId, decision, opts);
89517
+ handlePermissionResponse(toolUseId, decision, opts, senderParticipantId) {
89518
+ this.#mainThread.handlePermissionResponse(toolUseId, decision, opts, senderParticipantId);
89240
89519
  }
89241
89520
  handlePlanContinue(toolUseId, decision, feedback, opts) {
89242
89521
  this.#planHandler.handlePlanContinue(toolUseId, decision, feedback, opts);
@@ -89594,6 +89873,10 @@ var Task = class {
89594
89873
  return arr.shift();
89595
89874
  }
89596
89875
  async #handleBeforeSpawn(initialContent) {
89876
+ if (this.#hydrationPromise) {
89877
+ await this.#hydrationPromise;
89878
+ this.#hydrationPromise = null;
89879
+ }
89597
89880
  await this.#ensureCompactContextFromStore();
89598
89881
  if (this.#needsPromotionContext) {
89599
89882
  this.#needsPromotionContext = false;
@@ -90868,7 +91151,7 @@ var TaskManager = class {
90868
91151
  const task = this.#tasks.get(taskId);
90869
91152
  return task?.orchestrator.latestSettings.permissionMode ?? null;
90870
91153
  }
90871
- handlePermissionResponse(taskId, toolUseId, decision, opts, threadId) {
91154
+ handlePermissionResponse(taskId, toolUseId, decision, opts, threadId, senderParticipantId) {
90872
91155
  const task = this.#tasks.get(taskId);
90873
91156
  if (!task) {
90874
91157
  this.#deps.log({ event: "permission_response_no_orchestrator", taskId, toolUseId });
@@ -90877,7 +91160,7 @@ var TaskManager = class {
90877
91160
  if (threadId) {
90878
91161
  const thread = task.orchestrator.getSideThread(threadId);
90879
91162
  if (thread) {
90880
- thread.handlePermissionResponse(toolUseId, decision, opts);
91163
+ thread.handlePermissionResponse(toolUseId, decision, opts, senderParticipantId);
90881
91164
  return;
90882
91165
  }
90883
91166
  this.#deps.log({
@@ -90887,7 +91170,7 @@ var TaskManager = class {
90887
91170
  toolUseId
90888
91171
  });
90889
91172
  }
90890
- task.orchestrator.handlePermissionResponse(toolUseId, decision, opts);
91173
+ task.orchestrator.handlePermissionResponse(toolUseId, decision, opts, senderParticipantId);
90891
91174
  }
90892
91175
  /**
90893
91176
  * Request permission for a proxy MCP tool call.
@@ -90914,7 +91197,38 @@ var TaskManager = class {
90914
91197
  if (this.#tasks.has(params.taskId)) return;
90915
91198
  const cwd = params.cwd;
90916
91199
  const mode = params.mode ?? "task";
90917
- this.#deps.taskStateStore.createTask({ ...params, cwd, mode }).catch((err) => {
91200
+ const storeAndHydrate = async () => {
91201
+ const template = params.templateId ? await this.#deps.templateStore.get(params.templateId) : null;
91202
+ if (params.templateId && !template) {
91203
+ this.#deps.log({
91204
+ event: "task_template_not_found",
91205
+ taskId: params.taskId,
91206
+ templateId: params.templateId
91207
+ });
91208
+ }
91209
+ const now = Date.now();
91210
+ const initialOverlay = template?.items.map((item2) => ({
91211
+ id: item2.id,
91212
+ subject: item2.content,
91213
+ description: item2.description,
91214
+ status: "pending",
91215
+ blocks: [],
91216
+ blockedBy: item2.deps,
91217
+ createdAt: now,
91218
+ updatedAt: now
91219
+ })) ?? [];
91220
+ await this.#deps.taskStateStore.createTask({
91221
+ ...params,
91222
+ cwd,
91223
+ mode,
91224
+ appliedTemplateId: params.templateId,
91225
+ initialOverlay: initialOverlay.length > 0 ? { ...DEFAULT_TASK_OVERLAY, userTasks: initialOverlay } : void 0
91226
+ });
91227
+ if (initialOverlay.length > 0) {
91228
+ this.#deps.notifyTaskResourceChange(params.taskId);
91229
+ }
91230
+ };
91231
+ const hydrationPromise = storeAndHydrate().catch((err) => {
90918
91232
  this.#deps.log({
90919
91233
  event: "task_state_store_create_failed",
90920
91234
  taskId: params.taskId,
@@ -90929,7 +91243,8 @@ var TaskManager = class {
90929
91243
  adoptedSubprocess: claim ?? void 0,
90930
91244
  cwd,
90931
91245
  mode,
90932
- scheduleId: params.scheduleId
91246
+ scheduleId: params.scheduleId,
91247
+ hydrationPromise
90933
91248
  });
90934
91249
  this.#tasks.set(params.taskId, {
90935
91250
  taskId: params.taskId,
@@ -91264,6 +91579,7 @@ var TaskManager = class {
91264
91579
  channelId,
91265
91580
  humanParticipantId: buildHumanParticipantId(this.#deps.userId),
91266
91581
  ownerDisplayName: this.#deps.displayName,
91582
+ ownerParticipantId: buildHumanParticipantId(this.#deps.userId),
91267
91583
  getCollabParticipants: () => this.#deps.getCollabParticipantsForTask(taskId),
91268
91584
  epoch: this.#deps.epoch,
91269
91585
  store: this.#deps.store,
@@ -91419,7 +91735,8 @@ var TaskManager = class {
91419
91735
  adoptedSubprocess: opts?.adoptedSubprocess,
91420
91736
  mode: opts?.mode ?? "task",
91421
91737
  scheduleId: opts?.scheduleId,
91422
- collabQueuePersistence: this.#deps.collabQueuePersistence
91738
+ collabQueuePersistence: this.#deps.collabQueuePersistence,
91739
+ hydrationPromise: opts?.hydrationPromise
91423
91740
  });
91424
91741
  }
91425
91742
  };
@@ -91489,7 +91806,17 @@ function buildTaskStateStore(dataDir) {
91489
91806
  get version() {
91490
91807
  return _version;
91491
91808
  },
91492
- async createTask({ taskId, channelId, title, cwd, mode, scheduleId, scheduleName }, options) {
91809
+ async createTask({
91810
+ taskId,
91811
+ channelId,
91812
+ title,
91813
+ cwd,
91814
+ mode,
91815
+ scheduleId,
91816
+ scheduleName,
91817
+ appliedTemplateId,
91818
+ initialOverlay
91819
+ }, options) {
91493
91820
  const now = Date.now();
91494
91821
  pushBroadcast(options);
91495
91822
  await store.set(taskId, {
@@ -91509,7 +91836,9 @@ function buildTaskStateStore(dataDir) {
91509
91836
  totalOutputTokens: 0,
91510
91837
  mode: mode ?? "task",
91511
91838
  ...scheduleId ? { scheduleId } : {},
91512
- ...scheduleName ? { scheduleName } : {}
91839
+ ...scheduleName ? { scheduleName } : {},
91840
+ ...appliedTemplateId ? { appliedTemplateId } : {},
91841
+ ...initialOverlay ? { taskOverlay: initialOverlay } : {}
91513
91842
  });
91514
91843
  },
91515
91844
  async updateTaskStatus(taskId, status, options) {
@@ -91524,10 +91853,20 @@ function buildTaskStateStore(dataDir) {
91524
91853
  );
91525
91854
  },
91526
91855
  async updateCwd(taskId, cwd, options) {
91527
- await safeUpdate(taskId, (task) => ({ ...task, cwd, updatedAt: Date.now() }), options);
91856
+ const now = Date.now();
91857
+ await safeUpdate(
91858
+ taskId,
91859
+ (task) => ({ ...task, cwd, updatedAt: now, lastActivityAt: now }),
91860
+ options
91861
+ );
91528
91862
  },
91529
91863
  async updateMode(taskId, mode, options) {
91530
- await safeUpdate(taskId, (task) => ({ ...task, mode, updatedAt: Date.now() }), options);
91864
+ const now = Date.now();
91865
+ await safeUpdate(
91866
+ taskId,
91867
+ (task) => ({ ...task, mode, updatedAt: now, lastActivityAt: now }),
91868
+ options
91869
+ );
91531
91870
  },
91532
91871
  async updateTodoProgress(taskId, progress, options) {
91533
91872
  const now = Date.now();
@@ -91578,8 +91917,7 @@ function buildTaskStateStore(dataDir) {
91578
91917
  taskId,
91579
91918
  (task) => ({
91580
91919
  ...task,
91581
- taskOverlay: overlay,
91582
- updatedAt: Date.now()
91920
+ taskOverlay: overlay
91583
91921
  }),
91584
91922
  options
91585
91923
  );
@@ -91589,8 +91927,7 @@ function buildTaskStateStore(dataDir) {
91589
91927
  taskId,
91590
91928
  (task) => ({
91591
91929
  ...task,
91592
- composerSettings: { ...task.composerSettings, ...settings },
91593
- updatedAt: Date.now()
91930
+ composerSettings: { ...task.composerSettings, ...settings }
91594
91931
  }),
91595
91932
  options
91596
91933
  );
@@ -91601,8 +91938,7 @@ function buildTaskStateStore(dataDir) {
91601
91938
  (task) => ({
91602
91939
  ...task,
91603
91940
  totalCostUsd: stats.totalCostUsd,
91604
- totalOutputTokens: stats.totalOutputTokens,
91605
- updatedAt: Date.now()
91941
+ totalOutputTokens: stats.totalOutputTokens
91606
91942
  }),
91607
91943
  options
91608
91944
  );
@@ -91612,8 +91948,7 @@ function buildTaskStateStore(dataDir) {
91612
91948
  taskId,
91613
91949
  (task) => ({
91614
91950
  ...task,
91615
- lastTurnStats: snapshot,
91616
- updatedAt: Date.now()
91951
+ lastTurnStats: snapshot
91617
91952
  }),
91618
91953
  options
91619
91954
  );
@@ -91623,8 +91958,7 @@ function buildTaskStateStore(dataDir) {
91623
91958
  taskId,
91624
91959
  (task) => ({
91625
91960
  ...task,
91626
- lastTokenCount: snapshot,
91627
- updatedAt: Date.now()
91961
+ lastTokenCount: snapshot
91628
91962
  }),
91629
91963
  options
91630
91964
  );
@@ -91634,8 +91968,7 @@ function buildTaskStateStore(dataDir) {
91634
91968
  taskId,
91635
91969
  (task) => ({
91636
91970
  ...task,
91637
- lastPlanDetection: detection ?? void 0,
91638
- updatedAt: Date.now()
91971
+ lastPlanDetection: detection ?? void 0
91639
91972
  }),
91640
91973
  options
91641
91974
  );
@@ -91648,8 +91981,7 @@ function buildTaskStateStore(dataDir) {
91648
91981
  totalCostUsd: 0,
91649
91982
  totalOutputTokens: 0,
91650
91983
  lastTurnStats: void 0,
91651
- lastTokenCount: void 0,
91652
- updatedAt: Date.now()
91984
+ lastTokenCount: void 0
91653
91985
  }),
91654
91986
  options
91655
91987
  );
@@ -91657,21 +91989,21 @@ function buildTaskStateStore(dataDir) {
91657
91989
  async setPrUrl(taskId, prUrl, options) {
91658
91990
  await safeUpdate(
91659
91991
  taskId,
91660
- (task) => task.prUrl === prUrl ? task : { ...task, prUrl, updatedAt: Date.now() },
91992
+ (task) => task.prUrl === prUrl ? task : { ...task, prUrl },
91661
91993
  options
91662
91994
  );
91663
91995
  },
91664
91996
  async setAppliedTemplateId(taskId, templateId, options) {
91665
91997
  await safeUpdate(
91666
91998
  taskId,
91667
- (task) => task.appliedTemplateId === templateId ? task : { ...task, appliedTemplateId: templateId, updatedAt: Date.now() },
91999
+ (task) => task.appliedTemplateId === templateId ? task : { ...task, appliedTemplateId: templateId },
91668
92000
  options
91669
92001
  );
91670
92002
  },
91671
92003
  async setAbandonedAt(taskId, timestamp, options) {
91672
92004
  await safeUpdate(
91673
92005
  taskId,
91674
- (task) => task.abandonedAt != null ? task : { ...task, abandonedAt: timestamp, updatedAt: Date.now() },
92006
+ (task) => task.abandonedAt != null ? task : { ...task, abandonedAt: timestamp },
91675
92007
  { broadcast: false, ...options }
91676
92008
  );
91677
92009
  },
@@ -93497,6 +93829,7 @@ async function createDaemon(deps) {
93497
93829
  });
93498
93830
  shipyardResolver.addSubResolver("task", taskResourceResolver);
93499
93831
  const collabParticipantsRef = { current: () => [] };
93832
+ const templateStoreEarly = buildTemplateStore(deps.dataDir);
93500
93833
  const taskManager = new TaskManager({
93501
93834
  userId: deps.auth.userId,
93502
93835
  displayName: deps.auth.displayName,
@@ -93524,7 +93857,8 @@ async function createDaemon(deps) {
93524
93857
  gitCheckpoint,
93525
93858
  workspaceRoot: deps.workspaceRoot,
93526
93859
  notifyTaskResourceChange: (taskId) => taskResourceResolver.notifyChange(taskId),
93527
- collabQueuePersistence
93860
+ collabQueuePersistence,
93861
+ templateStore: templateStoreEarly
93528
93862
  });
93529
93863
  const proxyRef = { current: null };
93530
93864
  const preWarmManager = new PreWarmManager({
@@ -93544,7 +93878,7 @@ async function createDaemon(deps) {
93544
93878
  logger.error({ err, ...ctx }, "rate-limit store write failed");
93545
93879
  }
93546
93880
  });
93547
- const templateStore = buildTemplateStore(deps.dataDir);
93881
+ const templateStore = templateStoreEarly;
93548
93882
  {
93549
93883
  const settings = await userSettingsStore.getSettings();
93550
93884
  await seedBuiltInTemplates(
@@ -93794,7 +94128,18 @@ async function createDaemon(deps) {
93794
94128
  log: (entry) => deps.log(entry)
93795
94129
  });
93796
94130
  abandonedSweeper.start();
94131
+ const awarenessSampler = createLoroAwarenessSampler({
94132
+ getRepo: () => repo,
94133
+ listTaskIds: () => taskManager.listManagedTasks().map((t) => t.taskId),
94134
+ buildDocIdsForTask: (taskId) => [
94135
+ buildPlanDocId(taskId, PLAN_EPOCH),
94136
+ buildCanvasDocId(taskId, CANVAS_EPOCH)
94137
+ ],
94138
+ log: deps.log
94139
+ });
94140
+ awarenessSampler.start();
93797
94141
  async function dispose() {
94142
+ awarenessSampler.stop();
93798
94143
  abandonedSweeper.stop();
93799
94144
  scheduleEvaluator.dispose();
93800
94145
  preWarmManager.dispose();
@@ -93845,6 +94190,7 @@ async function createDaemon(deps) {
93845
94190
  rateLimitStore,
93846
94191
  templateStore,
93847
94192
  themeStore,
94193
+ notifyTaskResourceChange: (taskId) => taskResourceResolver.notifyChange(taskId),
93848
94194
  setCollabParticipantsProvider: (provider) => {
93849
94195
  collabParticipantsRef.current = provider;
93850
94196
  },
@@ -94186,6 +94532,7 @@ function filterOutboundForCollab(msg, collabTaskId) {
94186
94532
  case "task_state_update":
94187
94533
  case "task_removed":
94188
94534
  case "permission_request":
94535
+ case "permission_resolved":
94189
94536
  case "pr_action_result":
94190
94537
  case "plan_detected":
94191
94538
  case "plan_continuation_timeout":
@@ -94514,7 +94861,14 @@ function routeMessage(msg, callbacks, log) {
94514
94861
  callbacks.onRemoveWorktree(msg.worktreePath);
94515
94862
  break;
94516
94863
  case "create_task":
94517
- callbacks.onCreateTask(msg.taskId, msg.channelId, msg.title, msg.cwd, msg.mode);
94864
+ callbacks.onCreateTask(
94865
+ msg.taskId,
94866
+ msg.channelId,
94867
+ msg.title,
94868
+ msg.cwd,
94869
+ msg.mode,
94870
+ msg.templateId
94871
+ );
94518
94872
  break;
94519
94873
  case "promote_task":
94520
94874
  callbacks.onPromoteTask(msg.taskId);
@@ -96157,7 +96511,15 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
96157
96511
  guardedSend,
96158
96512
  {
96159
96513
  onPermissionResponse: (taskId, toolUseId, decision, opts, threadId) => {
96160
- daemon.taskManager.handlePermissionResponse(taskId, toolUseId, decision, opts, threadId);
96514
+ const senderParticipantId = deps?.peerRoleRegistry && deps?.peerMachineId ? deps.peerRoleRegistry.getParticipantId(deps.peerMachineId) : void 0;
96515
+ daemon.taskManager.handlePermissionResponse(
96516
+ taskId,
96517
+ toolUseId,
96518
+ decision,
96519
+ opts,
96520
+ threadId,
96521
+ senderParticipantId
96522
+ );
96161
96523
  },
96162
96524
  onUpdateSettings: (settings) => {
96163
96525
  if (settings.disabledMcpServers && daemon.capabilities.mcpServers) {
@@ -96189,9 +96551,26 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
96189
96551
  logAdapter({ event: "settings_updated", settings });
96190
96552
  },
96191
96553
  onUpdateTaskSettings: (taskId, settings) => {
96192
- daemon.taskManager.applyTaskSettings(taskId, settings);
96193
- handler.sendControl({ type: "settings_ack", settings, taskId });
96194
- logAdapter({ event: "task_settings_updated", taskId, settings });
96554
+ const enforcedRole = deps?.peerRoleRegistry && deps?.peerMachineId ? deps.peerRoleRegistry.getRole(deps.peerMachineId) : deps?.peerRole;
96555
+ let applied = settings;
96556
+ if (enforcedRole !== void 0 && enforcedRole !== "owner" && settings.permissionMode) {
96557
+ const { permissionMode: _dropped, ...rest } = settings;
96558
+ applied = rest;
96559
+ logAdapter({
96560
+ event: "task_settings_permission_mode_rejected",
96561
+ taskId,
96562
+ role: enforcedRole,
96563
+ requested: settings.permissionMode
96564
+ });
96565
+ handler.sendControl({
96566
+ type: "error",
96567
+ error: "Only the task owner can change permissionMode"
96568
+ });
96569
+ }
96570
+ if (Object.keys(applied).length === 0) return;
96571
+ daemon.taskManager.applyTaskSettings(taskId, applied);
96572
+ handler.sendControl({ type: "settings_ack", settings: applied, taskId });
96573
+ logAdapter({ event: "task_settings_updated", taskId, settings: applied });
96195
96574
  },
96196
96575
  onRequestCapabilities: () => {
96197
96576
  sendCapabilities(handler);
@@ -96601,8 +96980,8 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
96601
96980
  const threads = await daemon.taskManager.listThreads(taskId);
96602
96981
  controlHandler.sendControl({ type: "thread_list", taskId, threads });
96603
96982
  },
96604
- onCreateTask: (taskId, channelId, title, cwd, mode) => {
96605
- daemon.taskManager.createTask({ taskId, channelId, title, cwd, mode });
96983
+ onCreateTask: (taskId, channelId, title, cwd, mode, templateId) => {
96984
+ daemon.taskManager.createTask({ taskId, channelId, title, cwd, mode, templateId });
96606
96985
  },
96607
96986
  onPromoteTask: (taskId) => {
96608
96987
  daemon.taskManager.promoteTask(taskId);
@@ -96736,7 +97115,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
96736
97115
  });
96737
97116
  },
96738
97117
  onNotifyTemplateApplied: (taskId, templateId) => {
96739
- daemon.taskStateStore.setAppliedTemplateId(taskId, templateId).catch((err) => {
97118
+ daemon.taskStateStore.setAppliedTemplateId(taskId, templateId).then(() => daemon.notifyTaskResourceChange(taskId)).catch((err) => {
96740
97119
  logAdapter({
96741
97120
  event: "notify_template_applied_failed",
96742
97121
  taskId,
@@ -99907,73 +100286,137 @@ function buildCollabRoomManager(deps) {
99907
100286
  log.info("Collab browser preview URL channel wired");
99908
100287
  },
99909
100288
  onFileIOChannel: (_machineId, rawChannel) => {
99910
- const peerRole = peerRoleRegistry.getRole(_machineId);
99911
- if (peerRole !== "owner" && peerRole !== "collaborator-full") {
99912
- log.warn(
99913
- { role: peerRole },
99914
- "Collab peer with insufficient role attempted file I/O access"
99915
- );
100289
+ const wireFileIo = () => {
100290
+ const channelState = narrow(rawChannel).readyState;
100291
+ if (channelState === "closed" || channelState === "closing") {
100292
+ log.info("Collab file I/O raw channel closed before peer registered \u2014 skipping wire");
100293
+ return;
100294
+ }
100295
+ const peerRole = peerRoleRegistry.getRole(_machineId);
100296
+ if (peerRole !== "owner" && peerRole !== "collaborator-full") {
100297
+ log.warn(
100298
+ { role: peerRole },
100299
+ "Collab peer with insufficient role attempted file I/O access"
100300
+ );
100301
+ return;
100302
+ }
100303
+ const dc = narrow(rawChannel);
100304
+ const cwd = daemon.taskManager.getTaskCwd(collabTaskId) ?? workspaceRoot;
100305
+ setupFileIOChannel({
100306
+ dc,
100307
+ cwd,
100308
+ label: "file-io-collab",
100309
+ logAdapter,
100310
+ fileWatcherPool,
100311
+ fileIOHandlers,
100312
+ handlerDeps: {
100313
+ diffTurn: (taskId, turnIndex) => daemon.taskManager.diffTurn(taskId, turnIndex),
100314
+ diffTurnFile: (taskId, turnIndex, path2) => daemon.taskManager.diffTurnFile(taskId, turnIndex, path2),
100315
+ revertTurn: (taskId, turnIndex, mode, filePath) => daemon.taskManager.revertTurn(taskId, turnIndex, mode, filePath),
100316
+ /**
100317
+ * Scope the collab peer's `set_cwd` allowlist to THIS task's
100318
+ * cwd only — not the global set. Without this, a collaborator-full
100319
+ * peer for task X could redirect to task Y's cwd and cross
100320
+ * task-isolation boundaries.
100321
+ */
100322
+ isAllowedCwd: (abs) => {
100323
+ const taskCwd = daemon.taskManager.getTaskCwd(collabTaskId);
100324
+ if (!taskCwd) return false;
100325
+ return isUnderAllowedRoot2(abs, [taskCwd]);
100326
+ }
100327
+ }
100328
+ });
100329
+ log.info("Collab file I/O channel wired");
100330
+ };
100331
+ if (peerRoleRegistry.isRegistered(_machineId)) {
100332
+ wireFileIo();
99916
100333
  return;
99917
100334
  }
99918
- const dc = narrow(rawChannel);
99919
- const cwd = daemon.taskManager.getTaskCwd(collabTaskId) ?? workspaceRoot;
99920
- setupFileIOChannel({
99921
- dc,
99922
- cwd,
99923
- label: "file-io-collab",
99924
- logAdapter,
99925
- fileWatcherPool,
99926
- fileIOHandlers,
99927
- handlerDeps: {
99928
- diffTurn: (taskId, turnIndex) => daemon.taskManager.diffTurn(taskId, turnIndex),
99929
- diffTurnFile: (taskId, turnIndex, path2) => daemon.taskManager.diffTurnFile(taskId, turnIndex, path2),
99930
- revertTurn: (taskId, turnIndex, mode, filePath) => daemon.taskManager.revertTurn(taskId, turnIndex, mode, filePath),
99931
- /**
99932
- * Scope the collab peer's `set_cwd` allowlist to THIS task's
99933
- * cwd only — not the global set. Without this, a collaborator-full
99934
- * peer for task X could redirect to task Y's cwd and cross
99935
- * task-isolation boundaries.
99936
- */
99937
- isAllowedCwd: (abs) => {
99938
- const taskCwd = daemon.taskManager.getTaskCwd(collabTaskId);
99939
- if (!taskCwd) return false;
99940
- return isUnderAllowedRoot2(abs, [taskCwd]);
99941
- }
99942
- }
100335
+ log.info("Collab file I/O channel open deferred \u2014 peer not yet registered");
100336
+ let done = false;
100337
+ const finish = () => {
100338
+ if (done) return;
100339
+ done = true;
100340
+ clearTimeout(timer);
100341
+ unsubscribe();
100342
+ };
100343
+ const unsubscribe = peerRoleRegistry.onRegister((peerId) => {
100344
+ if (peerId !== _machineId) return;
100345
+ if (done) return;
100346
+ finish();
100347
+ wireFileIo();
99943
100348
  });
99944
- log.info("Collab file I/O channel wired");
100349
+ const timer = setTimeout(() => {
100350
+ finish();
100351
+ log.warn(
100352
+ { machineId: _machineId },
100353
+ "Collab file I/O channel buffer expired \u2014 peer never registered"
100354
+ );
100355
+ }, 5e3);
99945
100356
  },
99946
100357
  onTerminalChannel: (_machineId, rawChannel, taskId, terminalId) => {
99947
- const peerRole = peerRoleRegistry.getRole(_machineId);
99948
- if (peerRole !== "owner" && peerRole !== "collaborator-full") {
99949
- log.warn(
99950
- { role: peerRole },
99951
- "Collab peer with insufficient role attempted terminal access"
100358
+ const wireTerminal = () => {
100359
+ const channelState = narrow(rawChannel).readyState;
100360
+ if (channelState === "closed" || channelState === "closing") {
100361
+ log.info("Collab terminal raw channel closed before peer registered \u2014 skipping wire");
100362
+ return;
100363
+ }
100364
+ const peerRole = peerRoleRegistry.getRole(_machineId);
100365
+ if (peerRole !== "owner" && peerRole !== "collaborator-full") {
100366
+ log.warn(
100367
+ { role: peerRole },
100368
+ "Collab peer with insufficient role attempted terminal access"
100369
+ );
100370
+ return;
100371
+ }
100372
+ const dc = narrow(rawChannel);
100373
+ const guarded = new GuardedChannel(toGuardedDataChannel5(dc), {
100374
+ label: "terminal-collab",
100375
+ policy: "drop",
100376
+ logAdapter
100377
+ });
100378
+ const handler = handleTerminalChannel(
100379
+ taskId,
100380
+ terminalId,
100381
+ workspaceRoot,
100382
+ (data) => {
100383
+ guarded.send(data);
100384
+ },
100385
+ logAdapter,
100386
+ { ptys: terminalPtys }
99952
100387
  );
100388
+ dc.onmessage = (ev) => handler.onMessage(typeof ev.data === "string" ? ev.data : String(ev.data));
100389
+ dc.onclose = () => {
100390
+ handler.dispose();
100391
+ guarded.dispose();
100392
+ };
100393
+ log.info({ taskId, terminalId }, "Collab terminal channel wired");
100394
+ };
100395
+ if (peerRoleRegistry.isRegistered(_machineId)) {
100396
+ wireTerminal();
99953
100397
  return;
99954
100398
  }
99955
- const dc = narrow(rawChannel);
99956
- const guarded = new GuardedChannel(toGuardedDataChannel5(dc), {
99957
- label: "terminal-collab",
99958
- policy: "drop",
99959
- logAdapter
99960
- });
99961
- const handler = handleTerminalChannel(
99962
- taskId,
99963
- terminalId,
99964
- workspaceRoot,
99965
- (data) => {
99966
- guarded.send(data);
99967
- },
99968
- logAdapter,
99969
- { ptys: terminalPtys }
99970
- );
99971
- dc.onmessage = (ev) => handler.onMessage(typeof ev.data === "string" ? ev.data : String(ev.data));
99972
- dc.onclose = () => {
99973
- handler.dispose();
99974
- guarded.dispose();
100399
+ log.info("Collab terminal channel open deferred \u2014 peer not yet registered");
100400
+ let done = false;
100401
+ const finish = () => {
100402
+ if (done) return;
100403
+ done = true;
100404
+ clearTimeout(timer);
100405
+ unsubscribe();
99975
100406
  };
99976
- log.info({ taskId, terminalId }, "Collab terminal channel wired");
100407
+ const unsubscribe = peerRoleRegistry.onRegister((peerId) => {
100408
+ if (peerId !== _machineId) return;
100409
+ if (done) return;
100410
+ finish();
100411
+ wireTerminal();
100412
+ });
100413
+ const timer = setTimeout(() => {
100414
+ finish();
100415
+ log.warn(
100416
+ { machineId: _machineId },
100417
+ "Collab terminal channel buffer expired \u2014 peer never registered"
100418
+ );
100419
+ }, 5e3);
99977
100420
  },
99978
100421
  onThreadMessageChannel: (machineId, rawChannel, taskId, threadId) => {
99979
100422
  if (!isTaskMessageChannelAllowed(taskId, collabTaskId)) {
@@ -101605,4 +102048,4 @@ export {
101605
102048
  _testing,
101606
102049
  serve
101607
102050
  };
101608
- //# sourceMappingURL=serve-FOWLAISV.js.map
102051
+ //# sourceMappingURL=serve-3EFFP3PN.js.map