@ycodium-ai/plugin-team 0.2.3272 → 0.2.3313

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
@@ -6872,6 +6872,13 @@ function findGoalMember(members, identity2) {
6872
6872
  (member) => "threadId" in identity2 ? "threadId" in member && member.threadId === identity2.threadId : "principalId" in member && member.principalId === identity2.principalId
6873
6873
  );
6874
6874
  }
6875
+ function assignedToGoalMember(goal, assignees) {
6876
+ const execution = goalExecution(goal);
6877
+ const members = execution.members.filter((member) => member.memberId !== execution.leadMemberId);
6878
+ return assignees.some(
6879
+ (assignee) => assignee.kind === "agent" ? findGoalMember(members, { threadId: assignee.threadId }) !== void 0 : assignee.kind === "remote-agent" ? findGoalMember(members, { principalId: assignee.principalId }) !== void 0 : false
6880
+ );
6881
+ }
6875
6882
 
6876
6883
  // src/domain/goalItem.ts
6877
6884
  var teamGoalItemId = (projectId, goalId) => `team-goal:${projectId}:${goalId}`;
@@ -6894,7 +6901,8 @@ var en_default = {
6894
6901
  noGoal: "Start a goal before publishing or reverting the handbook. Without a goal there are no team members, acknowledgement gates or reminders.",
6895
6902
  noRequirement: "No handbook release requires acknowledgement yet.",
6896
6903
  wrongRelease: "Acknowledge handbook release {{release}}, the currently required version.",
6897
- guard: "First acknowledge handbook release {{release}} (team_acknowledge_handbook).",
6904
+ 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.",
6905
+ leadAcknowledged: "{{member}} acknowledged handbook release {{release}}. Assignments to them are no longer refused: assign the items still waiting.",
6898
6906
  reminder: "Handbook release {{release}} requires acknowledgement. Read wiki_read_handbook, then call team_acknowledge_handbook with this release number.",
6899
6907
  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
6908
  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 +6938,15 @@ var en_default = {
6930
6938
  untitled: "(Untitled)",
6931
6939
  other: "Other",
6932
6940
  unassigned: "Unassigned",
6941
+ sessionRunning: "Session running",
6942
+ sessionFailed: "Failed",
6943
+ sessionWaiting: "Needs input",
6944
+ sessionStopped: "Stopped",
6945
+ sessionStoppedAt: "Stopped at {{time}}",
6946
+ sessionStatusTitle: "Status of the assigned member's session",
6947
+ queued: "Queued",
6948
+ dateLocale: "en",
6949
+ queuedTitle: "Assigned but not woken yet: waits until their current item is done",
6933
6950
  blockedBy: "Blocked by {{count}} work items",
6934
6951
  goalRequired: "Start a goal above before assigning work to members.",
6935
6952
  loading: "Loading board\u2026",
@@ -7161,25 +7178,31 @@ var en_default = {
7161
7178
  };
7162
7179
 
7163
7180
  // src/handbook/messages.ts
7164
- function handbookMessage(key, release) {
7165
- return en_default.handbook[key].replace(/{{release}}/g, String(release ?? ""));
7181
+ function handbookMessage(key, release, member) {
7182
+ return en_default.handbook[key].replace(/{{release}}/g, String(release ?? "")).replace(/{{member}}/g, member ?? "");
7166
7183
  }
7167
7184
 
7168
7185
  // src/domain/leadBrief.ts
7169
7186
  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
7187
  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.";
7188
+ var NO_STASH = "Do not use git stash: all worktrees of this repository share one stash stack.";
7189
+ 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
7190
  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
7191
  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.";
7192
+ 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
7193
  var memberStuckRule = (leadThreadId) => {
7176
7194
  const mention = leadThreadId === void 0 ? "" : ` that mentions your lead (mentions: [{ "kind": "agent", "threadId": "${leadThreadId}" }])`;
7177
7195
  const reply = leadThreadId === void 0 ? "" : " Your lead will reply or change the plan.";
7178
7196
  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
7197
  };
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.";
7198
+ 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.";
7199
+ 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.";
7200
+ var LEAD_CLAIM_CHECK_RULE = "Each time you look in on members, also check that every item you assigned has been moved to In Progress by the member it is assigned to, and that no two members are working on the same item; if an item has not been started or is being worked on twice, ask about it in its comments or reassign it.";
7201
+ var LEAD_SCHEDULE_CHECKIN_RULE = "Before you end a turn while any item of this goal is not done, including items still in Backlog and items assigned but not started, make sure your next look-in is scheduled: call list_wakes, and if no wake-up is waiting for you, call schedule_wake with delaySeconds of at most 900 and a note saying to look in on members. Nothing else wakes you while nobody has started an item.";
7181
7202
  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.";
7203
+ 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.";
7204
+ 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.";
7205
+ 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
7206
  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
7207
  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
7208
  var tableCell = (value) => value.replace(/\|/g, "\\|").replace(/[\r\n]/g, " ");
@@ -7210,12 +7233,17 @@ function leadBrief(goal, roster) {
7210
7233
  LEAD_SEND_BACK_RULE,
7211
7234
  LEAD_ACCEPT_RULE,
7212
7235
  LEAD_STUCK_RULE,
7236
+ LEAD_CHECKPOINT_RULE,
7237
+ LEAD_REPEATED_BLOCKER_RULE,
7238
+ LEAD_CLAIM_CHECK_RULE,
7213
7239
  `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}`,
7240
+ LEAD_REBALANCE_RULE,
7214
7241
  ...remoteMembers.length === 0 ? [] : [
7215
7242
  '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
7243
  "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."
7217
7244
  ],
7218
7245
  "Use work_list_items to check progress. When all work is complete, summarize the results here in one or two sentences.",
7246
+ LEAD_SCHEDULE_CHECKIN_RULE,
7219
7247
  "Members' worktrees and branches remain after the goal ends. After their work is merged into the main line, they can be deleted from the source control panel.",
7220
7248
  PERSON_MESSAGE_RULE,
7221
7249
  "If the work item tools are unavailable, tell the user directly that this project needs the board and project memory switches turned on first."
@@ -7548,7 +7576,15 @@ var BoardItem = Struct({
7548
7576
  state: Struct({ stateId: String4, name: String4 }),
7549
7577
  assignees: ArraySchema(WorkIdentity),
7550
7578
  blockedByTotal: Number5,
7551
- version: Number5
7579
+ version: Number5,
7580
+ assigneeSession: optionalKey2(
7581
+ Struct({
7582
+ threadId: String4,
7583
+ status: Literals(["running", "failed", "waiting", "stopped"]),
7584
+ stoppedAt: optionalKey2(String4)
7585
+ })
7586
+ ),
7587
+ queued: optionalKey2(Boolean3)
7552
7588
  });
7553
7589
  var BoardGoalItem = Struct({
7554
7590
  workItemId: String4,
@@ -7610,6 +7646,10 @@ var decodeProject = decodeUnknownOption2(Struct({ projectId: String4 }));
7610
7646
  var decodeFact = decodeUnknownOption2(
7611
7647
  Struct({ pluginId: Literal2("team"), name: String4 })
7612
7648
  );
7649
+ var eventProject = (event) => {
7650
+ const payload = decodeProject(event.payload);
7651
+ return isSome2(payload) ? payload.value.projectId : event.aggregateKind === "project" ? event.aggregateId : null;
7652
+ };
7613
7653
  var WORK_EVENTS = /* @__PURE__ */ new Set([
7614
7654
  "work.item-created",
7615
7655
  "work.item-updated",
@@ -7626,7 +7666,7 @@ var HANDBOOK_EVENTS = /* @__PURE__ */ new Set([
7626
7666
  "wiki.page-erased",
7627
7667
  "memory.toolkit-set"
7628
7668
  ]);
7629
- function registerTeamWatch(ctx, options) {
7669
+ function registerTeamWatch(ctx, hasMember, options) {
7630
7670
  const after = options.after ?? ((delay2, notify) => {
7631
7671
  const timer = NodeTimers.setTimeout(notify, delay2);
7632
7672
  return () => NodeTimers.clearTimeout(timer);
@@ -7634,38 +7674,95 @@ function registerTeamWatch(ctx, options) {
7634
7674
  const listeners = /* @__PURE__ */ new Map();
7635
7675
  const cursors = /* @__PURE__ */ new Map();
7636
7676
  const timers = /* @__PURE__ */ new Map();
7677
+ const pending = /* @__PURE__ */ new Map();
7678
+ const subscriptions = /* @__PURE__ */ new Map();
7637
7679
  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;
7680
+ const invalidate = (projectId, board, handbook, sequence = 0) => {
7641
7681
  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
7682
+ const batch = pending.get(projectId);
7683
+ pending.set(projectId, {
7684
+ board: board || batch?.board === true,
7685
+ handbook: handbook || batch?.handbook === true,
7686
+ boardSequence: Math.max(board ? sequence : 0, batch?.boardSequence ?? 0),
7687
+ handbookSequence: Math.max(handbook ? sequence : 0, batch?.handbookSequence ?? 0)
7646
7688
  });
7647
7689
  if (timers.has(projectId)) return;
7648
7690
  timers.set(
7649
7691
  projectId,
7650
7692
  after(50, () => {
7651
7693
  timers.delete(projectId);
7652
- for (const emit of [...listeners.get(projectId) ?? []])
7694
+ const batch2 = pending.get(projectId);
7695
+ pending.delete(projectId);
7696
+ if (batch2 === void 0) return;
7697
+ const previous = cursors.get(projectId) ?? emptyChanges();
7698
+ cursors.set(projectId, {
7699
+ cursor: batch2.board ? Math.max(previous.cursor + 1, batch2.boardSequence) : previous.cursor,
7700
+ handbookCursor: batch2.handbook ? Math.max(previous.handbookCursor + 1, batch2.handbookSequence) : previous.handbookCursor
7701
+ });
7702
+ for (const emit of listeners.get(projectId) ?? [])
7653
7703
  emit(cursors.get(projectId) ?? emptyChanges());
7654
7704
  })
7655
7705
  );
7656
7706
  };
7707
+ const changed = (event, board, handbook) => invalidate(eventProject(event), board, handbook, event.sequence);
7657
7708
  const stops = [.../* @__PURE__ */ new Set([...WORK_EVENTS, ...HANDBOOK_EVENTS])].map(
7658
7709
  (type) => ctx.events.onRaw(
7659
7710
  type,
7660
7711
  (event) => changed(event, WORK_EVENTS.has(type), HANDBOOK_EVENTS.has(type))
7661
7712
  )
7662
7713
  );
7714
+ const subscribe = (projectId) => {
7715
+ if (subscriptions.has(projectId)) return;
7716
+ const controller = new AbortController();
7717
+ const { signal } = controller;
7718
+ const known = /* @__PURE__ */ new Set();
7719
+ const active = () => !disposed && !signal.aborted && listeners.has(projectId);
7720
+ const done2 = (async () => {
7721
+ while (active()) {
7722
+ try {
7723
+ await ctx.threads.subscribeList(
7724
+ { projectId },
7725
+ async (frame) => {
7726
+ if (!active() || frame._tag === "synchronized") return;
7727
+ 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) : [
7728
+ frame.change._tag === "removed" ? frame.change.threadId : frame.change.thread.threadId
7729
+ ];
7730
+ if (frame._tag === "snapshot") {
7731
+ known.clear();
7732
+ for (const row of frame.value.items) known.add(row.threadId);
7733
+ } else if (frame._tag === "delta") {
7734
+ if (frame.change._tag === "removed") known.delete(frame.change.threadId);
7735
+ else known.add(frame.change.thread.threadId);
7736
+ }
7737
+ if (ids.length === 0) return;
7738
+ const matches = await hasMember(projectId, ids).catch(() => false);
7739
+ if (matches && active()) invalidate(projectId, true, false);
7740
+ },
7741
+ signal
7742
+ );
7743
+ } catch {
7744
+ }
7745
+ if (!active()) return;
7746
+ await new Promise((resolve2) => {
7747
+ const finish = () => {
7748
+ cancel();
7749
+ signal.removeEventListener("abort", finish);
7750
+ resolve2();
7751
+ };
7752
+ const cancel = after(1e3, finish);
7753
+ signal.addEventListener("abort", finish, { once: true });
7754
+ if (signal.aborted) finish();
7755
+ });
7756
+ }
7757
+ })();
7758
+ subscriptions.set(projectId, { controller, done: done2 });
7759
+ };
7663
7760
  stops.push(
7664
7761
  ctx.events.onRaw("plugin.event-appended", (event) => {
7665
7762
  const fact = decodeFact(event.payload);
7666
7763
  if (isNone2(fact)) return;
7667
7764
  const name = fact.value.name;
7668
- const board = name === "team/goal-started" || name === "team/roster-set";
7765
+ const board = name === "team/goal-started" || name === "team/roster-set" || name === "team/member-woken";
7669
7766
  const handbook = name === "team/goal-started" || name === "team/handbook-acknowledged" || name === "team/handbook-publication-completed";
7670
7767
  if (board || handbook) changed(event, board, handbook);
7671
7768
  })
@@ -7684,6 +7781,7 @@ function registerTeamWatch(ctx, options) {
7684
7781
  };
7685
7782
  listeners.set(projectId, subscribers);
7686
7783
  subscribers.add(ring);
7784
+ subscribe(projectId);
7687
7785
  try {
7688
7786
  ring(cursors.get(projectId) ?? emptyChanges());
7689
7787
  await new Promise((resolve2) => {
@@ -7694,20 +7792,30 @@ function registerTeamWatch(ctx, options) {
7694
7792
  subscribers.delete(ring);
7695
7793
  if (subscribers.size === 0) {
7696
7794
  listeners.delete(projectId);
7795
+ const subscription = subscriptions.get(projectId);
7796
+ subscriptions.delete(projectId);
7797
+ subscription?.controller.abort();
7697
7798
  timers.get(projectId)?.();
7698
7799
  timers.delete(projectId);
7800
+ pending.delete(projectId);
7699
7801
  cursors.delete(projectId);
7802
+ await subscription?.done;
7700
7803
  }
7701
7804
  }
7702
7805
  }
7703
7806
  });
7704
- ctx.onDispose(() => {
7807
+ ctx.onDispose(async () => {
7705
7808
  disposed = true;
7809
+ const owned = [...subscriptions.values()];
7810
+ for (const subscription of owned) subscription.controller.abort();
7706
7811
  for (const stop of stops) stop();
7707
7812
  for (const cancel of timers.values()) cancel();
7708
7813
  timers.clear();
7709
7814
  listeners.clear();
7710
7815
  cursors.clear();
7816
+ pending.clear();
7817
+ subscriptions.clear();
7818
+ await Promise.all(owned.map((subscription) => subscription.done));
7711
7819
  });
7712
7820
  }
7713
7821
 
@@ -7735,6 +7843,121 @@ async function readPages(maximum, read, keep = "oldest") {
7735
7843
  return { items, truncated };
7736
7844
  }
7737
7845
 
7846
+ // src/runtime/wakeQueries.ts
7847
+ function memberWorktreeFor(goals, project2, projectId, recipient) {
7848
+ if (!("threadId" in recipient) || project2 === void 0 || !project2.threads.includes(recipient.threadId) || project2.leadThreads?.includes(recipient.threadId))
7849
+ return;
7850
+ const { threadId } = recipient;
7851
+ const leadThreadId = project2.leadThreads?.[0];
7852
+ if (leadThreadId === void 0) return;
7853
+ return {
7854
+ leadThreadId,
7855
+ failure: project2.worktreeFailures?.[threadId],
7856
+ // 只在要建树或报失败时读目标取名字,叫醒的热路径不多读。
7857
+ name: async () => (await goals.read(projectId))?.members.find((member) => member.threadId === threadId)?.name ?? project2.memberIds?.[threadId] ?? threadId
7858
+ };
7859
+ }
7860
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
7861
+ var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
7862
+ var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
7863
+ var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
7864
+ (blocker) => !isClosed(readFact(project2.items, blocker))
7865
+ );
7866
+ var isAssignedOpen = (project2, workItemId, threadId) => {
7867
+ const item = readFact(project2.items, workItemId);
7868
+ return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
7869
+ };
7870
+ var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
7871
+ var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
7872
+ var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
7873
+ var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
7874
+ var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
7875
+ function countDeferredWakes(project2, threadId) {
7876
+ let count = 0;
7877
+ for (const workItemId in project2.items) {
7878
+ const item = readFact(project2.items, workItemId);
7879
+ if (item !== null && isDeferredWake(project2, item, threadId)) count++;
7880
+ }
7881
+ return count;
7882
+ }
7883
+ function deferredWakes(project2, threadId) {
7884
+ return Object.entries(project2.items).filter(([, item]) => isDeferredWake(project2, item, threadId)).map(([workItemId, item]) => ({ workItemId, sequenceId: item.sequenceId })).sort(
7885
+ (a, b) => Number(hasOpenBlocker(project2, a.workItemId)) - Number(hasOpenBlocker(project2, b.workItemId)) || compareWakeItems(a, b)
7886
+ );
7887
+ }
7888
+
7889
+ // src/boardProgress.ts
7890
+ var sessionStatus = (thread) => {
7891
+ const { status, threadId } = thread;
7892
+ switch (status) {
7893
+ case "running":
7894
+ case "failed":
7895
+ return { threadId, status };
7896
+ case "needs-input":
7897
+ case "needs-approval":
7898
+ return { threadId, status: "waiting" };
7899
+ case "idle":
7900
+ case "stopped": {
7901
+ const stoppedAt = thread.summary?.latestTurn?.completedAt;
7902
+ return { threadId, status: "stopped", ...stoppedAt == null ? {} : { stoppedAt } };
7903
+ }
7904
+ default:
7905
+ return void 0;
7906
+ }
7907
+ };
7908
+ function boardProgress(rows, goal, threads, wakes) {
7909
+ const members = goalExecution(goal).members;
7910
+ const isMember2 = (threadId) => findGoalMember(members, { threadId }) !== void 0;
7911
+ const sessions = new Map(
7912
+ threads.filter((thread) => isMember2(thread.threadId)).map((thread) => [thread.threadId, thread])
7913
+ );
7914
+ const started = /* @__PURE__ */ new Map();
7915
+ const countStarted = (threadId, workItemId) => {
7916
+ const items = started.get(threadId) ?? /* @__PURE__ */ new Set();
7917
+ items.add(workItemId);
7918
+ started.set(threadId, items);
7919
+ };
7920
+ for (const item of rows) {
7921
+ if (item.state.group !== "started") continue;
7922
+ for (const assignee of item.assignees) {
7923
+ if (assignee.kind === "agent") countStarted(assignee.threadId, item.workItemId);
7924
+ }
7925
+ }
7926
+ for (const [workItemId, item] of Object.entries(wakes?.items ?? {})) {
7927
+ if (item.stateGroup === "started") {
7928
+ for (const threadId of item.assigneeThreads) countStarted(threadId, workItemId);
7929
+ }
7930
+ }
7931
+ const queuedByMember = /* @__PURE__ */ new Map();
7932
+ const queuedFor = (threadId) => {
7933
+ let items = queuedByMember.get(threadId);
7934
+ if (items === void 0) {
7935
+ items = new Set(
7936
+ wakes === void 0 ? [] : deferredWakes(wakes, threadId).map((item) => item.workItemId)
7937
+ );
7938
+ queuedByMember.set(threadId, items);
7939
+ }
7940
+ return items;
7941
+ };
7942
+ return (item) => {
7943
+ const assignees = item.assignees.filter(
7944
+ (assignee2) => assignee2.kind === "agent" && isMember2(assignee2.threadId)
7945
+ );
7946
+ const queued = assignees.some(
7947
+ (assignee2) => assignee2.kind === "agent" && queuedFor(assignee2.threadId).has(item.workItemId)
7948
+ );
7949
+ const assignee = item.state.group === "started" ? assignees.find(
7950
+ (assignee2) => assignee2.kind === "agent" && ((started.get(assignee2.threadId)?.size ?? 0) <= 1 || wakes?.items[item.workItemId]?.wakeStatus?.[assignee2.threadId] === "active")
7951
+ ) : void 0;
7952
+ const thread = assignee?.kind === "agent" ? sessions.get(assignee.threadId) : void 0;
7953
+ const session = thread === void 0 ? void 0 : sessionStatus(thread);
7954
+ return {
7955
+ ...session === void 0 ? {} : { assigneeSession: session },
7956
+ ...queued ? { queued: true } : {}
7957
+ };
7958
+ };
7959
+ }
7960
+
7738
7961
  // src/workWrites.ts
7739
7962
  var decodeHostRefusal = decodeUnknownOption2(
7740
7963
  Struct({
@@ -7766,12 +7989,17 @@ async function writeWork(invocation, commandType, write) {
7766
7989
  // src/board.ts
7767
7990
  var ITEM_LIMIT = 500;
7768
7991
  var PEOPLE_LIMIT = 200;
7992
+ var MEMBER_THREAD_LIMIT = 50;
7769
7993
  var REMOTE_AGENT_LIMIT = 1e3;
7770
7994
  var decodeRead2 = decodeUnknownSync(ReadBoardInput);
7771
7995
  var decodeCreate = decodeUnknownOption2(CreateItemInput);
7772
7996
  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;
7997
+ function registerBoardCommands(ctx, goals, rosters, wakes, options = {}) {
7998
+ const hasMember = async (projectId, threadIds) => {
7999
+ const members = goalExecution(await goals.read(projectId)).members;
8000
+ return threadIds.some((threadId) => findGoalMember(members, { threadId }) !== void 0);
8001
+ };
8002
+ const isMember2 = (projectId, threadId) => hasMember(projectId, [threadId]);
7775
8003
  ctx.command({
7776
8004
  name: "board",
7777
8005
  scope: "read",
@@ -7795,7 +8023,17 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7795
8023
  invocation.viewer?.communication.people.current({}).catch(() => null)
7796
8024
  ]);
7797
8025
  const goalItemId = goal === null ? null : teamGoalItemId(projectId, goal.goalId);
7798
- const [remoteWork, { items: rows, truncated }, goalItem] = await Promise.all([
8026
+ const leadThreadId = goal?.threads.find(
8027
+ ({ memberId }) => memberId === goal.leadMemberId
8028
+ )?.threadId;
8029
+ const [
8030
+ remoteWork,
8031
+ { items: rows, truncated },
8032
+ goalItem,
8033
+ memberThreads,
8034
+ leadThread2,
8035
+ wakeState
8036
+ ] = await Promise.all([
7799
8037
  Promise.all(
7800
8038
  (roster?.members ?? []).flatMap(
7801
8039
  (member) => member.principal === void 0 ? [] : [
@@ -7808,8 +8046,37 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7808
8046
  ),
7809
8047
  readPages(ITEM_LIMIT, (page) => ctx.work.items.list({ projectId, ...page })),
7810
8048
  // 目标那件按号单读:看板只列最新的 500 件,目标那件可能不在里面。
7811
- goalItemId === null ? void 0 : ctx.work.items.get({ projectId, workItemIds: [goalItemId] }).then(({ items: items2 }) => items2.find((item) => item.workItemId === goalItemId))
8049
+ goalItemId === null ? void 0 : ctx.work.items.get({ projectId, workItemIds: [goalItemId] }).then(({ items: items2 }) => items2.find((item) => item.workItemId === goalItemId)),
8050
+ // 成员都是负责人的子会话;人数很少,只读摘要,按小上限防游标出错。
8051
+ leadThreadId === void 0 ? null : readPages(
8052
+ MEMBER_THREAD_LIMIT,
8053
+ (page) => ctx.threads.list({
8054
+ ...page,
8055
+ projectId,
8056
+ parentThreadId: leadThreadId,
8057
+ include: ["summary"],
8058
+ limit: MEMBER_THREAD_LIMIT
8059
+ })
8060
+ ).catch(() => null),
8061
+ // 负责人自己也可能领件;与成员分开读,一边失败不连累另一边。
8062
+ leadThreadId === void 0 ? null : ctx.threads.get({ threadId: leadThreadId }).catch(() => null),
8063
+ wakes.get().catch(() => null)
7812
8064
  ]);
8065
+ const progress = boardProgress(
8066
+ rows,
8067
+ goal,
8068
+ [
8069
+ ...memberThreads?.items ?? [],
8070
+ ...leadThread2 === null ? [] : [
8071
+ {
8072
+ threadId: leadThread2.id,
8073
+ status: leadThread2.status,
8074
+ summary: { latestTurn: leadThread2.latestTurn }
8075
+ }
8076
+ ]
8077
+ ],
8078
+ wakeState?.[projectId]
8079
+ );
7813
8080
  const enabled = (toolkit) => toolkitStates.toolkits.some((row) => row.toolkit === toolkit && row.enabled);
7814
8081
  const items = rows.map((item) => ({
7815
8082
  workItemId: item.workItemId,
@@ -7818,7 +8085,8 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7818
8085
  state: { stateId: item.state.stateId, name: item.state.name },
7819
8086
  assignees: item.assignees.map(workIdentity),
7820
8087
  blockedByTotal: item.blockedByTotal,
7821
- version: item.version
8088
+ version: item.version,
8089
+ ...progress(item)
7822
8090
  }));
7823
8091
  return {
7824
8092
  states: stateList.states.map(({ stateId, name, sequence, defaultKey }) => ({
@@ -7916,7 +8184,7 @@ function registerBoardCommands(ctx, goals, rosters, options = {}) {
7916
8184
  return "code" in result2 ? result2 : { workItemId: result2.result.workItemId, version: result2.result.version };
7917
8185
  }
7918
8186
  });
7919
- registerTeamWatch(ctx, options);
8187
+ registerTeamWatch(ctx, hasMember, options);
7920
8188
  }
7921
8189
 
7922
8190
  // src/detailContracts.ts
@@ -8196,46 +8464,16 @@ function createRosterService(ctx, options = {}) {
8196
8464
  };
8197
8465
  }
8198
8466
 
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
8467
  // src/runtime/deliver.ts
8238
8468
  import * as NodeCrypto from "node:crypto";
8469
+ var worktreeSetupSentence = async (ctx, projectId, branch) => {
8470
+ const intro = `You are in your own newly created worktree on branch ${branch}.`;
8471
+ const project2 = await ctx.projects.get({ projectId }).catch(() => null);
8472
+ const setup = project2?.scripts.find((script) => script.runOnWorktreeCreate);
8473
+ if (project2 == null || setup === void 0)
8474
+ return `${intro} Install dependencies according to the project's instructions before starting work.`;
8475
+ 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.`;
8476
+ };
8239
8477
  var stableHash = (key) => NodeCrypto.createHash("sha256").update(key).digest("hex").slice(0, 32);
8240
8478
  var memberWorktreeBranch = (name, threadId) => {
8241
8479
  const slug = Array.from(
@@ -8272,7 +8510,7 @@ async function deliverWake(ctx, input) {
8272
8510
  requestId: input.requestId,
8273
8511
  to: { principalId: recipient.principalId },
8274
8512
  text: typeof input.text === "string" ? input.text : await input.text(),
8275
- delivery: "wake"
8513
+ delivery: input.steer ? "steer" : "wake"
8276
8514
  });
8277
8515
  } else {
8278
8516
  const execution = await ctx.projections.getThreadExecution(recipient.threadId);
@@ -8292,14 +8530,23 @@ async function deliverWake(ctx, input) {
8292
8530
  runtimeMode: execution.runtimeMode,
8293
8531
  interactionMode: execution.interactionMode
8294
8532
  };
8533
+ const runningTurnId = input.steer && execution.session?.status === "running" ? execution.session.activeTurnId : null;
8295
8534
  const startTurn = (suffix) => ctx.threads.turns.start({
8296
8535
  ...turn,
8297
8536
  requestId: `${input.requestId}${suffix}`,
8298
- message: { ...turn.message, messageId: `${input.messageId}${suffix}` }
8537
+ message: { ...turn.message, messageId: `${input.messageId}${suffix}` },
8538
+ ...runningTurnId == null ? {} : {
8539
+ intentAction: {
8540
+ kind: "steer",
8541
+ source: "automatic",
8542
+ turnId: runningTurnId,
8543
+ commandId: `${input.requestId}${suffix}:steer`
8544
+ }
8545
+ }
8299
8546
  });
8300
8547
  let failure = worktree?.failure;
8301
8548
  let delivered = false;
8302
- if (worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8549
+ if (runningTurnId == null && worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8303
8550
  const { lane } = worktree;
8304
8551
  const first = await lane.serialize(threadId, async () => {
8305
8552
  const current = await ctx.projections.getThreadExecution(threadId);
@@ -8315,7 +8562,7 @@ async function deliverWake(ctx, input) {
8315
8562
  ...turn.message,
8316
8563
  text: `${text}
8317
8564
 
8318
- You are in your own newly created worktree on branch ${branch}. Install dependencies according to the project's instructions before starting work.`
8565
+ ${await worktreeSetupSentence(ctx, input.projectId, branch)}`
8319
8566
  },
8320
8567
  bootstrap: {
8321
8568
  prepareWorktree: {
@@ -8374,6 +8621,7 @@ var RecoveryPlan = Struct({
8374
8621
  workItemIds: ArraySchema(String4),
8375
8622
  resumeQueue: optionalKey2(Literal2(true))
8376
8623
  });
8624
+ var withoutStartFailure = ({ startFailureTurnId: _done, ...member }) => member;
8377
8625
  var REQUESTED = "team/member-recovery-requested";
8378
8626
  var DELIVERED = "team/member-recovery-delivered";
8379
8627
  var decodeRecoveryFact = decodeUnknownOption2(
@@ -8535,7 +8783,9 @@ function foldRecovery(state, event, previous) {
8535
8783
  for (const [projectId, project2] of Object.entries(state)) {
8536
8784
  const member = project2.recovery?.[threadId];
8537
8785
  if (member === void 0 || !isMember(project2, { threadId })) continue;
8538
- const turnId = session.activeTurnId ?? member.turnId;
8786
+ const startFailed = session.status === "error" && session.activeTurnId === null && member.status !== "running" && member.status !== "error" && member.turnId !== null;
8787
+ const startFailureTurnId = startFailed ? `${member.turnId}#start-${event.sequence}` : session.status === "error" && session.activeTurnId === null ? member.startFailureTurnId : void 0;
8788
+ const turnId = startFailureTurnId ?? session.activeTurnId ?? member.turnId;
8539
8789
  if (session.status !== "running" && turnId !== null && member.handledTurnIds.includes(turnId))
8540
8790
  return state;
8541
8791
  const succeeded = (session.status === "ready" || session.status === "idle") && member.status === "running";
@@ -8548,7 +8798,12 @@ function foldRecovery(state, event, previous) {
8548
8798
  (id) => isOpenWorkFor(project2, id, threadId)
8549
8799
  );
8550
8800
  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 ? {
8801
+ const pending = queuedContinuation ? member.pending : failed && hasOpenWork ? member.pending?.turnId === turnId && member.pending.kind !== "stall" ? { ...member.pending, category: session.failureCategory ?? member.pending.category } : {
8802
+ turnId,
8803
+ category: session.failureCategory ?? "unknown",
8804
+ sequence: event.sequence,
8805
+ ...startFailed && member.turnId !== null ? { afterTurnId: member.turnId } : {}
8806
+ } : succeeded && turnId !== null && hasStarted ? {
8552
8807
  turnId,
8553
8808
  category: "normal-end",
8554
8809
  kind: "stall",
@@ -8562,8 +8817,9 @@ function foldRecovery(state, event, previous) {
8562
8817
  recovery: {
8563
8818
  ...project2.recovery,
8564
8819
  [threadId]: {
8565
- ...member,
8566
- turnId,
8820
+ ...withoutStartFailure(member),
8821
+ ...startFailureTurnId === void 0 ? {} : { startFailureTurnId },
8822
+ turnId: startFailureTurnId === void 0 ? turnId : member.turnId,
8567
8823
  status: session.status,
8568
8824
  retriedItemIds: succeeded ? [] : member.retriedItemIds,
8569
8825
  ...succeeded ? { unprogressedRetryItemIds: [] } : {},
@@ -8674,6 +8930,81 @@ function dueRecoveries(state, changedThreadId) {
8674
8930
  );
8675
8931
  }
8676
8932
 
8933
+ // src/runtime/reviewWakes.ts
8934
+ var decodeCompletion = decodeUnknownOption2(
8935
+ Struct({
8936
+ projectId: String4,
8937
+ workItemId: String4,
8938
+ state: Struct({ group: String4 }),
8939
+ actor: Union2([
8940
+ Struct({ kind: Literal2("agent"), threadId: String4 }),
8941
+ Struct({ kind: Literal2("remote-agent"), principalId: String4 })
8942
+ ])
8943
+ })
8944
+ );
8945
+ var openLeadItemsBlockedBy = (project2, workItemId) => {
8946
+ const lead = project2.leadThreads?.[0];
8947
+ return lead === void 0 ? [] : Object.entries(project2.items).filter(
8948
+ ([id]) => isAssignedOpen(project2, id, lead) && readFact(project2.blockers, id)?.includes(workItemId) === true
8949
+ );
8950
+ };
8951
+ function reviewOnUpdate(previous, next, event) {
8952
+ const decoded = decodeCompletion(event.payload);
8953
+ if (isNone2(decoded) || decoded.value.state.group !== "completed") return null;
8954
+ const { projectId, workItemId, actor } = decoded.value;
8955
+ if (readFact(previous.items, workItemId)?.stateGroup === "completed") return null;
8956
+ const lead = next.leadThreads?.[0];
8957
+ if (lead === void 0) return null;
8958
+ const by = actor.kind === "agent" ? { threadId: actor.threadId } : { principalId: actor.principalId };
8959
+ if (!isMember(next, by) || "threadId" in by && next.leadThreads?.includes(by.threadId))
8960
+ return null;
8961
+ const goals = openLeadItemsBlockedBy(next, workItemId);
8962
+ if (goals.length === 0 || goals.some(([id]) => isWorkable(next, id))) return null;
8963
+ return {
8964
+ key: `review:${workItemId}:${lead}#${event.sequence}`,
8965
+ projectId,
8966
+ workItemId,
8967
+ threadId: lead,
8968
+ by
8969
+ };
8970
+ }
8971
+ var isLiveReview = (project2, review) => review.threadId === project2.leadThreads?.[0] && readFact(project2.items, review.workItemId)?.stateGroup === "completed" && openLeadItemsBlockedBy(project2, review.workItemId).length > 0;
8972
+ var isReviewDue = (project2, key) => project2.pendingReviews?.some((review) => review.key === key && isLiveReview(project2, review)) === true;
8973
+ var dueReviews = (project2) => (project2.pendingReviews ?? []).filter((review) => isLiveReview(project2, review)).map((review) => {
8974
+ const item = readFact(project2.items, review.workItemId);
8975
+ return {
8976
+ ...review,
8977
+ kind: "review",
8978
+ lead: true,
8979
+ ...item === null ? {} : { sequenceId: item.sequenceId }
8980
+ };
8981
+ });
8982
+ function pruneReviews(previous, state) {
8983
+ let next = state;
8984
+ for (const [projectId, project2] of Object.entries(state)) {
8985
+ const pending = project2.pendingReviews;
8986
+ if (pending === void 0 || pending.length === 0) continue;
8987
+ const before = previous[projectId];
8988
+ if (before !== void 0 && before.leadThreads === project2.leadThreads && before.items === project2.items && before.blockers === project2.blockers)
8989
+ continue;
8990
+ const live = pending.filter((review) => isLiveReview(project2, review));
8991
+ if (live.length !== pending.length)
8992
+ next = { ...next, [projectId]: { ...project2, pendingReviews: live } };
8993
+ }
8994
+ return next;
8995
+ }
8996
+ function reviewFacts(project2, queues) {
8997
+ const { busy, waiting } = queues;
8998
+ return {
8999
+ idle: project2.threads.filter(
9000
+ (threadId) => !project2.leadThreads?.includes(threadId) && !busy.has(threadId) && (waiting.get(threadId) ?? []).length === 0
9001
+ ),
9002
+ queued: [...busy].flatMap(
9003
+ (threadId) => [...waiting.get(threadId) ?? []].sort(compareWakeItems).map(({ workItemId, sequenceId }) => ({ workItemId, sequenceId, threadId }))
9004
+ )
9005
+ };
9006
+ }
9007
+
8677
9008
  // src/runtime/wakeState.ts
8678
9009
  var WAKE_STATE_KEY = "wakes";
8679
9010
  var WAKE_STATE_VERSION = 7;
@@ -8890,6 +9221,15 @@ function foldWorkWakes(state, event) {
8890
9221
  }
8891
9222
  };
8892
9223
  }
9224
+ if (project3.pendingReviews?.some((review) => review.key === key)) {
9225
+ return {
9226
+ ...state,
9227
+ [projectId2]: {
9228
+ ...project3,
9229
+ pendingReviews: project3.pendingReviews.filter((review) => review.key !== key)
9230
+ }
9231
+ };
9232
+ }
8893
9233
  if (project3.woken.includes(key)) return state;
8894
9234
  const items = { ...project3.items };
8895
9235
  for (const [workItemId2, item] of Object.entries(items)) {
@@ -8975,6 +9315,9 @@ function foldWorkWakes(state, event) {
8975
9315
  };
8976
9316
  if (work.type === "work.item-claimed" && currentItem.claimedBy.length === 0 && hasOpenBlocker(next, workItemId))
8977
9317
  next = markBlockedAfterClaim(next, workItemId);
9318
+ const review = work.type === "work.item-updated" ? reviewOnUpdate(project2, next, event) : null;
9319
+ if (review !== null)
9320
+ next = { ...next, pendingReviews: [...next.pendingReviews ?? [], review] };
8978
9321
  break;
8979
9322
  }
8980
9323
  case "work.relation-added":
@@ -9046,7 +9389,7 @@ function foldWorkWakes(state, event) {
9046
9389
  return { ...state, [projectId]: next };
9047
9390
  }
9048
9391
  function foldWakes(state, event) {
9049
- return foldRecovery(foldWorkWakes(state, event), event, state);
9392
+ return foldRecovery(pruneReviews(state, foldWorkWakes(state, event)), event, state);
9050
9393
  }
9051
9394
  var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
9052
9395
  var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken) => {
@@ -9070,9 +9413,9 @@ var itemWakeCandidate = (projectId, project2, workItemId, item, threadId, woken)
9070
9413
  };
9071
9414
  };
9072
9415
  var needsMemberSlot = (wake) => wake.kind === "item" && wake.lead !== true;
9073
- function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9416
+ function memberQueues(projectId, project2, woken = new Set(project2.woken), onlyThreadId) {
9074
9417
  const busy = /* @__PURE__ */ new Set();
9075
- const slots = /* @__PURE__ */ new Map();
9418
+ const waiting = /* @__PURE__ */ new Map();
9076
9419
  for (const [workItemId, item] of Object.entries(project2.items)) {
9077
9420
  if (onlyThreadId !== void 0 && !item.assigneeThreads.includes(onlyThreadId)) continue;
9078
9421
  if (!isWorkable(project2, workItemId)) continue;
@@ -9082,12 +9425,24 @@ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9082
9425
  if (item.wakeStatus?.[threadId] === "active") busy.add(threadId);
9083
9426
  const candidate = itemWakeCandidate(projectId, project2, workItemId, item, threadId, woken);
9084
9427
  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);
9428
+ const queue = waiting.get(threadId);
9429
+ if (queue === void 0) waiting.set(threadId, [candidate]);
9430
+ else queue.push(candidate);
9088
9431
  }
9089
9432
  }
9090
- for (const threadId of busy) slots.delete(threadId);
9433
+ return { busy, waiting };
9434
+ }
9435
+ function memberItemSlots(projectId, project2, woken, onlyThreadId) {
9436
+ const { busy, waiting } = memberQueues(projectId, project2, woken, onlyThreadId);
9437
+ const slots = /* @__PURE__ */ new Map();
9438
+ for (const [threadId, candidates] of waiting)
9439
+ if (!busy.has(threadId))
9440
+ slots.set(
9441
+ threadId,
9442
+ candidates.reduce(
9443
+ (first, candidate) => compareWakeItems(candidate, first) < 0 ? candidate : first
9444
+ )
9445
+ );
9091
9446
  return slots;
9092
9447
  }
9093
9448
  function isWakeDue(state, wake) {
@@ -9095,6 +9450,7 @@ function isWakeDue(state, wake) {
9095
9450
  if (project2 === null) return false;
9096
9451
  if (wake.kind === "comment")
9097
9452
  return project2.pendingComments.some((comment) => comment.key === wake.key);
9453
+ if (wake.kind === "review") return isReviewDue(project2, wake.key);
9098
9454
  if (!("threadId" in wake)) return false;
9099
9455
  const item = readFact(project2.items, wake.workItemId);
9100
9456
  if (item === null) return false;
@@ -9128,6 +9484,7 @@ function dueWakes(state) {
9128
9484
  due.push(candidate);
9129
9485
  }
9130
9486
  }
9487
+ due.push(...dueReviews(project2));
9131
9488
  for (const comment of project2.pendingComments) {
9132
9489
  const item = readFact(project2.items, comment.workItemId);
9133
9490
  due.push({
@@ -9160,6 +9517,15 @@ var itemText = (workItemId, sequenceId, item, leadThreadId) => `${itemHeadline(s
9160
9517
  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
9518
  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
9519
  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.`;
9520
+ var leadReviewText = (workItemId, sequenceId, title, by, facts) => [
9521
+ `${by} marked ${titled(workItemId, sequenceId, title)} done. Read it with work_read_items, passing workItemIds: ["${workItemId}"], and check it now.`,
9522
+ LEAD_ACCEPT_RULE,
9523
+ LEAD_SEND_BACK_RULE,
9524
+ ...facts.idle.length === 0 ? [] : [`Nothing in progress now: ${facts.idle.join("; ")}.`],
9525
+ ...facts.queued.length === 0 ? [] : [`Waiting in line, not blocked: ${facts.queued.join("; ")}.`],
9526
+ ...facts.idle.length > 0 && facts.queued.length > 0 ? [LEAD_REBALANCE_RULE] : [],
9527
+ LEAD_NO_MEMBER_TASKS
9528
+ ].join(" ");
9163
9529
  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
9530
  function startWakeRuntime(ctx, goals, options = {}) {
9165
9531
  const wakes = ctx.derivedState({
@@ -9189,11 +9555,35 @@ function startWakeRuntime(ctx, goals, options = {}) {
9189
9555
  requestId: `team-wake:${id}`,
9190
9556
  messageId: `team-wake-msg:${id}`,
9191
9557
  memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees },
9558
+ // 评论在收件人正在跑时插进当前这一轮,不排到这一轮结束(试跑 3:一轮 97 分钟,检查点落空)。
9559
+ ...wake.kind === "comment" ? { steer: true } : {},
9192
9560
  text: async () => {
9193
9561
  if ("principalId" in wake || wake.kind === "comment")
9194
9562
  return commentText2(wake.workItemId, wake.sequenceId);
9195
9563
  const item = await readItem(wake.projectId, wake.workItemId);
9196
9564
  const title = item?.title;
9565
+ if (wake.kind === "review") {
9566
+ const project2 = state[wake.projectId];
9567
+ const members = (await goals.read(wake.projectId))?.members ?? [];
9568
+ const name = (id2) => {
9569
+ const member = findGoalMember(members, id2);
9570
+ const key = "threadId" in id2 ? id2.threadId : id2.principalId;
9571
+ return `${member?.name ?? key} (${"threadId" in id2 ? "threadId" : "principalId"} ${key})`;
9572
+ };
9573
+ const facts = project2 === void 0 ? { idle: [], queued: [] } : reviewFacts(project2, memberQueues(wake.projectId, project2));
9574
+ return leadReviewText(
9575
+ wake.workItemId,
9576
+ wake.sequenceId,
9577
+ title,
9578
+ wake.by === void 0 ? "A member" : name(wake.by),
9579
+ {
9580
+ idle: facts.idle.map((threadId) => name({ threadId })),
9581
+ queued: facts.queued.map(
9582
+ (queued) => `${workLabel(queued.workItemId, queued.sequenceId)}, assigned to ${name({ threadId: queued.threadId })}`
9583
+ )
9584
+ }
9585
+ );
9586
+ }
9197
9587
  if (wake.kind === "item")
9198
9588
  return wake.lead === true ? leadItemText(wake.workItemId, wake.sequenceId, title) : itemText(wake.workItemId, wake.sequenceId, item, wake.leadThreadId);
9199
9589
  return (wake.lead === true ? leadUnblockedText : unblockedText)(
@@ -9247,7 +9637,7 @@ var readStallExecution = async (ctx, threadId) => {
9247
9637
  };
9248
9638
  var itemLabel = (item) => `W${item.sequenceId} (workItemId ${item.workItemId})`;
9249
9639
  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)}`;
9640
+ 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
9641
  var commentInstructions = (recovery) => recovery.items.map(
9252
9642
  (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
9643
  ).join("\n");
@@ -9261,6 +9651,7 @@ var stallLeadText = (recovery, name) => `Member ${name} (threadId ${recovery.thr
9261
9651
  ).join("")}${leadInstructions()}`;
9262
9652
  function startRecoveryRuntime(ctx, goals, state, worktrees) {
9263
9653
  let pending = Promise.resolve();
9654
+ const skipped = /* @__PURE__ */ new Map();
9264
9655
  let disposed = false;
9265
9656
  const execute = async (scope2) => {
9266
9657
  if (disposed) return;
@@ -9278,6 +9669,9 @@ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9278
9669
  )
9279
9670
  );
9280
9671
  }
9672
+ for (const [key, entry] of skipped)
9673
+ if (readFact(current, entry.projectId)?.recovery?.[entry.threadId]?.pending?.turnId !== entry.turnId)
9674
+ skipped.delete(key);
9281
9675
  const recoveries = dueRecoveries(
9282
9676
  projects,
9283
9677
  scope2?.kind === "thread" ? scope2.threadId : void 0
@@ -9290,8 +9684,22 @@ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9290
9684
  const stalled = recovery.kind === "stall";
9291
9685
  const execution = stalled ? await readStallExecution(ctx, recovery.threadId) : await ctx.projections.getThreadExecution(recovery.threadId);
9292
9686
  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")))
9687
+ 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"))) {
9688
+ if (!skipped.has(recovery.key)) {
9689
+ skipped.set(recovery.key, {
9690
+ projectId: recovery.projectId,
9691
+ threadId: recovery.threadId,
9692
+ turnId: recovery.turnId
9693
+ });
9694
+ ctx.log.warn("team member recovery skipped", {
9695
+ threadId: recovery.threadId,
9696
+ turnId: recovery.turnId,
9697
+ sessionStatus: execution?.session?.status ?? null,
9698
+ latestTurnState: execution?.latestTurn?.state ?? null
9699
+ });
9700
+ }
9294
9701
  continue;
9702
+ }
9295
9703
  if ("summary" in execution) {
9296
9704
  if (execution.summary.queuePaused || execution.summary.pendingApprovalCount > 0 || execution.summary.pendingUserInputCount > 0 || execution.summary.queue.length > 0 || (execution.pendingAsyncQuestionCount ?? 0) > 0 || // 成员给人发了消息、人还没回:他在等回答,不提醒也不叫负责人。
9297
9705
  (execution.awaitingPersonReplyCount ?? 0) > 0)
@@ -9441,6 +9849,58 @@ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9441
9849
  return { ready, flush };
9442
9850
  }
9443
9851
 
9852
+ // src/runtime/leadCheckin.ts
9853
+ var LEAD_CHECKIN_REMINDER = `Your turn ended while work on this goal is still open, and no wake-up is scheduled for you. ${LEAD_SCHEDULE_CHECKIN_RULE}`;
9854
+ var OPEN_GROUPS = ["backlog", "unstarted", "started"];
9855
+ var settledThreadOf = (payload) => {
9856
+ const value = payload;
9857
+ return typeof value?.threadId === "string" && (value.session?.status === "ready" || value.session?.status === "idle") && value.session.activeTurnId === null ? value.threadId : null;
9858
+ };
9859
+ function startLeadCheckinRuntime(ctx, state, goals) {
9860
+ const check = async (threadId) => {
9861
+ const projects = await state.get();
9862
+ for (const [projectId, project2] of Object.entries(projects)) {
9863
+ if (!(project2.leadThreads ?? []).includes(threadId)) continue;
9864
+ const goal = await goals.read(projectId);
9865
+ if (goal === null) continue;
9866
+ const summary = await ctx.threads.execution.summary({ threadId, limit: 1 });
9867
+ if (summary.queue.length > 0 || summary.latestTurn?.state !== "completed") continue;
9868
+ const scheduled = await ctx.wakes.list({ threadId, state: "scheduled" });
9869
+ if (scheduled.wakes.length > 0) continue;
9870
+ const { items } = await readPages(
9871
+ Number.POSITIVE_INFINITY,
9872
+ (page) => ctx.work.items.list({ projectId, stateGroups: [...OPEN_GROUPS], ...page })
9873
+ );
9874
+ const open = items.filter(
9875
+ (item) => item.assignees.length === 0 ? item.createdBy.kind === "agent" && item.createdBy.threadId === threadId : assignedToGoalMember(goal, item.assignees)
9876
+ ).map((item) => `${item.workItemId}:${item.revision}`).sort();
9877
+ if (open.length === 0) continue;
9878
+ const fired = await ctx.wakes.list({ threadId, state: "fired" });
9879
+ const key = stableHash(`${projectId}
9880
+ ${open.join("\n")}
9881
+ fired:${fired.wakes.length}`);
9882
+ await deliverWake(ctx, {
9883
+ projectId,
9884
+ recipient: { threadId },
9885
+ requestId: `team-lead-checkin:${threadId}:${key}`,
9886
+ messageId: `team-lead-checkin-message:${threadId}:${key}`,
9887
+ text: LEAD_CHECKIN_REMINDER
9888
+ });
9889
+ }
9890
+ };
9891
+ ctx.reactor({
9892
+ name: "team-lead-checkin",
9893
+ events: ["thread.session-set"],
9894
+ handle: (event) => {
9895
+ const threadId = settledThreadOf(event.payload);
9896
+ if (threadId === null) return Promise.resolve();
9897
+ return check(threadId).catch((error) => {
9898
+ ctx.log.warn("team lead check-in reminder failed", { threadId, error });
9899
+ });
9900
+ }
9901
+ });
9902
+ }
9903
+
9444
9904
  // src/handbook/content.ts
9445
9905
  async function releaseRefs(memory, projectId, release) {
9446
9906
  return release === 0 ? [] : (await memory.handbook.releases.get({ projectId, release })).pages;
@@ -9491,6 +9951,23 @@ async function publicationRequiresAck(memory, projectId, input) {
9491
9951
  return false;
9492
9952
  }
9493
9953
 
9954
+ // src/handbook/waitingWork.ts
9955
+ async function hasUnassignedWork(items, projectId, goal) {
9956
+ const goalItemId = teamGoalItemId(projectId, goal.goalId);
9957
+ const waiting = (item) => item.workItemId !== goalItemId && !isClosed({ stateGroup: item.state.group }) && !assignedToGoalMember(goal, item.assignees);
9958
+ let cursor = null;
9959
+ do {
9960
+ const page = await items.list({
9961
+ projectId,
9962
+ limit: PAGE_SIZE,
9963
+ ...cursor === null ? {} : { cursor }
9964
+ });
9965
+ if (page.items.some(waiting)) return true;
9966
+ cursor = page.nextCursor;
9967
+ } while (cursor !== null);
9968
+ return false;
9969
+ }
9970
+
9494
9971
  // src/handbook/contracts.ts
9495
9972
  var Release = Int.check(isGreaterThanOrEqualTo3(1));
9496
9973
  var HandbookPageKind = Literals([
@@ -9549,6 +10026,10 @@ var Acknowledged = Struct({
9549
10026
  var ReminderSent = Struct({
9550
10027
  ...Acknowledged.fields
9551
10028
  });
10029
+ var LeadNotified = Struct({
10030
+ ...Acknowledged.fields,
10031
+ woke: Boolean3
10032
+ });
9552
10033
 
9553
10034
  // src/handbook/state.ts
9554
10035
  var HANDBOOK_EVENTS2 = ["plugin.event-appended", "wiki.handbook-released"];
@@ -9575,7 +10056,8 @@ var decodeFact3 = decodeUnknownOption2(
9575
10056
  envelope("handbook-publication-intended", PublicationIntent),
9576
10057
  envelope("handbook-publication-completed", PublicationCompleted),
9577
10058
  envelope("handbook-acknowledged", Acknowledged),
9578
- envelope("handbook-reminder-sent", ReminderSent)
10059
+ envelope("handbook-reminder-sent", ReminderSent),
10060
+ envelope("handbook-lead-notified", LeadNotified)
9579
10061
  ])
9580
10062
  );
9581
10063
  var decodeRelease = decodeUnknownSync(
@@ -9600,24 +10082,28 @@ var without = (rows, key) => {
9600
10082
  delete next[key];
9601
10083
  return next;
9602
10084
  };
10085
+ var queueReminders = (pending, project2, projectId, release, members) => {
10086
+ const next = { ...pending };
10087
+ for (const { memberId } of members) {
10088
+ const key = reminderKey(projectId, release, memberId);
10089
+ if (readFact(project2.reminded, key) === null)
10090
+ next[key] = { projectId, release, member: { memberId } };
10091
+ }
10092
+ return next;
10093
+ };
9603
10094
  function foldHandbook(state, event) {
9604
10095
  if (event.type === "wiki.handbook-released") {
9605
10096
  const fact2 = decodeRelease(event.payload);
9606
10097
  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
10098
  return {
9619
10099
  ...state,
9620
- pendingReminders,
10100
+ pendingReminders: fact2.requiresAck ? queueReminders(
10101
+ state.pendingReminders,
10102
+ project3,
10103
+ fact2.projectId,
10104
+ fact2.release,
10105
+ goalExecution(project3.goal).members
10106
+ ) : state.pendingReminders,
9621
10107
  projects: {
9622
10108
  ...state.projects,
9623
10109
  [fact2.projectId]: {
@@ -9634,11 +10120,24 @@ function foldHandbook(state, event) {
9634
10120
  const fact = decoded.value;
9635
10121
  const projectId = fact.name === "team/goal-started" ? fact.data.goal.projectId : fact.data.projectId;
9636
10122
  const project2 = readFact(state.projects, projectId) ?? emptyHandbookProject();
9637
- let next = project2;
9638
10123
  switch (fact.name) {
9639
- case "team/goal-started":
9640
- next = { ...project2, goal: fact.data.goal };
9641
- break;
10124
+ case "team/goal-started": {
10125
+ const goal = fact.data.goal;
10126
+ const required = project2.requiredRelease;
10127
+ return {
10128
+ ...state,
10129
+ pendingReminders: required === null ? state.pendingReminders : queueReminders(
10130
+ state.pendingReminders,
10131
+ project2,
10132
+ projectId,
10133
+ required,
10134
+ goalExecution(goal).members.filter(
10135
+ ({ memberId }) => memberId !== goal.leadMemberId && !hasAcknowledged(project2, memberId, required)
10136
+ )
10137
+ ),
10138
+ projects: { ...state.projects, [projectId]: { ...project2, goal } }
10139
+ };
10140
+ }
9642
10141
  case "team/handbook-publication-intended": {
9643
10142
  const intent = fact.data;
9644
10143
  if (readFact(state.intents, intent.requestId) !== null) return state;
@@ -9687,7 +10186,7 @@ function foldHandbook(state, event) {
9687
10186
  const { memberId, release } = fact.data;
9688
10187
  if (hasAcknowledged(project2, memberId, release)) return state;
9689
10188
  const previous = readFact(project2.acknowledgements, memberId);
9690
- next = {
10189
+ const next = {
9691
10190
  ...project2,
9692
10191
  acknowledgements: {
9693
10192
  ...project2.acknowledgements,
@@ -9697,27 +10196,40 @@ function foldHandbook(state, event) {
9697
10196
  }
9698
10197
  }
9699
10198
  };
10199
+ const key = reminderKey(projectId, release, memberId);
10200
+ const notice = project2.goal !== null && memberId !== project2.goal.leadMemberId && release === project2.requiredRelease;
9700
10201
  return {
9701
10202
  ...state,
9702
- pendingAcknowledgements: without(
9703
- state.pendingAcknowledgements,
9704
- reminderKey(projectId, release, memberId)
9705
- ),
10203
+ pendingAcknowledgements: without(state.pendingAcknowledgements, key),
10204
+ ...notice ? {
10205
+ pendingLeadNotices: {
10206
+ ...state.pendingLeadNotices,
10207
+ [key]: { version: 1, projectId, release, memberId }
10208
+ }
10209
+ } : {},
9706
10210
  projects: { ...state.projects, [projectId]: next }
9707
10211
  };
9708
10212
  }
10213
+ case "team/handbook-lead-notified": {
10214
+ const pendingLeadNotices = without(
10215
+ state.pendingLeadNotices ?? {},
10216
+ reminderKey(projectId, fact.data.release, fact.data.memberId)
10217
+ );
10218
+ return pendingLeadNotices === state.pendingLeadNotices ? state : { ...state, pendingLeadNotices };
10219
+ }
9709
10220
  case "team/handbook-reminder-sent": {
9710
10221
  const key = reminderKey(projectId, fact.data.release, fact.data.memberId);
9711
10222
  if (readFact(project2.reminded, key) !== null) return state;
9712
- next = { ...project2, reminded: { ...project2.reminded, [key]: true } };
9713
10223
  return {
9714
10224
  ...state,
9715
10225
  pendingReminders: without(state.pendingReminders, key),
9716
- projects: { ...state.projects, [projectId]: next }
10226
+ projects: {
10227
+ ...state.projects,
10228
+ [projectId]: { ...project2, reminded: { ...project2.reminded, [key]: true } }
10229
+ }
9717
10230
  };
9718
10231
  }
9719
10232
  }
9720
- return { ...state, projects: { ...state.projects, [projectId]: next } };
9721
10233
  }
9722
10234
 
9723
10235
  // src/handbook/runtime.ts
@@ -9810,20 +10322,23 @@ function startHandbookRuntime(ctx, worktrees) {
9810
10322
  };
9811
10323
  const remind = async () => {
9812
10324
  const snapshot = await state.get();
9813
- const wakeState = Object.keys(snapshot.pendingReminders).length === 0 ? void 0 : await worktrees.lane.state.get();
10325
+ const wakeState = Object.keys(snapshot.pendingReminders).length === 0 && Object.keys(snapshot.pendingLeadNotices ?? {}).length === 0 ? void 0 : await worktrees.lane.state.get();
9814
10326
  for (const [key, reminder] of Object.entries(snapshot.pendingReminders)) {
9815
10327
  const { projectId, release, member } = reminder;
10328
+ const project2 = readFact(snapshot.projects, projectId) ?? emptyHandbookProject();
10329
+ const recipient = currentRecipient(project2, member.memberId);
10330
+ if (recipient === void 0 || goalClosed(project2, wakeState?.[projectId])) continue;
9816
10331
  try {
9817
10332
  const id = stableHash(key);
9818
10333
  const worktree = memberWorktreeFor(
9819
10334
  worktrees.goals,
9820
10335
  wakeState?.[projectId],
9821
10336
  projectId,
9822
- member.recipient
10337
+ recipient
9823
10338
  );
9824
- const delivered = await deliverWake(ctx, {
10339
+ const delivered = hasAcknowledged(project2, member.memberId, release) || await deliverWake(ctx, {
9825
10340
  projectId,
9826
- recipient: member.recipient,
10341
+ recipient,
9827
10342
  requestId: `team-handbook:${id}`,
9828
10343
  messageId: `team-handbook-msg:${id}`,
9829
10344
  text: handbookMessage("reminder", release),
@@ -9846,6 +10361,39 @@ function startHandbookRuntime(ctx, worktrees) {
9846
10361
  ctx.log.warn("team handbook reminder failed", { projectId, key, error });
9847
10362
  }
9848
10363
  }
10364
+ const unassigned = /* @__PURE__ */ new Map();
10365
+ for (const [key, notice] of Object.entries(snapshot.pendingLeadNotices ?? {})) {
10366
+ const { projectId, release, memberId } = notice;
10367
+ const project2 = readFact(snapshot.projects, projectId) ?? emptyHandbookProject();
10368
+ const lead = leadThread(project2);
10369
+ try {
10370
+ const { goal } = project2;
10371
+ 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));
10372
+ if (waiting) {
10373
+ const name = (await worktrees.goals.read(projectId))?.members.find(
10374
+ (member) => member.memberId === memberId
10375
+ )?.name ?? memberId;
10376
+ const id = stableHash(`lead:${key}`);
10377
+ const delivered = await deliverWake(ctx, {
10378
+ projectId,
10379
+ recipient: { threadId: lead },
10380
+ requestId: `team-handbook-lead:${id}`,
10381
+ messageId: `team-handbook-lead-msg:${id}`,
10382
+ text: handbookMessage("leadAcknowledged", release, name)
10383
+ });
10384
+ if (!delivered) continue;
10385
+ }
10386
+ await append2(projectId, "handbook-lead-notified", `lead:${key}`, {
10387
+ version: 1,
10388
+ projectId,
10389
+ release,
10390
+ memberId,
10391
+ woke: waiting
10392
+ });
10393
+ } catch (error) {
10394
+ ctx.log.warn("team handbook lead notice failed", { projectId, key, error });
10395
+ }
10396
+ }
9849
10397
  };
9850
10398
  const execute = async () => {
9851
10399
  for (const intent of Object.values((await state.get()).pendingPublications)) {
@@ -9868,7 +10416,11 @@ function startHandbookRuntime(ctx, worktrees) {
9868
10416
  await remind();
9869
10417
  };
9870
10418
  const flush = () => serial(execute);
9871
- ctx.reactor({ name: "team-handbook", events: HANDBOOK_EVENTS2, handle: () => flush() });
10419
+ ctx.reactor({
10420
+ name: "team-handbook",
10421
+ events: [...HANDBOOK_EVENTS2, "work.item-updated"],
10422
+ handle: () => flush()
10423
+ });
9872
10424
  ctx.onDispose(async () => {
9873
10425
  disposed = true;
9874
10426
  await pending;
@@ -9973,9 +10525,12 @@ function startHandbookRuntime(ctx, worktrees) {
9973
10525
  const caller = actor.kind === "human" ? void 0 : actor.kind === "agent" ? { kind: "thread", threadId: actor.threadId } : actor;
9974
10526
  const member = callerMember(project2, caller);
9975
10527
  if (member !== void 0 && !hasAcknowledged(project2, member.memberId, project2.requiredRelease)) {
10528
+ const name = (await worktrees.goals.read(input.projectId))?.members.find(
10529
+ (row) => row.memberId === member.memberId
10530
+ )?.name ?? member.memberId;
9976
10531
  return {
9977
10532
  allowed: false,
9978
- detail: handbookMessage("guard", project2.requiredRelease)
10533
+ detail: handbookMessage("guard", project2.requiredRelease, name)
9979
10534
  };
9980
10535
  }
9981
10536
  }
@@ -9983,6 +10538,13 @@ function startHandbookRuntime(ctx, worktrees) {
9983
10538
  }
9984
10539
  };
9985
10540
  }
10541
+ var currentRecipient = (project2, memberId) => {
10542
+ const member = goalExecution(project2.goal).members.find((row) => row.memberId === memberId);
10543
+ if (member === void 0) return void 0;
10544
+ return "threadId" in member ? { threadId: member.threadId } : { principalId: member.principalId };
10545
+ };
10546
+ var leadThread = (project2) => project2.goal?.threads.find((thread) => thread.memberId === project2.goal?.leadMemberId)?.threadId;
10547
+ var goalClosed = (project2, wake) => project2.goal !== null && wake !== void 0 && isClosed(readFact(wake.items, teamGoalItemId(project2.goal.projectId, project2.goal.goalId)));
9986
10548
 
9987
10549
  // src/handbook/entry.ts
9988
10550
  var decodeAcknowledgement = decodeUnknownSync(AcknowledgeInput);
@@ -10152,16 +10714,17 @@ function registerHandbookPanel(ctx, runtime) {
10152
10714
  // src/index.ts
10153
10715
  var index_default = definePlugin({
10154
10716
  id: "team",
10155
- apiVersion: 32,
10717
+ apiVersion: 34,
10156
10718
  activate(ctx) {
10157
10719
  const rosters = createRosterService(ctx);
10158
10720
  registerRosterCommands(ctx, rosters);
10159
10721
  const goals = createGoalService(ctx, rosters);
10160
10722
  registerGoalCommands(ctx, goals);
10161
- registerBoardCommands(ctx, goals, rosters);
10162
- registerDetailCommands(ctx, goals);
10163
10723
  const wakes = startWakeRuntime(ctx, goals);
10724
+ registerBoardCommands(ctx, goals, rosters, wakes.state);
10725
+ registerDetailCommands(ctx, goals);
10164
10726
  startRecoveryRuntime(ctx, goals, wakes.state, wakes.worktrees);
10727
+ startLeadCheckinRuntime(ctx, wakes.state, goals);
10165
10728
  const handbook = startHandbookRuntime(ctx, { lane: wakes.worktrees, goals });
10166
10729
  registerHandbook(ctx, handbook);
10167
10730
  registerHandbookPanel(ctx, handbook);