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