@wowyuarm/dsh-agent-team 0.1.4 → 0.1.6

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 (77) hide show
  1. package/README.md +15 -1
  2. package/README.zh.md +20 -3
  3. package/package.json +7 -2
  4. package/packages/agent-team/README.md +6 -0
  5. package/packages/agent-team/README.zh.md +6 -0
  6. package/packages/agent-team/core-skills/member-skill-manager/SKILL.md +41 -0
  7. package/packages/agent-team/core-skills/member-skill-manager/references/auth-and-config.md +30 -0
  8. package/packages/agent-team/core-skills/member-skill-manager/references/writing-great-skills.md +17 -0
  9. package/packages/agent-team/lib/auto-compaction.js +39 -1
  10. package/packages/agent-team/lib/index.js +340 -7
  11. package/packages/agent-team/lib/ledger.js +219 -7
  12. package/packages/agent-team/lib/member-context.js +2 -2
  13. package/packages/agent-team/lib/member-skills.js +94 -0
  14. package/packages/agent-team/lib/spec.js +38 -0
  15. package/packages/agent-team/lib/typert.host.js +202 -27
  16. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
  17. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  18. package/packages/agent-team/lib/typert.remote-client.js +155 -22
  19. package/packages/agent-team/lib/types/auto-compaction.d.ts +9 -0
  20. package/packages/agent-team/lib/types/auto-compaction.d.ts.map +1 -1
  21. package/packages/agent-team/lib/types/index.d.ts +81 -1
  22. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  23. package/packages/agent-team/lib/types/ledger.d.ts +49 -1
  24. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/member-skills.d.ts +45 -0
  26. package/packages/agent-team/lib/types/member-skills.d.ts.map +1 -0
  27. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  28. package/packages/agent-team/lib/types/types.d.ts +127 -2
  29. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  30. package/packages/agent-team/preset/team-member/agent.cordis.yml +7 -4
  31. package/packages/client-agent-team/README.md +1 -1
  32. package/packages/client-agent-team/README.zh.md +1 -1
  33. package/packages/client-agent-team/lib/client.js +677 -363
  34. package/packages/client-agent-team/lib/client.js.map +1 -1
  35. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +2 -4
  36. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  37. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +48 -24
  38. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +20 -0
  40. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +3 -1
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamComposer.js +73 -17
  44. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +4 -8
  45. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +7 -40
  47. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.d.ts.map +1 -1
  48. package/packages/client-agent-team/lib/types/client/TeamMemberSessionComposer.js +16 -1
  49. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamMessage.js +48 -18
  51. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts +3 -2
  52. package/packages/client-agent-team/lib/types/client/TeamRowMenu.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts +5 -2
  54. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/TeamSidebarSection.js +2 -4
  56. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +82 -22
  58. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +4 -2
  61. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/index.js +15 -13
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts +20 -16
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/locales.js +20 -16
  66. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts +16 -0
  67. package/packages/client-agent-team/lib/types/client/sidebar-sections.d.ts.map +1 -0
  68. package/packages/client-agent-team/lib/types/client/sidebar-sections.js +102 -0
  69. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -1
  70. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/team-formatters.js +9 -2
  73. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts +5 -7
  74. package/packages/client-agent-team/lib/types/client/timeline-scroll.d.ts.map +1 -1
  75. package/packages/client-agent-team/lib/types/client/timeline-scroll.js +9 -22
  76. package/packages/tool-agent-team/lib/index.js +25 -5
  77. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -23,6 +23,18 @@ function emptyProjection() {
23
23
  function assertUnhandledKind(operation) {
24
24
  throw new Error(`agent-team ledger does not handle operation kind '${operation.kind}'`);
25
25
  }
26
+ /** Deep-freeze a Member capability overlay; absent stays absent. */
27
+ function freezeCapabilities(capabilities) {
28
+ if (capabilities === undefined)
29
+ return {};
30
+ const freezeAllow = (allow) => allow === undefined ? {} : { allow: Object.freeze([...allow]) };
31
+ return {
32
+ capabilities: Object.freeze({
33
+ ...(capabilities.tools === undefined ? {} : { tools: Object.freeze(freezeAllow(capabilities.tools.allow)) }),
34
+ ...(capabilities.skills === undefined ? {} : { skills: Object.freeze(freezeAllow(capabilities.skills.allow)) }),
35
+ }),
36
+ };
37
+ }
26
38
  /** Replay and append logic behind the Agent Team service interface. */
27
39
  export class AgentTeamLedger {
28
40
  table;
@@ -141,9 +153,11 @@ export class AgentTeamLedger {
141
153
  this.requireChannel(request.workspaceId, channelRef);
142
154
  this.assertHandleAvailable(request.workspaceId, handle);
143
155
  this.assertModelSelection(request.member.model);
156
+ this.assertCapabilities(request.member.capabilities);
144
157
  const member = Object.freeze({
145
158
  ...request.member, handle, description, presetId, state: 'enabled',
146
159
  ...(request.member.model === undefined ? {} : { model: Object.freeze({ ...request.member.model }) }),
160
+ ...freezeCapabilities(request.member.capabilities),
147
161
  });
148
162
  const operation = Object.freeze({
149
163
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-added',
@@ -173,12 +187,15 @@ export class AgentTeamLedger {
173
187
  if (handle !== prior.handle)
174
188
  this.assertHandleAvailable(prior.workspaceId, handle, prior.memberId);
175
189
  this.assertModelSelection(request.model);
176
- // An absent model must CLEAR any override (inherit the Host default);
177
- // spreading `prior` verbatim would silently keep the pinned selection.
178
- const { model: _priorModel, ...priorWithoutModel } = prior;
190
+ this.assertCapabilities(request.capabilities);
191
+ // An absent model or capabilities field must CLEAR any override
192
+ // (inherit the Host default / full standard capability surface);
193
+ // spreading `prior` verbatim would silently keep the pinned value.
194
+ const { model: _priorModel, capabilities: _priorCapabilities, ...priorWithoutOverlays } = prior;
179
195
  const member = Object.freeze({
180
- ...priorWithoutModel, handle, description,
196
+ ...priorWithoutOverlays, handle, description,
181
197
  ...(request.model === undefined ? {} : { model: Object.freeze({ ...request.model }) }),
198
+ ...freezeCapabilities(request.capabilities),
182
199
  });
183
200
  const operation = Object.freeze({
184
201
  ...this.operationBase(request, this.nextSequence()), kind: 'team/member-updated',
@@ -195,6 +212,35 @@ export class AgentTeamLedger {
195
212
  resumeMember(request) {
196
213
  return this.setMemberState(request, 'enabled');
197
214
  }
215
+ /**
216
+ * Move one enabled Member onto a fresh Session: the durable operation
217
+ * records the sessionId transition while identity, memory, and binding
218
+ * survive. The Host disposes the old handle and archives the previous
219
+ * Session log around this write, so the next turn starts empty.
220
+ */
221
+ renewMemberSession(request) {
222
+ return this.enqueue(async () => {
223
+ const existing = this.state.byRequest.get(request.requestId);
224
+ if (existing !== undefined) {
225
+ this.assertSameMemberSessionRenewed(existing, request);
226
+ return this.resolved(this.memberResult(existing));
227
+ }
228
+ this.assertHumanActor(request.actor);
229
+ const prior = this.requireMember(request.memberId);
230
+ if (prior.state !== 'enabled')
231
+ throw new Error(`Agent Member '${prior.handle}' is ${prior.state}; only enabled Members can start from a new context`);
232
+ if (prior.sessionId === request.sessionId)
233
+ throw new Error(`Agent Member '${prior.handle}' already runs Session '${request.sessionId}'`);
234
+ const member = Object.freeze({ ...prior, sessionId: request.sessionId });
235
+ const operation = Object.freeze({
236
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/member-session-renewed',
237
+ data: Object.freeze({ member, previousSessionId: prior.sessionId }),
238
+ });
239
+ await this.table.put(operation.operationId, operation);
240
+ this.apply(operation);
241
+ return this.committed(this.memberResult(operation));
242
+ });
243
+ }
198
244
  getMember(memberId) {
199
245
  return this.state.members.get(memberId);
200
246
  }
@@ -481,8 +527,10 @@ export class AgentTeamLedger {
481
527
  // Acceptance normally waits for every Claim to finish (in_review);
482
528
  // a Human may also accept early while work is in progress, which then
483
529
  // completes the still-active Claims inside the same atomic operation.
484
- if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress') {
485
- throw new Error(`Task '${task.taskRef}' must be in_review or in_progress before acceptance`);
530
+ // A never-claimed todo Task may be accepted directly as well: work
531
+ // finished outside the ledger is declared complete by the Human.
532
+ if (request.action === 'accept' && task.status !== 'in_review' && task.status !== 'in_progress' && task.status !== 'todo') {
533
+ throw new Error(`Task '${task.taskRef}' must be in_review, in_progress, or todo before acceptance`);
486
534
  }
487
535
  const priorActiveClaims = [...this.state.claims.values()].filter(claim => claim.taskRef === task.taskRef && claim.state === 'active');
488
536
  if (request.action === 'accept' && task.status === 'in_progress' && priorActiveClaims.length === 0) {
@@ -592,6 +640,74 @@ export class AgentTeamLedger {
592
640
  return this.committed(this.attentionResult(operation));
593
641
  });
594
642
  }
643
+ /**
644
+ * Append one Member-to-Member direct message as an audit-only operation.
645
+ * A DM is pure delivery: no Channel, Thread, revision, attention, or
646
+ * markers change, so apply() is a marker and no projection state moves.
647
+ */
648
+ sendDm(request) {
649
+ return this.enqueue(async () => {
650
+ const existing = this.state.byRequest.get(request.requestId);
651
+ if (existing !== undefined) {
652
+ this.assertSameDm(existing, request);
653
+ return this.resolved(this.dmResult(existing));
654
+ }
655
+ // Only an enabled Member in this Workspace may send a DM; the Human is
656
+ // not a sendable peer, and Members in other Workspaces are unreachable.
657
+ const sender = this.assertActorForWorkspace(request.actor, request.workspaceId);
658
+ if (sender.kind !== 'member')
659
+ throw new Error('agent-team DM requires Member authority');
660
+ const body = request.body.trim();
661
+ if (body === '')
662
+ throw new Error('DM body must not be empty');
663
+ const recipient = this.requireMember(request.recipientMemberId);
664
+ if (recipient.workspaceId !== request.workspaceId)
665
+ throw new Error(`Agent Member '${recipient.memberId}' is not in Workspace '${request.workspaceId}'`);
666
+ if (recipient.state !== 'enabled')
667
+ throw new Error(`Agent Member '${recipient.memberId}' is ${recipient.state}; DM delivery requires an enabled Member`);
668
+ if (recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID || !recipient.sessionId)
669
+ throw new Error('DM recipient must be an Agent Member');
670
+ if (recipient.memberId === sender.memberId)
671
+ throw new Error('Members cannot DM themselves');
672
+ const operation = Object.freeze({
673
+ ...this.operationBase(request, this.nextSequence()), kind: 'team/dm-sent',
674
+ data: Object.freeze({ workspaceId: request.workspaceId, senderMemberId: sender.memberId,
675
+ recipientMemberId: recipient.memberId, body }),
676
+ });
677
+ await this.table.put(operation.operationId, operation);
678
+ this.apply(operation);
679
+ return this.committed(this.dmResult(operation));
680
+ });
681
+ }
682
+ /**
683
+ * Bounded adjacent context for a DM relay: the truncated body of the most
684
+ * recent earlier DM between the two sessions, newest first. Returns
685
+ * undefined when this is their first exchange. `excluding` skips the
686
+ * operation id of the DM being delivered right now, so the context line is
687
+ * the adjacent prior exchange rather than a self-reference. Direction
688
+ * labels are from the reader's perspective: the reader is the recipient of
689
+ * the DM being delivered, so messages the reader sent are `you → them`.
690
+ */
691
+ dmHistoryBetween(senderSessionId, recipientMemberId, excluding) {
692
+ const sender = [...this.state.members.values()].find(member => member.sessionId === senderSessionId);
693
+ if (sender === undefined)
694
+ return undefined;
695
+ for (let index = this.state.ordered.length - 1; index >= 0; index -= 1) {
696
+ const operation = this.state.ordered[index];
697
+ if (operation.kind !== 'team/dm-sent' || operation.operationId === excluding)
698
+ continue;
699
+ const pair = operation.data.senderMemberId === sender.memberId && operation.data.recipientMemberId === recipientMemberId;
700
+ const mirror = operation.data.senderMemberId === recipientMemberId && operation.data.recipientMemberId === sender.memberId;
701
+ if (!pair && !mirror)
702
+ continue;
703
+ // pair: the other Member sent it to the reader → (them → you);
704
+ // mirror: the reader sent it to the other Member → (you → them).
705
+ const direction = pair ? 'them → you' : 'you → them';
706
+ const truncated = operation.data.body.length > 160 ? `${operation.data.body.slice(0, 160)}…` : operation.data.body;
707
+ return `(${direction}) ${truncated}`;
708
+ }
709
+ return undefined;
710
+ }
595
711
  attentionStatus(actor, request) {
596
712
  const authorized = this.assertActorForWorkspace(actor, request.workspaceId);
597
713
  const { task, thread } = this.threadContextForActor(authorized, request.workspaceId, request);
@@ -860,6 +976,8 @@ export class AgentTeamLedger {
860
976
  case 'team/member-suspended':
861
977
  case 'team/member-resumed':
862
978
  case 'team/member-session-restarted':
979
+ case 'team/member-context-cleared':
980
+ case 'team/member-session-renewed':
863
981
  case 'team/member-updated':
864
982
  case 'team/member-removed':
865
983
  return [{ kind: 'workspace', workspaceId: operation.data.member.workspaceId }];
@@ -896,6 +1014,10 @@ export class AgentTeamLedger {
896
1014
  // A read advances only the reader's private watermark; no projection
897
1015
  // visible to other participants changes, so nobody is woken.
898
1016
  return [];
1017
+ case 'team/dm-sent':
1018
+ // A DM changes no shared projection: delivery is a session-level
1019
+ // runtime effect, so no change waiter has anything to refetch.
1020
+ return [];
899
1021
  default:
900
1022
  return assertUnhandledKind(operation);
901
1023
  }
@@ -949,9 +1071,12 @@ export class AgentTeamLedger {
949
1071
  case 'team/member-suspended':
950
1072
  case 'team/member-resumed':
951
1073
  case 'team/member-session-restarted':
1074
+ case 'team/member-context-cleared':
1075
+ case 'team/member-session-renewed':
952
1076
  case 'team/member-updated':
953
1077
  case 'team/channel-member-added':
954
1078
  case 'team/thread-read':
1079
+ case 'team/dm-sent':
955
1080
  return [];
956
1081
  default:
957
1082
  return assertUnhandledKind(operation);
@@ -1058,6 +1183,28 @@ export class AgentTeamLedger {
1058
1183
  throw new Error('invalid Member restart');
1059
1184
  return;
1060
1185
  }
1186
+ if (operation.kind === 'team/member-context-cleared') {
1187
+ assertHuman();
1188
+ const prior = projection.members.get(operation.data.member.memberId);
1189
+ // Legacy in-place clear: the recorded Member is unchanged, and the
1190
+ // projection deliberately does not change either.
1191
+ if (prior === undefined || prior.state !== 'enabled' || !this.sameMemberIdentity(prior, operation.data.member))
1192
+ throw new Error('invalid Member context clear');
1193
+ return;
1194
+ }
1195
+ if (operation.kind === 'team/member-session-renewed') {
1196
+ assertHuman();
1197
+ const prior = projection.members.get(operation.data.member.memberId);
1198
+ // The renewal moves exactly the sessionId: the prior Member must be
1199
+ // enabled on the recorded previous Session, and every other fact must
1200
+ // carry over unchanged.
1201
+ if (prior === undefined || prior.state !== 'enabled'
1202
+ || prior.sessionId !== operation.data.previousSessionId
1203
+ || operation.data.member.sessionId === operation.data.previousSessionId
1204
+ || !this.sameMemberFacts(prior, operation.data.member))
1205
+ throw new Error('invalid Member session renewal');
1206
+ return;
1207
+ }
1061
1208
  if (operation.kind === 'team/channel-updated') {
1062
1209
  assertHuman();
1063
1210
  const prior = projection.channels.get(operation.data.channel.channelRef);
@@ -1305,6 +1452,18 @@ export class AgentTeamLedger {
1305
1452
  this.validateInboxDelta(operation.data.inbox, projection, refs, [], [], [activity]);
1306
1453
  return;
1307
1454
  }
1455
+ if (operation.kind === 'team/dm-sent') {
1456
+ const sender = assertMember();
1457
+ const recipient = projection.members.get(operation.data.recipientMemberId);
1458
+ if (recipient === undefined || recipient.workspaceId !== operation.data.workspaceId
1459
+ || recipient.state !== 'enabled' || recipient.memberId === AGENT_TEAM_HUMAN_MEMBER_ID
1460
+ || operation.data.senderMemberId !== sender.memberId
1461
+ || operation.data.recipientMemberId === sender.memberId
1462
+ || operation.data.body.trim() === '' || operation.data.body !== operation.data.body.trim()) {
1463
+ throw new Error('invalid DM operation');
1464
+ }
1465
+ return;
1466
+ }
1308
1467
  assertUnhandledKind(operation);
1309
1468
  }
1310
1469
  validateInboxDelta(delta, projection, _refs, additionalThreadRefs = [], additionalMessages = [], additionalActivities = []) {
@@ -1486,6 +1645,15 @@ export class AgentTeamLedger {
1486
1645
  // projection deliberately does not change.
1487
1646
  return;
1488
1647
  }
1648
+ if (operation.kind === 'team/member-context-cleared') {
1649
+ // Legacy audit-only marker: identity, memory path, and binding were
1650
+ // untouched; the Host recreated the live Session empty under the same id.
1651
+ return;
1652
+ }
1653
+ if (operation.kind === 'team/member-session-renewed') {
1654
+ target.members.set(operation.data.member.memberId, operation.data.member);
1655
+ return;
1656
+ }
1489
1657
  if (operation.kind === 'team/channel-updated') {
1490
1658
  target.channels.set(operation.data.channel.channelRef, operation.data.channel);
1491
1659
  return;
@@ -1567,6 +1735,11 @@ export class AgentTeamLedger {
1567
1735
  this.applyInboxDelta(target, operation.data.inbox);
1568
1736
  return;
1569
1737
  }
1738
+ if (operation.kind === 'team/dm-sent') {
1739
+ // Audit-only: delivery is a transient runtime effect, so the durable
1740
+ // projection deliberately does not change.
1741
+ return;
1742
+ }
1570
1743
  assertUnhandledKind(operation);
1571
1744
  }
1572
1745
  /** Facts arrive in ledger sequence order, so global and per-thread lists stay sorted by append only. */
@@ -2140,6 +2313,20 @@ export class AgentTeamLedger {
2140
2313
  if (model.provider.trim() === '' || model.model.trim() === '')
2141
2314
  throw new Error('member model selection must name a provider route and a model id');
2142
2315
  }
2316
+ /**
2317
+ * Capability allow-list entries are exact identifiers (skill names,
2318
+ * reserved tool names); only whitespace-only values are rejected. Unknown
2319
+ * names are intent, not errors — committing must stay replayable across
2320
+ * Harness upgrades, so divergence is derived at activation instead.
2321
+ */
2322
+ assertCapabilities(capabilities) {
2323
+ for (const surface of [capabilities?.tools?.allow, capabilities?.skills?.allow]) {
2324
+ if (surface === undefined)
2325
+ continue;
2326
+ if (surface.some(name => name.trim() === ''))
2327
+ throw new Error('capability allow-list names must not be empty');
2328
+ }
2329
+ }
2143
2330
  initialization() {
2144
2331
  const operation = this.state.ordered[0];
2145
2332
  if (operation === undefined)
@@ -2168,6 +2355,7 @@ export class AgentTeamLedger {
2168
2355
  || operation.data.member.workspaceId !== request.workspaceId || operation.data.member.handle !== request.handle.trim()
2169
2356
  || operation.data.member.description !== request.description.trim() || operation.data.member.presetId !== request.presetId.trim()
2170
2357
  || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.member.model ?? undefined)
2358
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.member.capabilities ?? undefined)
2171
2359
  || !this.sameList(operation.data.channelRefs, this.normalizeUnique(request.channelRefs, 'initial Member Channels')))
2172
2360
  this.throwRequestCollision(request.requestId);
2173
2361
  }
@@ -2176,6 +2364,11 @@ export class AgentTeamLedger {
2176
2364
  if (operation.kind !== kind || !this.sameActor(operation.actor, request.actor) || operation.data.member.memberId !== request.memberId)
2177
2365
  this.throwRequestCollision(request.requestId);
2178
2366
  }
2367
+ assertSameMemberSessionRenewed(operation, request) {
2368
+ if (operation.kind !== 'team/member-session-renewed' || !this.sameActor(operation.actor, request.actor)
2369
+ || operation.data.member.memberId !== request.memberId || operation.data.member.sessionId !== request.sessionId)
2370
+ this.throwRequestCollision(request.requestId);
2371
+ }
2179
2372
  assertSameChannelUpdate(operation, request) {
2180
2373
  if (operation.kind !== 'team/channel-updated' || !this.sameActor(operation.actor, request.actor)
2181
2374
  || operation.data.workspaceId !== request.workspaceId || operation.data.channel.channelRef !== request.channelRef
@@ -2186,7 +2379,8 @@ export class AgentTeamLedger {
2186
2379
  if (operation.kind !== 'team/member-updated' || !this.sameActor(operation.actor, request.actor)
2187
2380
  || operation.data.member.memberId !== request.memberId || operation.data.member.handle !== request.handle.trim()
2188
2381
  || operation.data.member.description !== request.description.trim()
2189
- || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined))
2382
+ || !isDeepStrictEqual(operation.data.member.model ?? undefined, request.model ?? undefined)
2383
+ || !isDeepStrictEqual(operation.data.member.capabilities ?? undefined, request.capabilities ?? undefined))
2190
2384
  this.throwRequestCollision(request.requestId);
2191
2385
  }
2192
2386
  assertSameChannelJoin(operation, request) {
@@ -2199,6 +2393,17 @@ export class AgentTeamLedger {
2199
2393
  || operation.data.workspaceId !== request.workspaceId || operation.data.channelRef !== request.channelRef || operation.data.memberId !== request.memberId)
2200
2394
  this.throwRequestCollision(request.requestId);
2201
2395
  }
2396
+ assertSameDm(operation, request) {
2397
+ if (operation.kind !== 'team/dm-sent' || !this.sameActor(operation.actor, request.actor)
2398
+ || operation.data.workspaceId !== request.workspaceId
2399
+ || operation.data.senderMemberId !== request.actor.memberId
2400
+ || operation.data.recipientMemberId !== request.recipientMemberId
2401
+ || operation.data.body !== request.body.trim())
2402
+ this.throwRequestCollision(request.requestId);
2403
+ }
2404
+ dmResult(operation) {
2405
+ return Object.freeze({ receipt: this.receipt(operation), recipient: this.requireMember(operation.data.recipientMemberId) });
2406
+ }
2202
2407
  assertSameMessage(operation, request, recipients) {
2203
2408
  if (operation.kind !== 'team/message-sent' || !this.sameActor(operation.actor, request.actor)
2204
2409
  || operation.data.workspaceId !== request.workspaceId || operation.data.message.channelRef !== request.channelRef
@@ -2282,6 +2487,13 @@ export class AgentTeamLedger {
2282
2487
  && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2283
2488
  && left.privateMemoryPath === right.privateMemoryPath;
2284
2489
  }
2490
+ /** sameMemberIdentity minus the sessionId: every durable Member fact a session renewal must carry over unchanged. */
2491
+ sameMemberFacts(left, right) {
2492
+ return left.memberId === right.memberId && left.workspaceId === right.workspaceId
2493
+ && left.handle === right.handle && left.description === right.description && left.presetId === right.presetId
2494
+ && isDeepStrictEqual(left.model ?? undefined, right.model ?? undefined)
2495
+ && left.privateMemoryPath === right.privateMemoryPath;
2496
+ }
2285
2497
  sameActor(left, right) {
2286
2498
  return left.kind === right.kind && left.memberId === right.memberId && left.handle === right.handle;
2287
2499
  }
@@ -59,10 +59,10 @@ export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-pat
59
59
  const warning = overBudget
60
60
  ? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
61
61
  : '';
62
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
62
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
63
63
  }
64
64
  function renderUnavailableMemory(privateMemoryPath, reason) {
65
- return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
65
+ return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\nPrivate memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd. Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
66
66
  }
67
67
  function escape(value) {
68
68
  return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Per-Member private skill provider: the Team-owned wrapper around the
3
+ * Harness filesystem provider that scopes one Member's skill discovery to
4
+ * exactly the plugin's bundled read-only core skills plus that Member's
5
+ * private `skills/` directory (default roots excluded), and filters `list()`
6
+ * output by that Member's selection ref.
7
+ *
8
+ * Deliberate interface reservation: this per-Member provider seam is the
9
+ * primitive future Runtime Revision manifests orchestrate — do not remove
10
+ * during cleanup.
11
+ *
12
+ * @module @wowyuarm/dsh-agent-team/member-skills
13
+ */
14
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
15
+ /**
16
+ * Register the Member-private skill provider on one Member's agent scope and
17
+ * return its disposer. The provider files into that agent's exact layer, so
18
+ * no sibling Member or ordinary session can observe the catalog; discovery,
19
+ * watching, and invalidation flow through the same Harness provider below.
20
+ *
21
+ * Registration goes through the traceable service resolved FROM the agent
22
+ * context (the same shape as `tools.restrict()`), never through a plugin
23
+ * mount: a plugin mounted from Host activation code lands on the Host's
24
+ * async-trace fiber instead of the agent scope, and a plugin `inject` would
25
+ * hold Member activation open while the Host is still starting. A deployment
26
+ * without the skill registry keeps its Members; they simply carry no private
27
+ * skill catalog.
28
+ */
29
+ export function mountMemberSkillProvider(agentCtx, config) {
30
+ const selection = config.selection;
31
+ // A no-op swap stands in for deployments without the skill registry, so
32
+ // the Host's edit path never sees a half-initialized ref.
33
+ selection.swap = () => { };
34
+ const skills = agentCtx.get('skills');
35
+ if (skills === undefined)
36
+ return () => { };
37
+ return skills.registerProvider(control => {
38
+ const provider = new MemberPrivateSkillProvider(agentCtx, control, config, selection);
39
+ // The ref swap updates the live filter and drops cached catalogs through
40
+ // the provider's own registration, so the next catalog query (and the
41
+ // next step's durable replacement catalog) sees the new selection.
42
+ selection.swap = (allow) => {
43
+ selection.current = allow;
44
+ control.invalidate();
45
+ };
46
+ return provider;
47
+ });
48
+ }
49
+ /** One Member's bundled-plus-private skill roots wrapped as a filtered registry provider. */
50
+ class MemberPrivateSkillProvider {
51
+ selection;
52
+ name;
53
+ wrapped;
54
+ constructor(ctx, control, config, selection) {
55
+ this.selection = selection;
56
+ this.name = `member-private:${config.skillsDirectory}`;
57
+ // Two Team-owned roots, no project/user/global leakage: the bundled
58
+ // read-only core skills first, then the Member's writable private
59
+ // directory. Within one provider both roots share the custom rank, and
60
+ // discovery keeps the first root's same-name candidate, so a bundled
61
+ // skill stays stable across plugin upgrades while a Member installs
62
+ // its own additions under their own names.
63
+ const customSkillDirs = config.bundledSkillsDirectory === undefined
64
+ ? [config.skillsDirectory]
65
+ : [config.bundledSkillsDirectory, config.skillsDirectory];
66
+ this.wrapped = new FileSystemSkillProvider(ctx, control, {
67
+ providerName: this.name,
68
+ includeDefaultRoots: false,
69
+ customSkillDirs,
70
+ });
71
+ }
72
+ /**
73
+ * Filter the wrapped discovery to the Member's selection. Filtering is
74
+ * list()-output only — the directory is still scanned and watched, so this
75
+ * is a performance/visibility semantic, not a security boundary.
76
+ */
77
+ async list(options) {
78
+ const output = await this.wrapped.list(options);
79
+ const allow = this.selection.current;
80
+ if (allow === undefined)
81
+ return output;
82
+ const allowed = new Set(allow);
83
+ return filterObservation(output, candidate => allowed.has(candidate.name));
84
+ }
85
+ async get(candidate, options) {
86
+ return this.wrapped.get(candidate, options);
87
+ }
88
+ }
89
+ /** Apply one name filter to either provider observation shape. */
90
+ function filterObservation(output, keep) {
91
+ if (!('candidates' in output))
92
+ return output.filter(keep);
93
+ return { candidates: output.candidates.filter(keep), complete: output.complete };
94
+ }
@@ -32,6 +32,19 @@ const modelSelectionSchema = z.object({
32
32
  model,
33
33
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
34
34
  }));
35
+ // Pure intent: allow-list entries are plain strings. They are never
36
+ // validated against a known-name set here — committing must survive Harness
37
+ // upgrades that rename or remove tools, so divergence is derived at
38
+ // activation instead (see AgentTeamCapabilityWarning).
39
+ const memberCapabilitiesSchema = z.object({
40
+ // Deliberate interface reservation, no UI writes it today: Runtime Revision
41
+ // manifests depend on this seam — do not remove during cleanup.
42
+ tools: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
43
+ skills: z.object({ allow: z.array(z.string().min(1)) }).strict().transform(omitUndefined).optional(),
44
+ }).strict().transform(({ tools, skills }) => ({
45
+ ...(tools === undefined ? {} : { tools }),
46
+ ...(skills === undefined ? {} : { skills }),
47
+ }));
35
48
  const memberSchema = z.object({
36
49
  memberId: memberIdSchema,
37
50
  sessionId: sessionIdSchema,
@@ -41,6 +54,8 @@ const memberSchema = z.object({
41
54
  presetId: z.string().min(1),
42
55
  // Ledgers written before per-Member model selection existed omit the field.
43
56
  model: modelSelectionSchema.optional(),
57
+ // Ledgers written before member capabilities existed omit the field.
58
+ capabilities: memberCapabilitiesSchema.optional(),
44
59
  privateMemoryPath: z.string().min(1),
45
60
  state: z.union([z.literal('enabled'), z.literal('suspended'), z.literal('inactive')]),
46
61
  }).strict();
@@ -237,6 +252,18 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
237
252
  kind: z.literal('team/member-session-restarted'),
238
253
  data: z.object({ member: memberSchema }).strict(),
239
254
  }).strict(),
255
+ z.object({
256
+ ...operationBase,
257
+ previousOperationId: operationIdSchema.nullable(),
258
+ kind: z.literal('team/member-context-cleared'),
259
+ data: z.object({ member: memberSchema }).strict(),
260
+ }).strict(),
261
+ z.object({
262
+ ...operationBase,
263
+ previousOperationId: operationIdSchema.nullable(),
264
+ kind: z.literal('team/member-session-renewed'),
265
+ data: z.object({ member: memberSchema, previousSessionId: sessionIdSchema }).strict(),
266
+ }).strict(),
240
267
  z.object({
241
268
  ...operationBase,
242
269
  previousOperationId: operationIdSchema.nullable(),
@@ -372,6 +399,17 @@ const storedAgentTeamOperationSchema = z.discriminatedUnion('kind', [
372
399
  inbox: inboxDeltaSchema,
373
400
  }).strict(),
374
401
  }).strict(),
402
+ z.object({
403
+ ...operationBase,
404
+ previousOperationId: operationIdSchema.nullable(),
405
+ kind: z.literal('team/dm-sent'),
406
+ data: z.object({
407
+ workspaceId: workspaceIdSchema,
408
+ senderMemberId: memberIdSchema,
409
+ recipientMemberId: memberIdSchema,
410
+ body: z.string().min(1),
411
+ }).strict(),
412
+ }).strict(),
375
413
  ]);
376
414
  /** Durable validator for the closed Agent Team operation union; ledgers written before message occurredAt existed normalize on load. */
377
415
  export const agentTeamOperationSchema = storedAgentTeamOperationSchema.transform(stampOperationMessages);