@ycodium-ai/plugin-team 0.2.3252 → 0.2.3254

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.
Files changed (2) hide show
  1. package/dist/index.js +201 -39
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -7211,6 +7211,7 @@ function leadBrief(goal, roster) {
7211
7211
  "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."
7212
7212
  ],
7213
7213
  "Use work_list_items to check progress. When all work is complete, summarize the results here in one or two sentences.",
7214
+ "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.",
7214
7215
  PERSON_MESSAGE_RULE,
7215
7216
  "If the work item tools are unavailable, tell the user directly that this project needs the board and project memory switches turned on first."
7216
7217
  ].join("\n\n");
@@ -8190,9 +8191,74 @@ function createRosterService(ctx, options = {}) {
8190
8191
  };
8191
8192
  }
8192
8193
 
8194
+ // src/runtime/wakeQueries.ts
8195
+ function memberWorktreeFor(goals, project2, projectId, recipient) {
8196
+ if (!("threadId" in recipient) || project2 === void 0 || !project2.threads.includes(recipient.threadId) || project2.leadThreads?.includes(recipient.threadId))
8197
+ return;
8198
+ const { threadId } = recipient;
8199
+ const leadThreadId = project2.leadThreads?.[0];
8200
+ if (leadThreadId === void 0) return;
8201
+ return {
8202
+ leadThreadId,
8203
+ failure: project2.worktreeFailures?.[threadId],
8204
+ // 只在要建树或报失败时读目标取名字,叫醒的热路径不多读。
8205
+ name: async () => (await goals.read(projectId))?.members.find((member) => member.threadId === threadId)?.name ?? project2.memberIds?.[threadId] ?? threadId
8206
+ };
8207
+ }
8208
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8209
+ var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
8210
+ var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
8211
+ var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8212
+ (blocker) => !isClosed(readFact(project2.items, blocker))
8213
+ );
8214
+ var isAssignedOpen = (project2, workItemId, threadId) => {
8215
+ const item = readFact(project2.items, workItemId);
8216
+ return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
8217
+ };
8218
+ var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
8219
+ var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
8220
+ var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8221
+ var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
8222
+ var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
8223
+ function countDeferredWakes(project2, threadId) {
8224
+ let count = 0;
8225
+ for (const workItemId in project2.items) {
8226
+ const item = readFact(project2.items, workItemId);
8227
+ if (item !== null && isDeferredWake(project2, item, threadId)) count++;
8228
+ }
8229
+ return count;
8230
+ }
8231
+
8193
8232
  // src/runtime/deliver.ts
8194
8233
  import * as NodeCrypto from "node:crypto";
8195
8234
  var stableHash = (key) => NodeCrypto.createHash("sha256").update(key).digest("hex").slice(0, 32);
8235
+ var memberWorktreeBranch = (name, threadId) => {
8236
+ const slug = Array.from(
8237
+ name.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-")
8238
+ ).slice(0, 32).join("").replace(/^-+|-+$/g, "") || "member";
8239
+ return `team/${slug}-${stableHash(threadId).slice(0, 16)}`;
8240
+ };
8241
+ var createMemberWorktreeLane = (state, events) => {
8242
+ const tails = /* @__PURE__ */ new Map();
8243
+ return {
8244
+ state,
8245
+ events,
8246
+ serialize: (threadId, run3) => {
8247
+ const next = (tails.get(threadId) ?? Promise.resolve()).then(run3);
8248
+ const tail = next.then(
8249
+ () => void 0,
8250
+ () => void 0
8251
+ );
8252
+ tails.set(threadId, tail);
8253
+ void tail.then(() => {
8254
+ if (tails.get(threadId) === tail) tails.delete(threadId);
8255
+ });
8256
+ return next;
8257
+ }
8258
+ };
8259
+ };
8260
+ var shortReason = (error) => (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim().slice(0, 240) || "Worktree preparation failed";
8261
+ var FALLBACK_SUFFIX = ":worktree-fallback";
8196
8262
  async function deliverWake(ctx, input) {
8197
8263
  const { recipient } = input;
8198
8264
  if ("principalId" in recipient) {
@@ -8206,46 +8272,93 @@ async function deliverWake(ctx, input) {
8206
8272
  } else {
8207
8273
  const execution = await ctx.projections.getThreadExecution(recipient.threadId);
8208
8274
  if (execution === null || execution.archivedAt !== null) return false;
8209
- await ctx.threads.turns.start({
8210
- requestId: input.requestId,
8211
- threadId: recipient.threadId,
8275
+ const { threadId } = recipient;
8276
+ const text = typeof input.text === "string" ? input.text : await input.text();
8277
+ const worktree = input.memberWorktree;
8278
+ const turn = {
8279
+ threadId,
8212
8280
  message: {
8213
8281
  messageId: input.messageId,
8214
8282
  role: "user",
8215
- text: typeof input.text === "string" ? input.text : await input.text(),
8283
+ text,
8216
8284
  attachments: []
8217
8285
  },
8218
8286
  modelSelection: execution.modelSelection,
8219
8287
  runtimeMode: execution.runtimeMode,
8220
8288
  interactionMode: execution.interactionMode
8289
+ };
8290
+ const startTurn = (suffix) => ctx.threads.turns.start({
8291
+ ...turn,
8292
+ requestId: `${input.requestId}${suffix}`,
8293
+ message: { ...turn.message, messageId: `${input.messageId}${suffix}` }
8221
8294
  });
8222
- }
8223
- return true;
8224
- }
8295
+ let failure = worktree?.failure;
8296
+ let delivered = false;
8297
+ if (worktree !== void 0 && failure === void 0 && !execution.worktreePath) {
8298
+ const { lane } = worktree;
8299
+ const first = await lane.serialize(threadId, async () => {
8300
+ const current = await ctx.projections.getThreadExecution(threadId);
8301
+ if (current?.worktreePath) return { kind: "registered" };
8302
+ const recorded = (await lane.state.get())[input.projectId]?.worktreeFailures?.[threadId];
8303
+ if (recorded !== void 0) return { kind: "failed", failure: recorded };
8304
+ const branch = memberWorktreeBranch(await worktree.name(), threadId);
8305
+ try {
8306
+ await ctx.threads.turns.start({
8307
+ ...turn,
8308
+ requestId: input.requestId,
8309
+ message: {
8310
+ ...turn.message,
8311
+ text: `${text}
8225
8312
 
8226
- // src/runtime/wakeQueries.ts
8227
- var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
8228
- var compareWakeItems = (a, b) => a.sequenceId - b.sequenceId || compare(a.workItemId, b.workItemId);
8229
- var isClosed = (item) => item?.stateGroup === "completed" || item?.stateGroup === "cancelled";
8230
- var hasOpenBlocker = (project2, workItemId) => (readFact(project2.blockers, workItemId) ?? []).some(
8231
- (blocker) => !isClosed(readFact(project2.items, blocker))
8232
- );
8233
- var isAssignedOpen = (project2, workItemId, threadId) => {
8234
- const item = readFact(project2.items, workItemId);
8235
- return item !== null && item !== void 0 && !isClosed(item) && item.assigneeThreads.includes(threadId);
8236
- };
8237
- var isWorkable = (project2, workItemId) => !isClosed(readFact(project2.items, workItemId)) && !hasOpenBlocker(project2, workItemId);
8238
- var isOpenWorkFor = (project2, workItemId, threadId) => isAssignedOpen(project2, workItemId, threadId) && !hasOpenBlocker(project2, workItemId);
8239
- var isMember = (project2, recipient) => "threadId" in recipient ? project2.threads.includes(recipient.threadId) : project2.remoteMembers.includes(recipient.principalId);
8240
- var isQueuedUnclaimed = (item, threadId) => item.wakeStatus?.[threadId] === "pending" && !item.claimedBy.includes(threadId);
8241
- var isDeferredWake = (project2, item, threadId) => project2.threads.includes(threadId) && !project2.leadThreads?.includes(threadId) && item.assigneeThreads.includes(threadId) && isQueuedUnclaimed(item, threadId) && !isClosed(item);
8242
- function countDeferredWakes(project2, threadId) {
8243
- let count = 0;
8244
- for (const workItemId in project2.items) {
8245
- const item = readFact(project2.items, workItemId);
8246
- if (item !== null && isDeferredWake(project2, item, threadId)) count++;
8313
+ You are in your own newly created worktree on branch ${branch}. Install dependencies according to the project's instructions before starting work.`
8314
+ },
8315
+ bootstrap: {
8316
+ prepareWorktree: {
8317
+ // bootstrapTurnStart 将它传作 refName;gitDriver/worktrees 用 cwd 下的 HEAD 执行 git worktree add -b。
8318
+ baseBranch: "HEAD",
8319
+ branch,
8320
+ startFromOrigin: false
8321
+ }
8322
+ }
8323
+ });
8324
+ } catch (error) {
8325
+ await startTurn(FALLBACK_SUFFIX);
8326
+ const reason = shortReason(error);
8327
+ const receipt = await lane.events.append({
8328
+ commandId: `team-worktree-failed:${threadId}`,
8329
+ name: "member-worktree-failed",
8330
+ aggregate: { kind: "project", id: input.projectId },
8331
+ data: { version: 1, projectId: input.projectId, threadId, reason }
8332
+ });
8333
+ const persisted = (await lane.state.get({ throughSequence: receipt.sequence }))[input.projectId]?.worktreeFailures?.[threadId];
8334
+ return { kind: "fallback", failure: persisted ?? { reason } };
8335
+ }
8336
+ return { kind: "worktree" };
8337
+ });
8338
+ if (first.kind === "worktree") return true;
8339
+ if (first.kind !== "registered") failure = first.failure;
8340
+ delivered = first.kind === "fallback";
8341
+ }
8342
+ if (!delivered) await startTurn(failure === void 0 ? "" : FALLBACK_SUFFIX);
8343
+ if (worktree !== void 0 && failure !== void 0 && failure.reported !== true) {
8344
+ const reported = await deliverWake(ctx, {
8345
+ projectId: input.projectId,
8346
+ recipient: { threadId: worktree.leadThreadId },
8347
+ requestId: `team-worktree-report:${threadId}`,
8348
+ messageId: `team-worktree-report-msg:${threadId}`,
8349
+ text: `Could not create a worktree for member ${await worktree.name()} (threadId ${threadId}): ${failure.reason}. They will work in the project folder for now.`
8350
+ });
8351
+ if (!reported) throw new Error("Member worktree failure could not be reported to the lead");
8352
+ const receipt = await worktree.lane.events.append({
8353
+ commandId: `team-worktree-reported:${threadId}`,
8354
+ name: "member-worktree-reported",
8355
+ aggregate: { kind: "project", id: input.projectId },
8356
+ data: { version: 1, projectId: input.projectId, threadId }
8357
+ });
8358
+ await worktree.lane.state.get({ throughSequence: receipt.sequence });
8359
+ }
8247
8360
  }
8248
- return count;
8361
+ return true;
8249
8362
  }
8250
8363
 
8251
8364
  // src/runtime/recoveryState.ts
@@ -8576,6 +8689,12 @@ var MemberWokenFact = Struct({
8576
8689
  threadId: optionalKey2(String4),
8577
8690
  principalId: optionalKey2(String4)
8578
8691
  });
8692
+ var MemberWorktreeFailedFact = Struct({
8693
+ version: Literal2(1),
8694
+ projectId: String4,
8695
+ threadId: String4,
8696
+ reason: String4
8697
+ });
8579
8698
  var emptyProject = () => ({
8580
8699
  threads: [],
8581
8700
  leadThreads: [],
@@ -8607,6 +8726,20 @@ var decodeEnvelope2 = decodeUnknownOption2(
8607
8726
  pluginId: Literal2("team"),
8608
8727
  name: Literal2("team/member-woken"),
8609
8728
  data: MemberWokenFact
8729
+ }),
8730
+ Struct({
8731
+ pluginId: Literal2("team"),
8732
+ name: Literal2("team/member-worktree-failed"),
8733
+ data: MemberWorktreeFailedFact
8734
+ }),
8735
+ Struct({
8736
+ pluginId: Literal2("team"),
8737
+ name: Literal2("team/member-worktree-reported"),
8738
+ data: Struct({
8739
+ version: Literal2(1),
8740
+ projectId: String4,
8741
+ threadId: String4
8742
+ })
8610
8743
  })
8611
8744
  ])
8612
8745
  );
@@ -8727,6 +8860,20 @@ function foldWorkWakes(state, event) {
8727
8860
  }
8728
8861
  };
8729
8862
  }
8863
+ if (fact.name === "team/member-worktree-failed" || fact.name === "team/member-worktree-reported") {
8864
+ const { projectId: projectId3, threadId } = fact.data;
8865
+ const project4 = readFact(state, projectId3) ?? emptyProject();
8866
+ const previous = project4.worktreeFailures?.[threadId];
8867
+ const failure = fact.name === "team/member-worktree-failed" ? previous ?? { reason: fact.data.reason } : previous === void 0 ? void 0 : { ...previous, reported: true };
8868
+ if (failure === void 0 || failure === previous) return state;
8869
+ return {
8870
+ ...state,
8871
+ [projectId3]: {
8872
+ ...project4,
8873
+ worktreeFailures: { ...project4.worktreeFailures, [threadId]: failure }
8874
+ }
8875
+ };
8876
+ }
8730
8877
  const { projectId: projectId2, key } = fact.data;
8731
8878
  const project3 = readFact(state, projectId2) ?? emptyProject();
8732
8879
  if (project3.pendingComments.some((comment) => comment.key === key)) {
@@ -9009,7 +9156,7 @@ var leadItemText = (workItemId, sequenceId, title) => `You have ${titled(workIte
9009
9156
  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_SEND_BACK_RULE} When all of them are acceptable, mark this item done with work_update_item. ${LEAD_NO_MEMBER_TASKS}`;
9010
9157
  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.`;
9011
9158
  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.`;
9012
- function startWakeRuntime(ctx, options = {}) {
9159
+ function startWakeRuntime(ctx, goals, options = {}) {
9013
9160
  const wakes = ctx.derivedState({
9014
9161
  key: WAKE_STATE_KEY,
9015
9162
  version: WAKE_STATE_VERSION,
@@ -9017,6 +9164,7 @@ function startWakeRuntime(ctx, options = {}) {
9017
9164
  init: () => ({}),
9018
9165
  apply: foldWakes
9019
9166
  });
9167
+ const worktrees = createMemberWorktreeLane(wakes, ctx.events);
9020
9168
  const hash2 = options.hash ?? stableHash;
9021
9169
  let pending = Promise.resolve();
9022
9170
  const readItem = async (projectId, workItemId) => {
@@ -9029,11 +9177,13 @@ function startWakeRuntime(ctx, options = {}) {
9029
9177
  if (!isWakeDue(state, wake)) continue;
9030
9178
  try {
9031
9179
  const id = hash2(wake.key);
9180
+ const worktree = memberWorktreeFor(goals, state[wake.projectId], wake.projectId, wake);
9032
9181
  const delivered = await deliverWake(ctx, {
9033
9182
  projectId: wake.projectId,
9034
9183
  recipient: wake,
9035
9184
  requestId: `team-wake:${id}`,
9036
9185
  messageId: `team-wake-msg:${id}`,
9186
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees },
9037
9187
  text: async () => {
9038
9188
  if ("principalId" in wake || wake.kind === "comment")
9039
9189
  return commentText2(wake.workItemId, wake.sequenceId);
@@ -9081,7 +9231,7 @@ function startWakeRuntime(ctx, options = {}) {
9081
9231
  });
9082
9232
  const ready = flush();
9083
9233
  void ready.catch((error) => ctx.log?.warn("team wake catch-up failed", { error }));
9084
- return { flush, ready, state: wakes };
9234
+ return { flush, ready, state: wakes, worktrees };
9085
9235
  }
9086
9236
 
9087
9237
  // src/runtime/recovery.ts
@@ -9104,7 +9254,7 @@ ${commentInstructions(recovery)}`;
9104
9254
  var stallLeadText = (recovery, name) => `Member ${name} (threadId ${recovery.threadId}) ended this turn normally, but ${recovery.items.map(itemLabel).join(", ")} ${recovery.items.length === 1 ? "is" : "are"} still open. ${deferredText(recovery)}${recovery.alreadyRetried ? "They have already been reminded once to continue this work, with no progress since. " : ""}${recovery.items.filter((item) => item.waitingForReply).map(
9105
9255
  (item) => `Their last action on ${itemLabel(item)} was their own comment (commentId ${item.lastCommentId}); reply to it or arrange another member to help. `
9106
9256
  ).join("")}${leadInstructions()}`;
9107
- function startRecoveryRuntime(ctx, goals, state) {
9257
+ function startRecoveryRuntime(ctx, goals, state, worktrees) {
9108
9258
  let pending = Promise.resolve();
9109
9259
  let disposed = false;
9110
9260
  const execute = async (scope2) => {
@@ -9222,12 +9372,16 @@ function startRecoveryRuntime(ctx, goals, state) {
9222
9372
  await requestPlan(plan);
9223
9373
  }
9224
9374
  }
9375
+ const worktree = memberWorktreeFor(goals, current[recovery.projectId], recovery.projectId, {
9376
+ threadId: plan.recipientThreadId
9377
+ });
9225
9378
  const delivered = await deliverWake(ctx, {
9226
9379
  projectId: recovery.projectId,
9227
9380
  recipient: { threadId: plan.recipientThreadId },
9228
9381
  requestId: `team-recovery:${id}:${plan.action}`,
9229
9382
  messageId: `team-recovery-msg:${id}:${plan.action}`,
9230
- text: plan.text
9383
+ text: plan.text,
9384
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees }
9231
9385
  });
9232
9386
  if (!delivered) continue;
9233
9387
  const receipt = await ctx.events.append({
@@ -9572,7 +9726,7 @@ var terminalFailure = (error) => {
9572
9726
  if (!rejected && !invalid2) return null;
9573
9727
  return error instanceof Error ? error.message : "detail" in error ? String(error.detail) : String(reason);
9574
9728
  };
9575
- function startHandbookRuntime(ctx) {
9729
+ function startHandbookRuntime(ctx, worktrees) {
9576
9730
  const state = ctx.derivedState({
9577
9731
  key: "handbook",
9578
9732
  version: 5,
@@ -9651,16 +9805,24 @@ function startHandbookRuntime(ctx) {
9651
9805
  };
9652
9806
  const remind = async () => {
9653
9807
  const snapshot = await state.get();
9808
+ const wakeState = Object.keys(snapshot.pendingReminders).length === 0 ? void 0 : await worktrees.lane.state.get();
9654
9809
  for (const [key, reminder] of Object.entries(snapshot.pendingReminders)) {
9655
9810
  const { projectId, release, member } = reminder;
9656
9811
  try {
9657
9812
  const id = stableHash(key);
9813
+ const worktree = memberWorktreeFor(
9814
+ worktrees.goals,
9815
+ wakeState?.[projectId],
9816
+ projectId,
9817
+ member.recipient
9818
+ );
9658
9819
  const delivered = await deliverWake(ctx, {
9659
9820
  projectId,
9660
9821
  recipient: member.recipient,
9661
9822
  requestId: `team-handbook:${id}`,
9662
9823
  messageId: `team-handbook-msg:${id}`,
9663
- text: handbookMessage("reminder", release)
9824
+ text: handbookMessage("reminder", release),
9825
+ memberWorktree: worktree === void 0 ? void 0 : { ...worktree, lane: worktrees.lane }
9664
9826
  });
9665
9827
  if (!delivered) continue;
9666
9828
  const previousKey = JSON.stringify([release, member.memberId]);
@@ -9985,7 +10147,7 @@ function registerHandbookPanel(ctx, runtime) {
9985
10147
  // src/index.ts
9986
10148
  var index_default = definePlugin({
9987
10149
  id: "team",
9988
- apiVersion: 29,
10150
+ apiVersion: 30,
9989
10151
  activate(ctx) {
9990
10152
  const rosters = createRosterService(ctx);
9991
10153
  registerRosterCommands(ctx, rosters);
@@ -9993,9 +10155,9 @@ var index_default = definePlugin({
9993
10155
  registerGoalCommands(ctx, goals);
9994
10156
  registerBoardCommands(ctx, goals, rosters);
9995
10157
  registerDetailCommands(ctx, goals);
9996
- const wakes = startWakeRuntime(ctx);
9997
- startRecoveryRuntime(ctx, goals, wakes.state);
9998
- const handbook = startHandbookRuntime(ctx);
10158
+ const wakes = startWakeRuntime(ctx, goals);
10159
+ startRecoveryRuntime(ctx, goals, wakes.state, wakes.worktrees);
10160
+ const handbook = startHandbookRuntime(ctx, { lane: wakes.worktrees, goals });
9999
10161
  registerHandbook(ctx, handbook);
10000
10162
  registerHandbookPanel(ctx, handbook);
10001
10163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-team",
3
- "version": "0.2.3252",
3
+ "version": "0.2.3254",
4
4
  "description": "A project team roster with members, a lead, and goals.",
5
5
  "keywords": [
6
6
  "teams",
@@ -34,7 +34,7 @@
34
34
  "tailwindcss": "^4.0.0",
35
35
  "typescript": "~6.0.3",
36
36
  "vite-plus": "0.2.2",
37
- "@ycodium-ai/plugin-api": "0.2.3252"
37
+ "@ycodium-ai/plugin-api": "0.2.3254"
38
38
  },
39
39
  "ycodium": {
40
40
  "plugin": "./dist/index.js",
@@ -46,6 +46,7 @@
46
46
  "commands": [
47
47
  "thread.create",
48
48
  "thread.turn.start",
49
+ "thread.worktree.prepare",
49
50
  "thread.turn-queue.resume",
50
51
  "work.item.create",
51
52
  "work.item.update",