@ycodium-ai/plugin-team 0.2.3272 → 0.2.3306

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.
package/dist/index.js CHANGED
@@ -6894,7 +6894,8 @@ var en_default = {
6894
6894
  noGoal: "Start a goal before publishing or reverting the handbook. Without a goal there are no team members, acknowledgement gates or reminders.",
6895
6895
  noRequirement: "No handbook release requires acknowledgement yet.",
6896
6896
  wrongRelease: "Acknowledge handbook release {{release}}, the currently required version.",
6897
- guard: "First acknowledge handbook release {{release}} (team_acknowledge_handbook).",
6897
+ guard: "{{member}} has not acknowledged handbook release {{release}} yet. The team plugin has asked them to; when they do, you are woken if items still wait to be assigned. Assign then; do not retry on a timer.",
6898
+ leadAcknowledged: "{{member}} acknowledged handbook release {{release}}. Assignments to them are no longer refused: assign the items still waiting.",
6898
6899
  reminder: "Handbook release {{release}} requires acknowledgement. Read wiki_read_handbook, then call team_acknowledge_handbook with this release number.",
6899
6900
  leadBrief: "Read the handbook with wiki_read_handbook and explicitly acknowledge its currently required release with team_acknowledge_handbook before taking new work. Publish or revert it through team_publish_handbook or team_revert_handbook; publishing a release that requires acknowledgement also acknowledges it for you.",
6900
6901
  memberBrief: "Read the handbook with wiki_read_handbook and call team_acknowledge_handbook for its currently required release before taking new work; reading alone does not acknowledge it."
@@ -6930,6 +6931,15 @@ var en_default = {
6930
6931
  untitled: "(Untitled)",
6931
6932
  other: "Other",
6932
6933
  unassigned: "Unassigned",
6934
+ sessionRunning: "Session running",
6935
+ sessionFailed: "Failed",
6936
+ sessionWaiting: "Needs input",
6937
+ sessionStopped: "Stopped",
6938
+ sessionStoppedAt: "Stopped at {{time}}",
6939
+ sessionStatusTitle: "Status of the assigned member's session",
6940
+ queued: "Queued",
6941
+ dateLocale: "en",
6942
+ queuedTitle: "Assigned but not woken yet: waits until their current item is done",
6933
6943
  blockedBy: "Blocked by {{count}} work items",
6934
6944
  goalRequired: "Start a goal above before assigning work to members.",
6935
6945
  loading: "Loading board\u2026",
@@ -7161,25 +7171,29 @@ var en_default = {
7161
7171
  };
7162
7172
 
7163
7173
  // src/handbook/messages.ts
7164
- function handbookMessage(key, release) {
7165
- return en_default.handbook[key].replace(/{{release}}/g, String(release ?? ""));
7174
+ function handbookMessage(key, release, member) {
7175
+ return en_default.handbook[key].replace(/{{release}}/g, String(release ?? "")).replace(/{{member}}/g, member ?? "");
7166
7176
  }
7167
7177
 
7168
7178
  // src/domain/leadBrief.ts
7169
7179
  var PERSON_MESSAGE_RULE = "Send a message to the person only for a product decision you cannot make yourself; put progress, updates and technical problems in work item comments with work_post_comment instead.";
7170
7180
  var MEMBER_CHECKS_RULE = "Before you mark an item done, run the checks that this project's rules name for the code you changed, and fix every failure. Do not mark an item done while a check still fails; say in a work item comment what still fails instead.";
7171
- var MEMBER_NO_STASH_RULE = "Do not use git stash: all worktrees of this repository share one stash stack. To try something you can undo, commit to a temporary branch or copy files into an ignored folder inside your own worktree, and delete it before you mark the item done.";
7181
+ var NO_STASH = "Do not use git stash: all worktrees of this repository share one stash stack.";
7182
+ var MEMBER_NO_STASH_RULE = `${NO_STASH} To try something you can undo, commit to a temporary branch or copy files into an ignored folder inside your own worktree, and delete it before you mark the item done.`;
7172
7183
  var MEMBER_ASK_BEFORE_OUTSIDE_RULE = "Before you change dependencies or patches, write outside the project root, or fetch code from the network (git clone, downloads), mention your lead in a work item comment and wait for an answer.";
7173
7184
  var MEMBER_READ_RANGES_RULE = "Read files in ranges: find the place with Grep first, then read only the lines you need; read only the relevant section of a skill file.";
7174
- var LEAD_ACCEPT_RULE = "When you check a finished item, run the checks that this project's rules name yourself. Send the item back if its changes touch patches/ or a lockfile, unless the item has a comment from you approving that change.";
7185
+ var LEAD_ACCEPT_RULE = `When you check a finished item, run the checks that this project's rules name yourself. Send the item back if its changes touch patches/ or a lockfile, unless the item has a comment from you approving that change. ${NO_STASH} To compare against the base, run the same check in your own working directory on the base branch and compare the error counts; do not stash in the member's worktree.`;
7175
7186
  var memberStuckRule = (leadThreadId) => {
7176
7187
  const mention = leadThreadId === void 0 ? "" : ` that mentions your lead (mentions: [{ "kind": "agent", "threadId": "${leadThreadId}" }])`;
7177
7188
  const reply = leadThreadId === void 0 ? "" : " Your lead will reply or change the plan.";
7178
7189
  return `If the same error still fails after you have tried to fix it twice, stop and post a work item comment with work_post_comment${mention} saying what fails and what you tried.${reply}`;
7179
7190
  };
7180
- var LEAD_STUCK_RULE = "When a member mentions you in a comment because they are stuck, answer in that item's comments: give a direction, split the item, or reassign it.";
7191
+ var LEAD_STUCK_RULE = "When a member is stuck, answer in that item's comments before you next look in on members: give a concrete direction, split the item, or reassign it. A member is stuck when they mention you because they are stuck, and also when a comment of theirs reports a blocker, such as a test or error they cannot get past, even if it does not mention you. Do not just note it.";
7192
+ var LEAD_REPEATED_BLOCKER_RULE = "If, when you look in, a blocker a member reported is still there and their branch has no new commit since your last look-in, do not just ask for a work-in-progress commit: ask what they have tried and which exact line fails, then give a direction, or split or reassign the item.";
7181
7193
  var LEAD_SPLIT_RULE = "Use work_create_item to split the goal into a few work items: one item per task, with a clear action in the title. Members do not see this conversation or your goal, so put everything a member needs into the description: the deliverable, the constraints, and how to hand the work in. Create them without assignees.";
7182
- var LEAD_OWN_ITEM_RULE = "If a work item for this goal is assigned to you, also record it as blocked by every item you create. When they are all done you will be told; then check the results and mark that item done with work_update_item.";
7194
+ var LEAD_OWN_ITEM_RULE = "If a work item for this goal is assigned to you, also record it as blocked by every item you create. Each time a member marks an item done you will be told, so check it then; when they are all done, mark that item done with work_update_item.";
7195
+ var LEAD_REBALANCE_RULE = "When a member has nothing in progress while another member has an item waiting in line that is not blocked, move one waiting item to the idle member with work_update_item.";
7196
+ var LEAD_CHECKPOINT_RULE = "Each time you look in on members, check each item in progress: if it entered In Progress more than 30 minutes ago and has had no new commit on the member's branch and no comment since, post a comment with work_post_comment that mentions the member and asks them to run the tests and typecheck for what they have changed so far, commit the work in progress to their branch, and post a one-line progress and blocker comment on the item. Take the times from work_history and, when you can read the member's worktree on this machine, from git log -1 --format=%cI there, and compare them with the current time; do not estimate. When you cannot read their worktree, as with a remote member, judge by work_history and comments alone; not being able to see commits does not mean there are none.";
7183
7197
  var LEAD_SEND_BACK_RULE = "If a finished item is not acceptable, send it back: use work_update_item to set its state back to an open one (see work_list_states), and change its assignees too if someone else should redo it. Then say what is wrong in a comment with work_post_comment, mentioning the member.";
7184
7198
  var LEAD_NO_MEMBER_TASKS = "Do not do members' tasks yourself, and never assign an item you create to yourself: merging, pushing and integrating the work are member tasks too.";
7185
7199
  var tableCell = (value) => value.replace(/\|/g, "\\|").replace(/[\r\n]/g, " ");
@@ -7210,7 +7224,10 @@ function leadBrief(goal, roster) {
7210
7224
  LEAD_SEND_BACK_RULE,
7211
7225
  LEAD_ACCEPT_RULE,
7212
7226
  LEAD_STUCK_RULE,
7227
+ LEAD_CHECKPOINT_RULE,
7228
+ LEAD_REPEATED_BLOCKER_RULE,
7213
7229
  `Only after all dependencies are recorded, assign each item to exactly one member with work_update_item. For local members use assignees: [{ "kind": "agent", "threadId": "<threadId from the table>" }]. Assign work to members with no item in progress first; do not give one member two items while another is idle. An item assigned to a local member who already has work waits in line and wakes them after their current item is finished. Record dependencies before assigning so blocked work waits until it can start. ${LEAD_NO_MEMBER_TASKS}`,
7230
+ LEAD_REBALANCE_RULE,
7214
7231
  ...remoteMembers.length === 0 ? [] : [
7215
7232
  'Remote members are on another machine. To give them work, assign the work item to them with work_update_item and assignees: [{ "kind": "remote-agent", "principalId": "<principalId from the table>" }]; they pick it up from their own machine. Do not create conversations, start turns, or send wake-up signals for them on this machine.',
7216
7233
  "A comment that mentions a remote member, or replies to one of their comments, wakes them on their machine, so you can ask or answer them in work item comments. Once they mark an item done they stop working on it, so to send work back to a remote member you still reopen the item as described above."
@@ -7548,7 +7565,15 @@ var BoardItem = Struct({
7548
7565
  state: Struct({ stateId: String4, name: String4 }),
7549
7566
  assignees: ArraySchema(WorkIdentity),
7550
7567
  blockedByTotal: Number5,
7551
- version: Number5
7568
+ version: Number5,
7569
+ assigneeSession: optionalKey2(
7570
+ Struct({
7571
+ threadId: String4,
7572
+ status: Literals(["running", "failed", "waiting", "stopped"]),
7573
+ stoppedAt: optionalKey2(String4)
7574
+ })
7575
+ ),
7576
+ queued: optionalKey2(Boolean3)
7552
7577
  });
7553
7578
  var BoardGoalItem = Struct({
7554
7579
  workItemId: String4,
@@ -7610,6 +7635,10 @@ var decodeProject = decodeUnknownOption2(Struct({ projectId: String4 }));
7610
7635
  var decodeFact = decodeUnknownOption2(
7611
7636
  Struct({ pluginId: Literal2("team"), name: String4 })
7612
7637
  );
7638
+ var eventProject = (event) => {
7639
+ const payload = decodeProject(event.payload);
7640
+ return isSome2(payload) ? payload.value.projectId : event.aggregateKind === "project" ? event.aggregateId : null;
7641
+ };
7613
7642
  var WORK_EVENTS = /* @__PURE__ */ new Set([
7614
7643
  "work.item-created",
7615
7644
  "work.item-updated",
@@ -7626,7 +7655,7 @@ var HANDBOOK_EVENTS = /* @__PURE__ */ new Set([
7626
7655
  "wiki.page-erased",
7627
7656
  "memory.toolkit-set"
7628
7657
  ]);
7629
- function registerTeamWatch(ctx, options) {
7658
+ function registerTeamWatch(ctx, hasMember, options) {
7630
7659
  const after = options.after ?? ((delay2, notify) => {
7631
7660
  const timer = NodeTimers.setTimeout(notify, delay2);
7632
7661
  return () => NodeTimers.clearTimeout(timer);
@@ -7634,38 +7663,95 @@ function registerTeamWatch(ctx, options) {
7634
7663
  const listeners = /* @__PURE__ */ new Map();
7635
7664
  const cursors = /* @__PURE__ */ new Map();
7636
7665
  const timers = /* @__PURE__ */ new Map();
7666
+ const pending = /* @__PURE__ */ new Map();
7667
+ const subscriptions = /* @__PURE__ */ new Map();
7637
7668
  let disposed = false;
7638
- const changed = (event, board, handbook) => {
7639
- const payload = decodeProject(event.payload);
7640
- const projectId = isSome2(payload) ? payload.value.projectId : event.aggregateKind === "project" ? event.aggregateId : null;
7669
+ const invalidate = (projectId, board, handbook, sequence = 0) => {
7641
7670
  if (projectId === null || disposed || !listeners.has(projectId)) return;
7642
- const previous = cursors.get(projectId) ?? emptyChanges();
7643
- cursors.set(projectId, {
7644
- cursor: board ? Math.max(previous.cursor, event.sequence) : previous.cursor,
7645
- handbookCursor: handbook ? Math.max(previous.handbookCursor, event.sequence) : previous.handbookCursor
7671
+ const batch = pending.get(projectId);
7672
+ pending.set(projectId, {
7673
+ board: board || batch?.board === true,
7674
+ handbook: handbook || batch?.handbook === true,
7675
+ boardSequence: Math.max(board ? sequence : 0, batch?.boardSequence ?? 0),
7676
+ handbookSequence: Math.max(handbook ? sequence : 0, batch?.handbookSequence ?? 0)
7646
7677
  });
7647
7678
  if (timers.has(projectId)) return;
7648
7679
  timers.set(
7649
7680
  projectId,
7650
7681
  after(50, () => {
7651
7682
  timers.delete(projectId);
7652
- for (const emit of [...listeners.get(projectId) ?? []])
7683
+ const batch2 = pending.get(projectId);
7684
+ pending.delete(projectId);
7685
+ if (batch2 === void 0) return;
7686
+ const previous = cursors.get(projectId) ?? emptyChanges();
7687
+ cursors.set(projectId, {
7688
+ cursor: batch2.board ? Math.max(previous.cursor + 1, batch2.boardSequence) : previous.cursor,
7689
+ handbookCursor: batch2.handbook ? Math.max(previous.handbookCursor + 1, batch2.handbookSequence) : previous.handbookCursor
7690
+ });
7691
+ for (const emit of listeners.get(projectId) ?? [])
7653
7692
  emit(cursors.get(projectId) ?? emptyChanges());
7654
7693
  })
7655
7694
  );
7656
7695
  };
7696
+ const changed = (event, board, handbook) => invalidate(eventProject(event), board, handbook, event.sequence);
7657
7697
  const stops = [.../* @__PURE__ */ new Set([...WORK_EVENTS, ...HANDBOOK_EVENTS])].map(
7658
7698
  (type) => ctx.events.onRaw(
7659
7699
  type,
7660
7700
  (event) => changed(event, WORK_EVENTS.has(type), HANDBOOK_EVENTS.has(type))
7661
7701
  )
7662
7702
  );
7703
+ const subscribe = (projectId) => {
7704
+ if (subscriptions.has(projectId)) return;
7705
+ const controller = new AbortController();
7706
+ const { signal } = controller;
7707
+ const known = /* @__PURE__ */ new Set();
7708
+ const active = () => !disposed && !signal.aborted && listeners.has(projectId);
7709
+ const done2 = (async () => {
7710
+ while (active()) {
7711
+ try {
7712
+ await ctx.threads.subscribeList(
7713
+ { projectId },
7714
+ async (frame) => {
7715
+ if (!active() || frame._tag === "synchronized") return;
7716
+ const ids = frame._tag === "snapshot" ? [.../* @__PURE__ */ new Set([...known, ...frame.value.items.map((row) => row.threadId)])] : frame._tag === "refresh" ? frame.threads.map((row) => row.threadId) : [
7717
+ frame.change._tag === "removed" ? frame.change.threadId : frame.change.thread.threadId
7718
+ ];
7719
+ if (frame._tag === "snapshot") {
7720
+ known.clear();
7721
+ for (const row of frame.value.items) known.add(row.threadId);
7722
+ } else if (frame._tag === "delta") {
7723
+ if (frame.change._tag === "removed") known.delete(frame.change.threadId);
7724
+ else known.add(frame.change.thread.threadId);
7725
+ }
7726
+ if (ids.length === 0) return;
7727
+ const matches = await hasMember(projectId, ids).catch(() => false);
7728
+ if (matches && active()) invalidate(projectId, true, false);
7729
+ },
7730
+ signal
7731
+ );
7732
+ } catch {
7733
+ }
7734
+ if (!active()) return;
7735
+ await new Promise((resolve2) => {
7736
+ const finish = () => {
7737
+ cancel();
7738
+ signal.removeEventListener("abort", finish);
7739
+ resolve2();
7740
+ };
7741
+ const cancel = after(1e3, finish);
7742
+ signal.addEventListener("abort", finish, { once: true });
7743
+ if (signal.aborted) finish();
7744
+ });
7745
+ }
7746
+ })();
7747
+ subscriptions.set(projectId, { controller, done: done2 });
7748
+ };
7663
7749
  stops.push(
7664
7750
  ctx.events.onRaw("plugin.event-appended", (event) => {
7665
7751
  const fact = decodeFact(event.payload);
7666
7752
  if (isNone2(fact)) return;
7667
7753
  const name = fact.value.name;
7668
- const board = name === "team/goal-started" || name === "team/roster-set";
7754
+ const board = name === "team/goal-started" || name === "team/roster-set" || name === "team/member-woken";
7669
7755
  const handbook = name === "team/goal-started" || name === "team/handbook-acknowledged" || name === "team/handbook-publication-completed";
7670
7756
  if (board || handbook) changed(event, board, handbook);
7671
7757
  })
@@ -7684,6 +7770,7 @@ function registerTeamWatch(ctx, options) {
7684
7770
  };
7685
7771
  listeners.set(projectId, subscribers);
7686
7772
  subscribers.add(ring);
7773
+ subscribe(projectId);
7687
7774
  try {
7688
7775
  ring(cursors.get(projectId) ?? emptyChanges());
7689
7776
  await new Promise((resolve2) => {
@@ -7694,20 +7781,30 @@ function registerTeamWatch(ctx, options) {
7694
7781
  subscribers.delete(ring);
7695
7782
  if (subscribers.size === 0) {
7696
7783
  listeners.delete(projectId);
7784
+ const subscription = subscriptions.get(projectId);
7785
+ subscriptions.delete(projectId);
7786
+ subscription?.controller.abort();
7697
7787
  timers.get(projectId)?.();
7698
7788
  timers.delete(projectId);
7789
+ pending.delete(projectId);
7699
7790
  cursors.delete(projectId);
7791
+ await subscription?.done;
7700
7792
  }
7701
7793
  }
7702
7794
  }
7703
7795
  });
7704
- ctx.onDispose(() => {
7796
+ ctx.onDispose(async () => {
7705
7797
  disposed = true;
7798
+ const owned = [...subscriptions.values()];
7799
+ for (const subscription of owned) subscription.controller.abort();
7706
7800
  for (const stop of stops) stop();
7707
7801
  for (const cancel of timers.values()) cancel();
7708
7802
  timers.clear();
7709
7803
  listeners.clear();
7710
7804
  cursors.clear();
7805
+ pending.clear();
7806
+ subscriptions.clear();
7807
+ await Promise.all(owned.map((subscription) => subscription.done));
7711
7808
  });
7712
7809
  }
7713
7810
 
@@ -7735,6 +7832,121 @@ async function readPages(maximum, read, keep = "oldest") {
7735
7832
  return { items, truncated };
7736
7833
  }
7737
7834
 
7835
+ // src/runtime/wakeQueries.ts
7836
+ function memberWorktreeFor(goals, project2, projectId, recipient) {
7837
+ if (!("threadId" in recipient) || project2 === void 0 || !project2.threads.includes(recipient.threadId) || project2.leadThreads?.includes(recipient.threadId))
7838
+ return;
7839
+ const { threadId } = recipient;
7840
+ const leadThreadId = project2.leadThreads?.[0];
7841
+ if (leadThreadId === void 0) return;
7842
+ return {
7843
+ leadThreadId,
7844
+ failure: project2.worktreeFailures?.[threadId],
7845
+ // 只在要建树或报失败时读目标取名字,叫醒的热路径不多读。
7846
+ name: async () => (await goals.read(projectId))?.members.find((member) => member.threadId === threadId)?.name ?? project2.memberIds?.[threadId] ?? threadId
7847
+ };
7848
+ }
7849
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
7850
+ var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
7851
+ var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
7852
+ var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
7853
+ (blocker) => !isClosed(readFact(project2.items, blocker))
7854
+ );
7855
+ var isAssignedOpen = (project2, workItemId, threadId) => {
7856
+ const item = readFact(project2.items, workItemId);
7857
+ return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
7858
+ };
7859
+ var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
7860
+ var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
7861
+ var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
7862
+ var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
7863
+ var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
7864
+ function countDeferredWakes(project2, threadId) {
7865
+ let count = 0;
7866
+ for (const workItemId in project2.items) {
7867
+ const item = readFact(project2.items, workItemId);
7868
+ if (item !== null && isDeferredWake(project2, item, threadId)) count++;
7869
+ }
7870
+ return count;
7871
+ }
7872
+ function deferredWakes(project2, threadId) {
7873
+ return Object.entries(project2.items).filter(([, item]) => isDeferredWake(project2, item, threadId)).map(([workItemId, item]) => ({ workItemId, sequenceId: item.sequenceId })).sort(
7874
+ (a, b) => Number(hasOpenBlocker(project2, a.workItemId)) - Number(hasOpenBlocker(project2, b.workItemId)) || compareWakeItems(a, b)
7875
+ );
7876
+ }
7877
+
7878
+ // src/boardProgress.ts
7879
+ var sessionStatus = (thread) => {
7880
+ const { status, threadId } = thread;
7881
+ switch (status) {
7882
+ case "running":
7883
+ case "failed":
7884
+ return { threadId, status };
7885
+ case "needs-input":
7886
+ case "needs-approval":
7887
+ return { threadId, status: "waiting" };
7888
+ case "idle":
7889
+ case "stopped": {
7890
+ const stoppedAt = thread.summary?.latestTurn?.completedAt;
7891
+ return { threadId, status: "stopped", ...stoppedAt == null ? {} : { stoppedAt } };
7892
+ }
7893
+ default:
7894
+ return void 0;
7895
+ }
7896
+ };
7897
+ function boardProgress(rows, goal, threads, wakes) {
7898
+ const members = goalExecution(goal).members;
7899
+ const isMember2 = (threadId) => findGoalMember(members, { threadId }) !== void 0;
7900
+ const sessions = new Map(
7901
+ threads.filter((thread) => isMember2(thread.threadId)).map((thread) => [thread.threadId, thread])
7902
+ );
7903
+ const started = /* @__PURE__ */ new Map();
7904
+ const countStarted = (threadId, workItemId) => {
7905
+ const items = started.get(threadId) ?? /* @__PURE__ */ new Set();
7906
+ items.add(workItemId);
7907
+ started.set(threadId, items);
7908
+ };
7909
+ for (const item of rows) {
7910
+ if (item.state.group !== "started") continue;
7911
+ for (const assignee of item.assignees) {
7912
+ if (assignee.kind === "agent") countStarted(assignee.threadId, item.workItemId);
7913
+ }
7914
+ }
7915
+ for (const [workItemId, item] of Object.entries(wakes?.items ?? {})) {
7916
+ if (item.stateGroup === "started") {
7917
+ for (const threadId of item.assigneeThreads) countStarted(threadId, workItemId);
7918
+ }
7919
+ }
7920
+ const queuedByMember = /* @__PURE__ */ new Map();
7921
+ const queuedFor = (threadId) => {
7922
+ let items = queuedByMember.get(threadId);
7923
+ if (items === void 0) {
7924
+ items = new Set(
7925
+ wakes === void 0 ? [] : deferredWakes(wakes, threadId).map((item) => item.workItemId)
7926
+ );
7927
+ queuedByMember.set(threadId, items);
7928
+ }
7929
+ return items;
7930
+ };
7931
+ return (item) => {
7932
+ const assignees = item.assignees.filter(
7933
+ (assignee2) => assignee2.kind === "agent" && isMember2(assignee2.threadId)
7934
+ );
7935
+ const queued = assignees.some(
7936
+ (assignee2) => assignee2.kind === "agent" && queuedFor(assignee2.threadId).has(item.workItemId)
7937
+ );
7938
+ const assignee = item.state.group === "started" ? assignees.find(
7939
+ (assignee2) => assignee2.kind === "agent" && ((started.get(assignee2.threadId)?.size ?? 0) <= 1 || wakes?.items[item.workItemId]?.wakeStatus?.[assignee2.threadId] === "active")
7940
+ ) : void 0;
7941
+ const thread = assignee?.kind === "agent" ? sessions.get(assignee.threadId) : void 0;
7942
+ const session = thread === void 0 ? void 0 : sessionStatus(thread);
7943
+ return {
7944
+ ...session === void 0 ? {} : { assigneeSession: session },
7945
+ ...queued ? { queued: true } : {}
7946
+ };
7947
+ };
7948
+ }
7949
+
7738
7950
  // src/workWrites.ts
7739
7951
  var decodeHostRefusal = decodeUnknownOption2(
7740
7952
  Struct({
@@ -7766,12 +7978,17 @@ async function writeWork(invocation, commandType, write) {
7766
7978
  // src/board.ts
7767
7979
  var ITEM_LIMIT = 500;
7768
7980
  var PEOPLE_LIMIT = 200;
7981
+ var MEMBER_THREAD_LIMIT = 50;
7769
7982
  var REMOTE_AGENT_LIMIT = 1e3;
7770
7983
  var decodeRead2 = decodeUnknownSync(ReadBoardInput);
7771
7984
  var decodeCreate = decodeUnknownOption2(CreateItemInput);
7772
7985
  var decodeUpdate = decodeUnknownOption2(UpdateItemInput);
7773
- function registerBoardCommands(ctx, goals, rosters, options = {}) {
7774
- const isMember2 = async (projectId, threadId) => findGoalMember(goalExecution(await goals.read(projectId)).members, { threadId }) !== void 0;
7986
+ function registerBoardCommands(ctx, goals, rosters, wakes, options = {}) {
7987
+ const hasMember = async (projectId, threadIds) => {
7988
+ const members = goalExecution(await goals.read(projectId)).members;
7989
+ return threadIds.some((threadId) => findGoalMember(members, { threadId }) !== void 0);
7990
+ };
7991
+ const isMember2 = (projectId, threadId) => hasMember(projectId, [threadId]);
7775
7992
  ctx.command({
7776
7993
  name: "board",
7777
7994
  scope: "read",
@@ -7795,7 +8012,17 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7795
8012
  invocation.viewer?.communication.people.current({}).catch(() => null)
7796
8013
  ]);
7797
8014
  const goalItemId = goal === null ? null : teamGoalItemId(projectId, goal.goalId);
7798
- const [remoteWork, { items: rows, truncated }, goalItem] = await Promise.all([
8015
+ const leadThreadId = goal?.threads.find(
8016
+ ({ memberId }) => memberId === goal.leadMemberId
8017
+ )?.threadId;
8018
+ const [
8019
+ remoteWork,
8020
+ { items: rows, truncated },
8021
+ goalItem,
8022
+ memberThreads,
8023
+ leadThread2,
8024
+ wakeState
8025
+ ] = await Promise.all([
7799
8026
  Promise.all(
7800
8027
  (roster?.members ?? []).flatMap(
7801
8028
  (member) => member.principal === void 0 ? [] : [
@@ -7808,8 +8035,37 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7808
8035
  ),
7809
8036
  readPages(ITEM_LIMIT, (page) => ctx.work.items.list({ projectId, ...page })),
7810
8037
  // 目标那件按号单读:看板只列最新的 500 件,目标那件可能不在里面。
7811
- goalItemId === null ? void 0 : ctx.work.items.get({ projectId, workItemIds: [goalItemId] }).then(({ items: items2 }) => items2.find((item) => item.workItemId === goalItemId))
8038
+ goalItemId === null ? void 0 : ctx.work.items.get({ projectId, workItemIds: [goalItemId] }).then(({ items: items2 }) => items2.find((item) => item.workItemId === goalItemId)),
8039
+ // 成员都是负责人的子会话;人数很少,只读摘要,按小上限防游标出错。
8040
+ leadThreadId === void 0 ? null : readPages(
8041
+ MEMBER_THREAD_LIMIT,
8042
+ (page) => ctx.threads.list({
8043
+ ...page,
8044
+ projectId,
8045
+ parentThreadId: leadThreadId,
8046
+ include: ["summary"],
8047
+ limit: MEMBER_THREAD_LIMIT
8048
+ })
8049
+ ).catch(() => null),
8050
+ // 负责人自己也可能领件;与成员分开读,一边失败不连累另一边。
8051
+ leadThreadId === void 0 ? null : ctx.threads.get({ threadId: leadThreadId }).catch(() => null),
8052
+ wakes.get().catch(() => null)
7812
8053
  ]);
8054
+ const progress = boardProgress(
8055
+ rows,
8056
+ goal,
8057
+ [
8058
+ ...memberThreads?.items ?? [],
8059
+ ...leadThread2 === null ? [] : [
8060
+ {
8061
+ threadId: leadThread2.id,
8062
+ status: leadThread2.status,
8063
+ summary: { latestTurn: leadThread2.latestTurn }
8064
+ }
8065
+ ]
8066
+ ],
8067
+ wakeState?.[projectId]
8068
+ );
7813
8069
  const enabled = (toolkit) => toolkitStates.toolkits.some((row) => row.toolkit === toolkit && row.enabled);
7814
8070
  const items = rows.map((item) => ({
7815
8071
  workItemId: item.workItemId,
@@ -7818,7 +8074,8 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7818
8074
  state: { stateId: item.state.stateId, name: item.state.name },
7819
8075
  assignees: item.assignees.map(workIdentity),
7820
8076
  blockedByTotal: item.blockedByTotal,
7821
- version: item.version
8077
+ version: item.version,
8078
+ ...progress(item)
7822
8079
  }));
7823
8080
  return {
7824
8081
  states: stateList.states.map(({ stateId, name, sequence, defaultKey }) => ({
@@ -7916,7 +8173,7 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7916
8173
  return "code" in result2 ? result2 : { workItemId: result2.result.workItemId, version: result2.result.version };
7917
8174
  }
7918
8175
  });
7919
- registerTeamWatch(ctx, options);
8176
+ registerTeamWatch(ctx, hasMember, options);
7920
8177
  }
7921
8178
 
7922
8179
  // src/detailContracts.ts
@@ -8196,46 +8453,16 @@ function createRosterService(ctx, options = {}) {
8196
8453
  };
8197
8454
  }
8198
8455
 
8199
- // src/runtime/wakeQueries.ts
8200
- function memberWorktreeFor(goals, project2, projectId, recipient) {
8201
- if (!("threadId" in recipient) || project2 === void 0 || !project2.threads.includes(recipient.threadId) || project2.leadThreads?.includes(recipient.threadId))
8202
- return;
8203
- const { threadId } = recipient;
8204
- const leadThreadId = project2.leadThreads?.[0];
8205
- if (leadThreadId === void 0) return;
8206
- return {
8207
- leadThreadId,
8208
- failure: project2.worktreeFailures?.[threadId],
8209
- // 只在要建树或报失败时读目标取名字,叫醒的热路径不多读。
8210
- name: async () => (await goals.read(projectId))?.members.find((member) => member.threadId === threadId)?.name ?? project2.memberIds?.[threadId] ?? threadId
8211
- };
8212
- }
8213
- var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8214
- var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
8215
- var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
8216
- var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8217
- (blocker) => !isClosed(readFact(project2.items, blocker))
8218
- );
8219
- var isAssignedOpen = (project2, workItemId, threadId) => {
8220
- const item = readFact(project2.items, workItemId);
8221
- return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
8222
- };
8223
- var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
8224
- var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
8225
- var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8226
- var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
8227
- var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
8228
- function countDeferredWakes(project2, threadId) {
8229
- let count = 0;
8230
- for (const workItemId in project2.items) {
8231
- const item = readFact(project2.items, workItemId);
8232
- if (item !== null && isDeferredWake(project2, item, threadId)) count++;
8233
- }
8234
- return count;
8235
- }
8236
-
8237
8456
  // src/runtime/deliver.ts
8238
8457
  import * as NodeCrypto from "node:crypto";
8458
+ var worktreeSetupSentence = async (ctx, projectId, branch) => {
8459
+ const intro = `You are in your own newly created worktree on branch ${branch}.`;
8460
+ const project2 = await ctx.projects.get({ projectId }).catch(() => null);
8461
+ const setup = project2?.scripts.find((script) => script.runOnWorktreeCreate);
8462
+ if (project2 == null || setup === void 0)
8463
+ return `${intro} Install dependencies according to the project's instructions before starting work.`;
8464
+ return `${intro} If dependencies are missing, install them before starting work. This project defines a setup command for that: ${setup.command}; run it from the worktree root with the environment variables YCODIUM_PROJECT_ROOT set to "${project2.workspaceRoot}" and YCODIUM_WORKTREE_PATH set to this worktree's path. If it fails with "could not lock config file", wait a moment and run it again.`;
8465
+ };
8239
8466
  var stableHash = (key) => NodeCrypto.createHash("sha256").update(key).digest("hex").slice(0, 32);
8240
8467
  var memberWorktreeBranch = (name, threadId) => {
8241
8468
  const slug = Array.from(
@@ -8272,7 +8499,7 @@ async function deliverWake(ctx, input) {
8272
8499
  requestId: input.requestId,
8273
8500
  to: { principalId: recipient.principalId },
8274
8501
  text: typeof input.text === "string" ? input.text : await input.text(),
8275
- delivery: "wake"
8502
+ delivery: input.steer ? "steer" : "wake"
8276
8503
  });
8277
8504
  } else {
8278
8505
  const execution = await ctx.projections.getThreadExecution(recipient.threadId);
@@ -8292,14 +8519,23 @@ async function deliverWake(ctx, input) {
8292
8519
  runtimeMode: execution.runtimeMode,
8293
8520
  interactionMode: execution.interactionMode
8294
8521
  };
8522
+ const runningTurnId = input.steer && execution.session?.status === "running" ? execution.session.activeTurnId : null;
8295
8523
  const startTurn = (suffix) => ctx.threads.turns.start({
8296
8524
  ...turn,
8297
8525
  requestId: `${input.requestId}${suffix}`,
8298
- message: { ...turn.message, messageId: `${input.messageId}${suffix}` }
8526
+ message: { ...turn.message, messageId: `${input.messageId}${suffix}` },
8527
+ ...runningTurnId == null ? {} : {
8528
+ intentAction: {
8529
+ kind: "steer",
8530
+ source: "automatic",
8531
+ turnId: runningTurnId,
8532
+ commandId: `${input.requestId}${suffix}:steer`
8533
+ }
8534
+ }
8299
8535
  });
8300
8536
  let failure = worktree?.failure;
8301
8537
  let delivered = false;
8302
- if (worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8538
+ if (runningTurnId == null && worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8303
8539
  const { lane } = worktree;
8304
8540
  const first = await lane.serialize(threadId, async () => {
8305
8541
  const current = await ctx.projections.getThreadExecution(threadId);
@@ -8315,7 +8551,7 @@ async function deliverWake(ctx, input) {
8315
8551
  ...turn.message,
8316
8552
  text: `${text}
8317
8553
 
8318
- You are in your own newly created worktree on branch ${branch}. Install dependencies according to the project's instructions before starting work.`
8554
+ ${await worktreeSetupSentence(ctx, input.projectId, branch)}`
8319
8555
  },
8320
8556
  bootstrap: {
8321
8557
  prepareWorktree: {
@@ -8374,6 +8610,7 @@ var RecoveryPlan = Struct({
8374
8610
  workItemIds: ArraySchema(String4),
8375
8611
  resumeQueue: optionalKey2(Literal2(true))
8376
8612
  });
8613
+ var withoutStartFailure = ({ startFailureTurnId: _done, ...member }) => member;
8377
8614
  var REQUESTED = "team/member-recovery-requested";
8378
8615
  var DELIVERED = "team/member-recovery-delivered";
8379
8616
  var decodeRecoveryFact = decodeUnknownOption2(
@@ -8535,7 +8772,9 @@ function foldRecovery(state, event, previous) {
8535
8772
  for (const [projectId, project2] of Object.entries(state)) {
8536
8773
  const member = project2.recovery?.[threadId];
8537
8774
  if (member === void 0 || !isMember(project2, { threadId })) continue;
8538
- const turnId = session.activeTurnId ?? member.turnId;
8775
+ const startFailed = session.status === "error" && session.activeTurnId === null && member.status !== "running" && member.status !== "error" && member.turnId !== null;
8776
+ const startFailureTurnId = startFailed ? `${member.turnId}#start-${event.sequence}` : session.status === "error" && session.activeTurnId === null ? member.startFailureTurnId : void 0;
8777
+ const turnId = startFailureTurnId ?? session.activeTurnId ?? member.turnId;
8539
8778
  if (session.status !== "running" && turnId !== null && member.handledTurnIds.includes(turnId))
8540
8779
  return state;
8541
8780
  const succeeded = (session.status === "ready" || session.status === "idle") && member.status === "running";
@@ -8548,7 +8787,12 @@ function foldRecovery(state, event, previous) {
8548
8787
  (id) => isOpenWorkFor(project2, id, threadId)
8549
8788
  );
8550
8789
  const queuedContinuation = member.pending?.plan?.resumeQueue === true && !failed;
8551
- const pending = queuedContinuation ? member.pending : failed && hasOpenWork ? member.pending?.turnId === turnId && member.pending.kind !== "stall" ? { ...member.pending, category: session.failureCategory ?? member.pending.category } : { turnId, category: session.failureCategory ?? "unknown", sequence: event.sequence } : succeeded && turnId !== null && hasStarted ? {
8790
+ const pending = queuedContinuation ? member.pending : failed && hasOpenWork ? member.pending?.turnId === turnId && member.pending.kind !== "stall" ? { ...member.pending, category: session.failureCategory ?? member.pending.category } : {
8791
+ turnId,
8792
+ category: session.failureCategory ?? "unknown",
8793
+ sequence: event.sequence,
8794
+ ...startFailed && member.turnId !== null ? { afterTurnId: member.turnId } : {}
8795
+ } : succeeded && turnId !== null && hasStarted ? {
8552
8796
  turnId,
8553
8797
  category: "normal-end",
8554
8798
  kind: "stall",
@@ -8562,8 +8806,9 @@ function foldRecovery(state, event, previous) {
8562
8806
  recovery: {
8563
8807
  ...project2.recovery,
8564
8808
  [threadId]: {
8565
- ...member,
8566
- turnId,
8809
+ ...withoutStartFailure(member),
8810
+ ...startFailureTurnId === void 0 ? {} : { startFailureTurnId },
8811
+ turnId: startFailureTurnId === void 0 ? turnId : member.turnId,
8567
8812
  status: session.status,
8568
8813
  retriedItemIds: succeeded ? [] : member.retriedItemIds,
8569
8814
  ...succeeded ? { unprogressedRetryItemIds: [] } : {},
@@ -8674,6 +8919,81 @@ function dueRecoveries(state, changedThreadId) {
8674
8919
  );
8675
8920
  }
8676
8921
 
8922
+ // src/runtime/reviewWakes.ts
8923
+ var decodeCompletion = decodeUnknownOption2(
8924
+ Struct({
8925
+ projectId: String4,
8926
+ workItemId: String4,
8927
+ state: Struct({ group: String4 }),
8928
+ actor: Union2([
8929
+ Struct({ kind: Literal2("agent"), threadId: String4 }),
8930
+ Struct({ kind: Literal2("remote-agent"), principalId: String4 })
8931
+ ])
8932
+ })
8933
+ );
8934
+ var openLeadItemsBlockedBy = (project2, workItemId) => {
8935
+ const lead = project2.leadThreads?.[0];
8936
+ return lead === void 0 ? [] : Object.entries(project2.items).filter(
8937
+ ([id]) => isAssignedOpen(project2, id, lead) && readFact(project2.blockers, id)?.includes(workItemId) === true
8938
+ );
8939
+ };
8940
+ function reviewOnUpdate(previous, next, event) {
8941
+ const decoded = decodeCompletion(event.payload);
8942
+ if (isNone2(decoded) || decoded.value.state.group !== "completed") return null;
8943
+ const { projectId, workItemId, actor } = decoded.value;
8944
+ if (readFact(previous.items, workItemId)?.stateGroup === "completed") return null;
8945
+ const lead = next.leadThreads?.[0];
8946
+ if (lead === void 0) return null;
8947
+ const by = actor.kind === "agent" ? { threadId: actor.threadId } : { principalId: actor.principalId };
8948
+ if (!isMember(next, by) || "threadId" in by && next.leadThreads?.includes(by.threadId))
8949
+ return null;
8950
+ const goals = openLeadItemsBlockedBy(next, workItemId);
8951
+ if (goals.length === 0 || goals.some(([id]) => isWorkable(next, id))) return null;
8952
+ return {
8953
+ key: `review:${workItemId}:${lead}#${event.sequence}`,
8954
+ projectId,
8955
+ workItemId,
8956
+ threadId: lead,
8957
+ by
8958
+ };
8959
+ }
8960
+ var isLiveReview = (project2, review) => review.threadId === project2.leadThreads?.[0] && readFact(project2.items, review.workItemId)?.stateGroup === "completed" && openLeadItemsBlockedBy(project2, review.workItemId).length > 0;
8961
+ var isReviewDue = (project2, key) => project2.pendingReviews?.some((review) => review.key === key && isLiveReview(project2, review)) === true;
8962
+ var dueReviews = (project2) => (project2.pendingReviews ?? []).filter((review) => isLiveReview(project2, review)).map((review) => {
8963
+ const item = readFact(project2.items, review.workItemId);
8964
+ return {
8965
+ ...review,
8966
+ kind: "review",
8967
+ lead: true,
8968
+ ...item === null ? {} : { sequenceId: item.sequenceId }
8969
+ };
8970
+ });
8971
+ function pruneReviews(previous, state) {
8972
+ let next = state;
8973
+ for (const [projectId, project2] of Object.entries(state)) {
8974
+ const pending = project2.pendingReviews;
8975
+ if (pending === void 0 || pending.length === 0) continue;
8976
+ const before = previous[projectId];
8977
+ if (before !== void 0 && before.leadThreads === project2.leadThreads && before.items === project2.items && before.blockers === project2.blockers)
8978
+ continue;
8979
+ const live = pending.filter((review) => isLiveReview(project2, review));
8980
+ if (live.length !== pending.length)
8981
+ next = { ...next, [projectId]: { ...project2, pendingReviews: live } };
8982
+ }
8983
+ return next;
8984
+ }
8985
+ function reviewFacts(project2, queues) {
8986
+ const { busy, waiting } = queues;
8987
+ return {
8988
+ idle: project2.threads.filter(
8989
+ (threadId) => !project2.leadThreads?.includes(threadId) && !busy.has(threadId) && (waiting.get(threadId) ?? []).length === 0
8990
+ ),
8991
+ queued: [...busy].flatMap(
8992
+ (threadId) => [...waiting.get(threadId) ?? []].sort(compareWakeItems).map(({ workItemId, sequenceId }) => ({ workItemId, sequenceId, threadId }))
8993
+ )
8994
+ };
8995
+ }
8996
+
8677
8997
  // src/runtime/wakeState.ts
8678
8998
  var WAKE_STATE_KEY = "wakes";
8679
8999
  var WAKE_STATE_VERSION = 7;
@@ -8890,6 +9210,15 @@ function foldWorkWakes(state, event) {
8890
9210
  }
8891
9211
  };
8892
9212
  }
9213
+ if (project3.pendingReviews?.some((review) => review.key === key)) {
9214
+ return {
9215
+ ...state,
9216
+ [projectId2]: {
9217
+ ...project3,
9218
+ pendingReviews: project3.pendingReviews.filter((review) => review.key !== key)
9219
+ }
9220
+ };
9221
+ }
8893
9222
  if (project3.woken.includes(key)) return state;
8894
9223
  const items = { ...project3.items };
8895
9224
  for (const [workItemId2, item] of Object.entries(items)) {
@@ -8975,6 +9304,9 @@ function foldWorkWakes(state, event) {
8975
9304
  };
8976
9305
  if (work.type === "work.item-claimed" && currentItem.claimedBy.length === 0 && hasOpenBlocker(next, workItemId))
8977
9306
  next = markBlockedAfterClaim(next, workItemId);
9307
+ const review = work.type === "work.item-updated" ? reviewOnUpdate(project2, next, event) : null;
9308
+ if (review !== null)
9309
+ next = { ...next, pendingReviews: [...next.pendingReviews ?? [], review] };
8978
9310
  break;
8979
9311
  }
8980
9312
  case "work.relation-added":
@@ -9046,7 +9378,7 @@ function foldWorkWakes(state, event) {
9046
9378
  return { ...state, [projectId]: next };
9047
9379
  }
9048
9380
  function foldWakes(state, event) {
9049
- return foldRecovery(foldWorkWakes(state, event), event, state);
9381
+ return foldRecovery(pruneReviews(state, foldWorkWakes(state, event)), event, state);
9050
9382
  }
9051
9383
  var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9052
9384
  var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken) => {
@@ -9070,9 +9402,9 @@ var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken)
9070
9402
  };
9071
9403
  };
9072
9404
  var needsMemberSlot = (wake) => wake.kind === "item" && wake.lead !== true;
9073
- function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9405
+ function memberQueues(projectId, project2, woken = new Set(project2.woken), onlyThreadId) {
9074
9406
  const busy = /* @__PURE__ */ new Set();
9075
- const slots = /* @__PURE__ */ new Map();
9407
+ const waiting = /* @__PURE__ */ new Map();
9076
9408
  for (const [workItemId, item] of Object.entries(project2.items)) {
9077
9409
  if (onlyThreadId !== void 0 && !item.assigneeThreads.includes(onlyThreadId)) continue;
9078
9410
  if (!isWorkable(project2, workItemId)) continue;
@@ -9082,12 +9414,24 @@ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9082
9414
  if (item.wakeStatus?.[threadId] === "active") busy.add(threadId);
9083
9415
  const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
9084
9416
  if (candidate === null || !needsMemberSlot(candidate)) continue;
9085
- const first = slots.get(threadId);
9086
- if (first === void 0 || compareWakeItems(candidate, first) < 0)
9087
- slots.set(threadId, candidate);
9417
+ const queue = waiting.get(threadId);
9418
+ if (queue === void 0) waiting.set(threadId, [candidate]);
9419
+ else queue.push(candidate);
9088
9420
  }
9089
9421
  }
9090
- for (const threadId of busy) slots.delete(threadId);
9422
+ return { busy, waiting };
9423
+ }
9424
+ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9425
+ const { busy, waiting } = memberQueues(projectId, project2, woken, onlyThreadId);
9426
+ const slots = /* @__PURE__ */ new Map();
9427
+ for (const [threadId, candidates] of waiting)
9428
+ if (!busy.has(threadId))
9429
+ slots.set(
9430
+ threadId,
9431
+ candidates.reduce(
9432
+ (first, candidate) => compareWakeItems(candidate, first) < 0 ? candidate : first
9433
+ )
9434
+ );
9091
9435
  return slots;
9092
9436
  }
9093
9437
  function isWakeDue(state, wake) {
@@ -9095,6 +9439,7 @@ function isWakeDue(state, wake) {
9095
9439
  if (project2 === null) return false;
9096
9440
  if (wake.kind === "comment")
9097
9441
  return project2.pendingComments.some((comment) => comment.key === wake.key);
9442
+ if (wake.kind === "review") return isReviewDue(project2, wake.key);
9098
9443
  if (!("threadId" in wake)) return false;
9099
9444
  const item = readFact(project2.items, wake.workItemId);
9100
9445
  if (item === null) return false;
@@ -9128,6 +9473,7 @@ function dueWakes(state) {
9128
9473
  due.push(candidate);
9129
9474
  }
9130
9475
  }
9476
+ due.push(...dueReviews(project2));
9131
9477
  for (const comment of project2.pendingComments) {
9132
9478
  const item = readFact(project2.items, comment.workItemId);
9133
9479
  due.push({
@@ -9160,6 +9506,15 @@ var itemText = (workItemId, sequenceId, item, leadThreadId) => `${itemHeadline(s
9160
9506
  var leadItemText = (workItemId, sequenceId, title) => `You have ${titled(workItemId, sequenceId, title)}. You lead this team, so this item is the goal itself: do not claim it or do its work yourself. Read it with work_read_items, passing workItemIds: ["${workItemId}"]. If it has no work items yet, split it as your team brief says. ${LEAD_SPLIT_RULE} ${LEAD_OWN_ITEM_RULE} Record dependencies and assign each item to one member as the brief describes. ${LEAD_NO_MEMBER_TASKS} ${PERSON_MESSAGE_RULE}`;
9161
9507
  var leadUnblockedText = (workItemId, sequenceId, title) => `Everything blocking ${titled(workItemId, sequenceId, title)} is done now. Read those items with work_read_items and check the results. ${LEAD_ACCEPT_RULE} ${LEAD_SEND_BACK_RULE} When all of them are acceptable, mark this item done with work_update_item. ${LEAD_NO_MEMBER_TASKS}`;
9162
9508
  var unblockedText = (workItemId, sequenceId, title) => `Everything blocking ${titled(workItemId, sequenceId, title)} is done now. Read those items with work_read_items, finish this one, and mark it done with work_update_item.`;
9509
+ var leadReviewText = (workItemId, sequenceId, title, by, facts) => [
9510
+ `${by} marked ${titled(workItemId, sequenceId, title)} done. Read it with work_read_items, passing workItemIds: ["${workItemId}"], and check it now.`,
9511
+ LEAD_ACCEPT_RULE,
9512
+ LEAD_SEND_BACK_RULE,
9513
+ ...facts.idle.length === 0 ? [] : [`Nothing in progress now: ${facts.idle.join("; ")}.`],
9514
+ ...facts.queued.length === 0 ? [] : [`Waiting in line, not blocked: ${facts.queued.join("; ")}.`],
9515
+ ...facts.idle.length > 0 && facts.queued.length > 0 ? [LEAD_REBALANCE_RULE] : [],
9516
+ LEAD_NO_MEMBER_TASKS
9517
+ ].join(" ");
9163
9518
  var commentText2 = (workItemId, sequenceId) => `There is a new comment for you on ${workLabel(workItemId, sequenceId)}. Read it with work_list_comments, passing workItemId: "${workItemId}", and reply there if needed.`;
9164
9519
  function startWakeRuntime(ctx, goals, options = {}) {
9165
9520
  const wakes = ctx.derivedState({
@@ -9189,11 +9544,35 @@ function startWakeRuntime(ctx, goals, options = {}) {
9189
9544
  requestId: `team-wake:${id}`,
9190
9545
  messageId: `team-wake-msg:${id}`,
9191
9546
  memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees },
9547
+ // 评论在收件人正在跑时插进当前这一轮,不排到这一轮结束(试跑 3:一轮 97 分钟,检查点落空)。
9548
+ ...wake.kind === "comment" ? { steer: true } : {},
9192
9549
  text: async () => {
9193
9550
  if ("principalId" in wake || wake.kind === "comment")
9194
9551
  return commentText2(wake.workItemId, wake.sequenceId);
9195
9552
  const item = await readItem(wake.projectId, wake.workItemId);
9196
9553
  const title = item?.title;
9554
+ if (wake.kind === "review") {
9555
+ const project2 = state[wake.projectId];
9556
+ const members = (await goals.read(wake.projectId))?.members ?? [];
9557
+ const name = (id2) => {
9558
+ const member = findGoalMember(members, id2);
9559
+ const key = "threadId" in id2 ? id2.threadId : id2.principalId;
9560
+ return `${member?.name ?? key} (${"threadId" in id2 ? "threadId" : "principalId"} ${key})`;
9561
+ };
9562
+ const facts = project2 === void 0 ? { idle: [], queued: [] } : reviewFacts(project2, memberQueues(wake.projectId, project2));
9563
+ return leadReviewText(
9564
+ wake.workItemId,
9565
+ wake.sequenceId,
9566
+ title,
9567
+ wake.by === void 0 ? "A member" : name(wake.by),
9568
+ {
9569
+ idle: facts.idle.map((threadId) => name({ threadId })),
9570
+ queued: facts.queued.map(
9571
+ (queued) => `${workLabel(queued.workItemId, queued.sequenceId)}, assigned to ${name({ threadId: queued.threadId })}`
9572
+ )
9573
+ }
9574
+ );
9575
+ }
9197
9576
  if (wake.kind === "item")
9198
9577
  return wake.lead === true ? leadItemText(wake.workItemId, wake.sequenceId, title) : itemText(wake.workItemId, wake.sequenceId, item, wake.leadThreadId);
9199
9578
  return (wake.lead === true ? leadUnblockedText : unblockedText)(
@@ -9247,7 +9626,7 @@ var readStallExecution = async (ctx, threadId) => {
9247
9626
  };
9248
9627
  var itemLabel = (item) => `W${item.sequenceId} (workItemId ${item.workItemId})`;
9249
9628
  var deferredText = (recovery) => recovery.deferredWakeCount === void 0 ? "" : `They also have ${recovery.deferredWakeCount} assigned ${recovery.deferredWakeCount === 1 ? "item that has" : "items that have"} not been woken yet. `;
9250
- var leadText = (recovery, name, paused) => `Member ${name} (threadId ${recovery.threadId}) stopped because of an upstream error (${recovery.category}). ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still open. ${deferredText(recovery)}${recovery.alreadyRetried ? "An automatic continuation has already been attempted for this work during these consecutive failures. " : ""}${leadInstructions(paused)}`;
9629
+ var leadText = (recovery, name, paused) => `Member ${name} (threadId ${recovery.threadId}) ${recovery.afterTurnId === void 0 ? "stopped because of an upstream error" : "could not start a session"} (${recovery.category}). ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still open. ${deferredText(recovery)}${recovery.alreadyRetried ? "An automatic continuation has already been attempted for this work during these consecutive failures. " : ""}${leadInstructions(paused)}`;
9251
9630
  var commentInstructions = (recovery) => recovery.items.map(
9252
9631
  (item) => item.lastCommentId === void 0 ? `${itemLabel(item)} has no comments yet. Continue from your existing workspace changes and turn history.` : `For ${itemLabel(item)}, the latest comment is commentId ${item.lastCommentId}. Read work_list_comments with workItemId: "${item.workItemId}" and continue from that comment and your existing workspace changes; do not start over.`
9253
9632
  ).join("\n");
@@ -9261,6 +9640,7 @@ var stallLeadText = (recovery, name) => `Member ${name} (threadId ${recovery.thr
9261
9640
  ).join("")}${leadInstructions()}`;
9262
9641
  function startRecoveryRuntime(ctx, goals, state, worktrees) {
9263
9642
  let pending = Promise.resolve();
9643
+ const skipped = /* @__PURE__ */ new Map();
9264
9644
  let disposed = false;
9265
9645
  const execute = async (scope2) => {
9266
9646
  if (disposed) return;
@@ -9278,6 +9658,9 @@ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9278
9658
  )
9279
9659
  );
9280
9660
  }
9661
+ for (const [key, entry] of skipped)
9662
+ if (readFact(current, entry.projectId)?.recovery?.[entry.threadId]?.pending?.turnId !== entry.turnId)
9663
+ skipped.delete(key);
9281
9664
  const recoveries = dueRecoveries(
9282
9665
  projects,
9283
9666
  scope2?.kind === "thread" ? scope2.threadId : void 0
@@ -9290,8 +9673,22 @@ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9290
9673
  const stalled = recovery.kind === "stall";
9291
9674
  const execution = stalled ? await readStallExecution(ctx, recovery.threadId) : await ctx.projections.getThreadExecution(recovery.threadId);
9292
9675
  const queuedContinuation = recovery.plan?.resumeQueue === true;
9293
- if (execution === null || execution.archivedAt !== null || execution.session == null || !queuedContinuation && (execution.session.activeTurnId !== null || execution.latestTurn?.turnId !== recovery.turnId || (stalled ? !["ready", "idle", "stopped"].includes(execution.session.status) || execution.latestTurn.state !== "completed" || execution.latestTurn.completedAt === null : execution.session.status !== "error" || execution.latestTurn.state !== "error")))
9676
+ if (execution === null || execution.archivedAt !== null || execution.session == null || !queuedContinuation && (execution.session.activeTurnId !== null || execution.latestTurn?.turnId !== (recovery.afterTurnId ?? recovery.turnId) || (stalled ? !["ready", "idle", "stopped"].includes(execution.session.status) || execution.latestTurn.state !== "completed" || execution.latestTurn.completedAt === null : execution.session.status !== "error" || execution.latestTurn.state === "running"))) {
9677
+ if (!skipped.has(recovery.key)) {
9678
+ skipped.set(recovery.key, {
9679
+ projectId: recovery.projectId,
9680
+ threadId: recovery.threadId,
9681
+ turnId: recovery.turnId
9682
+ });
9683
+ ctx.log.warn("team member recovery skipped", {
9684
+ threadId: recovery.threadId,
9685
+ turnId: recovery.turnId,
9686
+ sessionStatus: execution?.session?.status ?? null,
9687
+ latestTurnState: execution?.latestTurn?.state ?? null
9688
+ });
9689
+ }
9294
9690
  continue;
9691
+ }
9295
9692
  if ("summary" in execution) {
9296
9693
  if (execution.summary.queuePaused || execution.summary.pendingApprovalCount > 0 || execution.summary.pendingUserInputCount > 0 || execution.summary.queue.length > 0 || (execution.pendingAsyncQuestionCount ?? 0) > 0 || // 成员给人发了消息、人还没回:他在等回答,不提醒也不叫负责人。
9297
9694
  (execution.awaitingPersonReplyCount ?? 0) > 0)
@@ -9491,6 +9888,28 @@ async function publicationRequiresAck(memory, projectId, input) {
9491
9888
  return false;
9492
9889
  }
9493
9890
 
9891
+ // src/handbook/waitingWork.ts
9892
+ async function hasUnassignedWork(items, projectId, goal) {
9893
+ const goalItemId = teamGoalItemId(projectId, goal.goalId);
9894
+ const members = goalExecution(goal).members.filter(
9895
+ (member) => member.memberId !== goal.leadMemberId
9896
+ );
9897
+ const waiting = (item) => item.workItemId !== goalItemId && !isClosed({ stateGroup: item.state.group }) && !item.assignees.some(
9898
+ (assignee) => assignee.kind === "agent" ? findGoalMember(members, { threadId: assignee.threadId }) !== void 0 : assignee.kind === "remote-agent" ? findGoalMember(members, { principalId: assignee.principalId }) !== void 0 : false
9899
+ );
9900
+ let cursor = null;
9901
+ do {
9902
+ const page = await items.list({
9903
+ projectId,
9904
+ limit: PAGE_SIZE,
9905
+ ...cursor === null ? {} : { cursor }
9906
+ });
9907
+ if (page.items.some(waiting)) return true;
9908
+ cursor = page.nextCursor;
9909
+ } while (cursor !== null);
9910
+ return false;
9911
+ }
9912
+
9494
9913
  // src/handbook/contracts.ts
9495
9914
  var Release = Int.check(isGreaterThanOrEqualTo3(1));
9496
9915
  var HandbookPageKind = Literals([
@@ -9549,6 +9968,10 @@ var Acknowledged = Struct({
9549
9968
  var ReminderSent = Struct({
9550
9969
  ...Acknowledged.fields
9551
9970
  });
9971
+ var LeadNotified = Struct({
9972
+ ...Acknowledged.fields,
9973
+ woke: Boolean3
9974
+ });
9552
9975
 
9553
9976
  // src/handbook/state.ts
9554
9977
  var HANDBOOK_EVENTS2 = ["plugin.event-appended", "wiki.handbook-released"];
@@ -9575,7 +9998,8 @@ var decodeFact3 = decodeUnknownOption2(
9575
9998
  envelope("handbook-publication-intended", PublicationIntent),
9576
9999
  envelope("handbook-publication-completed", PublicationCompleted),
9577
10000
  envelope("handbook-acknowledged", Acknowledged),
9578
- envelope("handbook-reminder-sent", ReminderSent)
10001
+ envelope("handbook-reminder-sent", ReminderSent),
10002
+ envelope("handbook-lead-notified", LeadNotified)
9579
10003
  ])
9580
10004
  );
9581
10005
  var decodeRelease = decodeUnknownSync(
@@ -9600,24 +10024,28 @@ var without = (rows, key) => {
9600
10024
  delete next[key];
9601
10025
  return next;
9602
10026
  };
10027
+ var queueReminders = (pending, project2, projectId, release, members) => {
10028
+ const next = { ...pending };
10029
+ for (const { memberId } of members) {
10030
+ const key = reminderKey(projectId, release, memberId);
10031
+ if (readFact(project2.reminded, key) === null)
10032
+ next[key] = { projectId, release, member: { memberId } };
10033
+ }
10034
+ return next;
10035
+ };
9603
10036
  function foldHandbook(state, event) {
9604
10037
  if (event.type === "wiki.handbook-released") {
9605
10038
  const fact2 = decodeRelease(event.payload);
9606
10039
  const project3 = readFact(state.projects, fact2.projectId) ?? emptyHandbookProject();
9607
- const pendingReminders = { ...state.pendingReminders };
9608
- if (fact2.requiresAck)
9609
- for (const { memberId, ...recipient } of goalExecution(project3.goal).members) {
9610
- const key = reminderKey(fact2.projectId, fact2.release, memberId);
9611
- if (readFact(project3.reminded, key) === null)
9612
- pendingReminders[key] = {
9613
- projectId: fact2.projectId,
9614
- release: fact2.release,
9615
- member: { memberId, recipient }
9616
- };
9617
- }
9618
10040
  return {
9619
10041
  ...state,
9620
- pendingReminders,
10042
+ pendingReminders: fact2.requiresAck ? queueReminders(
10043
+ state.pendingReminders,
10044
+ project3,
10045
+ fact2.projectId,
10046
+ fact2.release,
10047
+ goalExecution(project3.goal).members
10048
+ ) : state.pendingReminders,
9621
10049
  projects: {
9622
10050
  ...state.projects,
9623
10051
  [fact2.projectId]: {
@@ -9634,11 +10062,24 @@ function foldHandbook(state, event) {
9634
10062
  const fact = decoded.value;
9635
10063
  const projectId = fact.name === "team/goal-started" ? fact.data.goal.projectId : fact.data.projectId;
9636
10064
  const project2 = readFact(state.projects, projectId) ?? emptyHandbookProject();
9637
- let next = project2;
9638
10065
  switch (fact.name) {
9639
- case "team/goal-started":
9640
- next = { ...project2, goal: fact.data.goal };
9641
- break;
10066
+ case "team/goal-started": {
10067
+ const goal = fact.data.goal;
10068
+ const required = project2.requiredRelease;
10069
+ return {
10070
+ ...state,
10071
+ pendingReminders: required === null ? state.pendingReminders : queueReminders(
10072
+ state.pendingReminders,
10073
+ project2,
10074
+ projectId,
10075
+ required,
10076
+ goalExecution(goal).members.filter(
10077
+ ({ memberId }) => memberId !== goal.leadMemberId && !hasAcknowledged(project2, memberId, required)
10078
+ )
10079
+ ),
10080
+ projects: { ...state.projects, [projectId]: { ...project2, goal } }
10081
+ };
10082
+ }
9642
10083
  case "team/handbook-publication-intended": {
9643
10084
  const intent = fact.data;
9644
10085
  if (readFact(state.intents, intent.requestId) !== null) return state;
@@ -9687,7 +10128,7 @@ function foldHandbook(state, event) {
9687
10128
  const { memberId, release } = fact.data;
9688
10129
  if (hasAcknowledged(project2, memberId, release)) return state;
9689
10130
  const previous = readFact(project2.acknowledgements, memberId);
9690
- next = {
10131
+ const next = {
9691
10132
  ...project2,
9692
10133
  acknowledgements: {
9693
10134
  ...project2.acknowledgements,
@@ -9697,27 +10138,40 @@ function foldHandbook(state, event) {
9697
10138
  }
9698
10139
  }
9699
10140
  };
10141
+ const key = reminderKey(projectId, release, memberId);
10142
+ const notice = project2.goal !== null && memberId !== project2.goal.leadMemberId && release === project2.requiredRelease;
9700
10143
  return {
9701
10144
  ...state,
9702
- pendingAcknowledgements: without(
9703
- state.pendingAcknowledgements,
9704
- reminderKey(projectId, release, memberId)
9705
- ),
10145
+ pendingAcknowledgements: without(state.pendingAcknowledgements, key),
10146
+ ...notice ? {
10147
+ pendingLeadNotices: {
10148
+ ...state.pendingLeadNotices,
10149
+ [key]: { version: 1, projectId, release, memberId }
10150
+ }
10151
+ } : {},
9706
10152
  projects: { ...state.projects, [projectId]: next }
9707
10153
  };
9708
10154
  }
10155
+ case "team/handbook-lead-notified": {
10156
+ const pendingLeadNotices = without(
10157
+ state.pendingLeadNotices ?? {},
10158
+ reminderKey(projectId, fact.data.release, fact.data.memberId)
10159
+ );
10160
+ return pendingLeadNotices === state.pendingLeadNotices ? state : { ...state, pendingLeadNotices };
10161
+ }
9709
10162
  case "team/handbook-reminder-sent": {
9710
10163
  const key = reminderKey(projectId, fact.data.release, fact.data.memberId);
9711
10164
  if (readFact(project2.reminded, key) !== null) return state;
9712
- next = { ...project2, reminded: { ...project2.reminded, [key]: true } };
9713
10165
  return {
9714
10166
  ...state,
9715
10167
  pendingReminders: without(state.pendingReminders, key),
9716
- projects: { ...state.projects, [projectId]: next }
10168
+ projects: {
10169
+ ...state.projects,
10170
+ [projectId]: { ...project2, reminded: { ...project2.reminded, [key]: true } }
10171
+ }
9717
10172
  };
9718
10173
  }
9719
10174
  }
9720
- return { ...state, projects: { ...state.projects, [projectId]: next } };
9721
10175
  }
9722
10176
 
9723
10177
  // src/handbook/runtime.ts
@@ -9810,20 +10264,23 @@ function startHandbookRuntime(ctx, worktrees) {
9810
10264
  };
9811
10265
  const remind = async () => {
9812
10266
  const snapshot = await state.get();
9813
- const wakeState = Object.keys(snapshot.pendingReminders).length === 0 ? void 0 : await worktrees.lane.state.get();
10267
+ const wakeState = Object.keys(snapshot.pendingReminders).length === 0 && Object.keys(snapshot.pendingLeadNotices ?? {}).length === 0 ? void 0 : await worktrees.lane.state.get();
9814
10268
  for (const [key, reminder] of Object.entries(snapshot.pendingReminders)) {
9815
10269
  const { projectId, release, member } = reminder;
10270
+ const project2 = readFact(snapshot.projects, projectId) ?? emptyHandbookProject();
10271
+ const recipient = currentRecipient(project2, member.memberId);
10272
+ if (recipient === void 0 || goalClosed(project2, wakeState?.[projectId])) continue;
9816
10273
  try {
9817
10274
  const id = stableHash(key);
9818
10275
  const worktree = memberWorktreeFor(
9819
10276
  worktrees.goals,
9820
10277
  wakeState?.[projectId],
9821
10278
  projectId,
9822
- member.recipient
10279
+ recipient
9823
10280
  );
9824
- const delivered = await deliverWake(ctx, {
10281
+ const delivered = hasAcknowledged(project2, member.memberId, release) || await deliverWake(ctx, {
9825
10282
  projectId,
9826
- recipient: member.recipient,
10283
+ recipient,
9827
10284
  requestId: `team-handbook:${id}`,
9828
10285
  messageId: `team-handbook-msg:${id}`,
9829
10286
  text: handbookMessage("reminder", release),
@@ -9846,6 +10303,39 @@ function startHandbookRuntime(ctx, worktrees) {
9846
10303
  ctx.log.warn("team handbook reminder failed", { projectId, key, error });
9847
10304
  }
9848
10305
  }
10306
+ const unassigned = /* @__PURE__ */ new Map();
10307
+ for (const [key, notice] of Object.entries(snapshot.pendingLeadNotices ?? {})) {
10308
+ const { projectId, release, memberId } = notice;
10309
+ const project2 = readFact(snapshot.projects, projectId) ?? emptyHandbookProject();
10310
+ const lead = leadThread(project2);
10311
+ try {
10312
+ const { goal } = project2;
10313
+ const waiting = lead !== void 0 && goal !== null && currentRecipient(project2, memberId) !== void 0 && !goalClosed(project2, wakeState?.[projectId]) && await (unassigned.get(projectId) ?? unassigned.set(projectId, hasUnassignedWork(ctx.work.items, projectId, goal)).get(projectId));
10314
+ if (waiting) {
10315
+ const name = (await worktrees.goals.read(projectId))?.members.find(
10316
+ (member) => member.memberId === memberId
10317
+ )?.name ?? memberId;
10318
+ const id = stableHash(`lead:${key}`);
10319
+ const delivered = await deliverWake(ctx, {
10320
+ projectId,
10321
+ recipient: { threadId: lead },
10322
+ requestId: `team-handbook-lead:${id}`,
10323
+ messageId: `team-handbook-lead-msg:${id}`,
10324
+ text: handbookMessage("leadAcknowledged", release, name)
10325
+ });
10326
+ if (!delivered) continue;
10327
+ }
10328
+ await append2(projectId, "handbook-lead-notified", `lead:${key}`, {
10329
+ version: 1,
10330
+ projectId,
10331
+ release,
10332
+ memberId,
10333
+ woke: waiting
10334
+ });
10335
+ } catch (error) {
10336
+ ctx.log.warn("team handbook lead notice failed", { projectId, key, error });
10337
+ }
10338
+ }
9849
10339
  };
9850
10340
  const execute = async () => {
9851
10341
  for (const intent of Object.values((await state.get()).pendingPublications)) {
@@ -9868,7 +10358,11 @@ function startHandbookRuntime(ctx, worktrees) {
9868
10358
  await remind();
9869
10359
  };
9870
10360
  const flush = () => serial(execute);
9871
- ctx.reactor({ name: "team-handbook", events: HANDBOOK_EVENTS2, handle: () => flush() });
10361
+ ctx.reactor({
10362
+ name: "team-handbook",
10363
+ events: [...HANDBOOK_EVENTS2, "work.item-updated"],
10364
+ handle: () => flush()
10365
+ });
9872
10366
  ctx.onDispose(async () => {
9873
10367
  disposed = true;
9874
10368
  await pending;
@@ -9973,9 +10467,12 @@ function startHandbookRuntime(ctx, worktrees) {
9973
10467
  const caller = actor.kind === "human" ? void 0 : actor.kind === "agent" ? { kind: "thread", threadId: actor.threadId } : actor;
9974
10468
  const member = callerMember(project2, caller);
9975
10469
  if (member !== void 0 && !hasAcknowledged(project2, member.memberId, project2.requiredRelease)) {
10470
+ const name = (await worktrees.goals.read(input.projectId))?.members.find(
10471
+ (row) => row.memberId === member.memberId
10472
+ )?.name ?? member.memberId;
9976
10473
  return {
9977
10474
  allowed: false,
9978
- detail: handbookMessage("guard", project2.requiredRelease)
10475
+ detail: handbookMessage("guard", project2.requiredRelease, name)
9979
10476
  };
9980
10477
  }
9981
10478
  }
@@ -9983,6 +10480,13 @@ function startHandbookRuntime(ctx, worktrees) {
9983
10480
  }
9984
10481
  };
9985
10482
  }
10483
+ var currentRecipient = (project2, memberId) => {
10484
+ const member = goalExecution(project2.goal).members.find((row) => row.memberId === memberId);
10485
+ if (member === void 0) return void 0;
10486
+ return "threadId" in member ? { threadId: member.threadId } : { principalId: member.principalId };
10487
+ };
10488
+ var leadThread = (project2) => project2.goal?.threads.find((thread) => thread.memberId === project2.goal?.leadMemberId)?.threadId;
10489
+ var goalClosed = (project2, wake) => project2.goal !== null && wake !== void 0 && isClosed(readFact(wake.items, teamGoalItemId(project2.goal.projectId, project2.goal.goalId)));
9986
10490
 
9987
10491
  // src/handbook/entry.ts
9988
10492
  var decodeAcknowledgement = decodeUnknownSync(AcknowledgeInput);
@@ -10152,15 +10656,15 @@ function registerHandbookPanel(ctx, runtime) {
10152
10656
  // src/index.ts
10153
10657
  var index_default = definePlugin({
10154
10658
  id: "team",
10155
- apiVersion: 32,
10659
+ apiVersion: 34,
10156
10660
  activate(ctx) {
10157
10661
  const rosters = createRosterService(ctx);
10158
10662
  registerRosterCommands(ctx, rosters);
10159
10663
  const goals = createGoalService(ctx, rosters);
10160
10664
  registerGoalCommands(ctx, goals);
10161
- registerBoardCommands(ctx, goals, rosters);
10162
- registerDetailCommands(ctx, goals);
10163
10665
  const wakes = startWakeRuntime(ctx, goals);
10666
+ registerBoardCommands(ctx, goals, rosters, wakes.state);
10667
+ registerDetailCommands(ctx, goals);
10164
10668
  startRecoveryRuntime(ctx, goals, wakes.state, wakes.worktrees);
10165
10669
  const handbook = startHandbookRuntime(ctx, { lane: wakes.worktrees, goals });
10166
10670
  registerHandbook(ctx, handbook);