@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/assets/ui.js +10 -10
- package/dist/index.js +618 -114
- package/locales/en.json +11 -1
- package/locales/zh-CN.json +11 -1
- package/package.json +5 -2
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: "
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } : {
|
|
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
|
-
|
|
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
|
|
9405
|
+
function memberQueues(projectId, project2, woken = new Set(project2.woken), onlyThreadId) {
|
|
9074
9406
|
const busy = /* @__PURE__ */ new Set();
|
|
9075
|
-
const
|
|
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
|
|
9086
|
-
if (
|
|
9087
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9641
|
-
|
|
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
|
-
|
|
9704
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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({
|
|
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:
|
|
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);
|