@chatpanel/events 0.101.0 → 0.103.1

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 (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -348
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/team-trail.js CHANGED
@@ -1,73 +1,2 @@
1
- // A team run as a trail reads it: one line per event in the trail's own vocabulary, and
2
- // the run's lanes one per taskfolded from the same events. Both clients draw a run
3
- // from these; neither decides for itself what "task.done" means in words. The event
4
- // vocabulary is team-run.js's `emit`; the lanes are what a pane, a card or a phone renders.
5
-
6
- /** One trail line per team event, in the trail's own vocabulary. */
7
- export function teamLine(ev) {
8
- const role = ev.role ? `${ev.role}` : 'team';
9
- switch (ev.type) {
10
- case 'run.started': return { type: 'status', text: `team ${ev.team}: ${(ev.roles || []).join(', ')}` };
11
- case 'plan.ready': return { type: 'status', text: `plan: ${(ev.tasks || []).length} task${(ev.tasks || []).length === 1 ? '' : 's'} (${ev.by})` };
12
- case 'task.added': return { type: 'status', text: `${ev.kind === 'merge' ? 'the merge' : ev.title || ev.taskId} is ${role}'s${(ev.dependsOn || []).length ? ` — after ${ev.dependsOn.join(', ')}` : ''}` };
13
- case 'task.started': return { type: 'tool', name: role, text: `${role} · ${ev.title || ev.taskId}${ev.resumed ? ` (resumed, ${ev.steps} steps so far)` : ''}` };
14
- case 'task.waiting': return { type: 'status', text: `${role} is waiting on you — ${ev.text || 'a question on the board'}` };
15
- case 'run.waiting': return { type: 'status', text: `waiting on you — ${ev.text || ev.type || 'a question on the board'}` };
16
- case 'run.resumed': return { type: 'status', text: `team ${ev.team} resumed${(ev.carried || []).length ? ` (${ev.carried.length} task${ev.carried.length === 1 ? '' : 's'} carried over)` : ''}` };
17
- case 'board.post': return ev.post && ev.post.kind !== 'finding' ? { type: 'status', text: `${ev.post.by} ${ev.post.replyTo ? 'replied' : 'posted'} (${ev.post.kind}): ${String(ev.post.text || '').slice(0, 120)}` } : null;
18
- case 'board.decision': return { type: 'status', text: `${ev.by || 'someone'} ${ev.status} a post` };
19
- case 'board.thread-removed': return { type: 'status', text: `${ev.by || 'someone'} removed a thread from the board` };
20
- case 'task.note': return { type: 'status', text: `${role}: ${ev.text}` };
21
- case 'task.handoff': return { type: 'status', text: `${role} handed off ${ev.from ? `from ${ev.from} ` : ''}to ${ev.to} by ${ev.by || 'person'}${ev.reason ? ` — ${ev.reason}` : ''}` };
22
- case 'task.step': return null;
23
- case 'task.scored': return null;
24
- // The route is the lane's business (task.model already names it); the reasons are a line
25
- // only when there are any — a re-appointment says its own.
26
- case 'task.routed': return ev.reasons?.length && ev.attempt === 1 ? { type: 'status', text: `${role} → ${ev.engine?.id || '?'}${ev.engine?.model ? `/${ev.engine.model}` : ''} (${ev.reasons.join('; ')})` } : null;
27
- case 'task.scm': return ev.commits ? { type: 'status', text: `${role} committed ${ev.commits} on ${ev.branch || 'a branch'}${ev.headAfter ? ` @ ${String(ev.headAfter).slice(0, 7)}` : ''}` } : null;
28
- case 'task.reappointed': return { type: 'status', text: `${role} → ${ev.model} (${(ev.after || []).join(', ')} unavailable${ev.error ? `: ${String(ev.error).slice(0, 120)}` : ''})` };
29
- case 'task.tool': return { type: 'tool', name: ev.name, text: `${role} ran ${ev.name}${ev.text ? ` — ${ev.text}` : ''}` };
30
- case 'task.finding': return { type: 'status', text: `${role}: ${String(ev.finding?.text || '').slice(0, 140)}` };
31
- case 'task.done': return { type: 'status', text: `${role} done · ${ev.findings || 0} finding${ev.findings === 1 ? '' : 's'}` };
32
- case 'task.failed': return { type: 'error', text: `${role} ${ev.status || 'failed'}${ev.error ? ` — ${ev.error}` : ''}` };
33
- case 'run.merging': return { type: 'status', text: `merging (${ev.policy})` };
34
- case 'run.done': return { type: 'status', text: `team ${ev.status}${ev.usage?.spent?.tokens ? ` · ${ev.usage.spent.tokens} tokens` : ''}` };
35
- // A PROJECT (project-run.js): the executive's steps, the jobs, the report — the same
36
- // strip a run's lines go to, so a person sees the loop move without opening the board.
37
- case 'project.thinking': return { type: 'status', text: `executive is ${ev.what === 'project_review' ? 'reviewing the round' : 'planning the jobs'}${ev.model ? ` (${ev.model})` : ''}` };
38
- case 'project.status': return { type: 'status', text: `project ${ev.status}${ev.by ? ` (${ev.by})` : ''}` };
39
- case 'project.decision': return ev.kind === 'answer' || ev.kind === 'status' ? null : { type: 'status', text: `${ev.by || 'executive'} · ${ev.kind}: ${String(ev.text || '').split('\n')[0].slice(0, 140)}` };
40
- case 'job.posted': return { type: 'status', text: `job posted: ${ev.job?.title || ev.job?.id || '?'}${ev.job?.needs?.skills?.length ? ` (${ev.job.needs.skills.join(', ')})` : ''}` };
41
- case 'job.updated': return ev.job?.status && ['recruited', 'done', 'failed'].includes(ev.job.status) ? { type: ev.job.status === 'failed' ? 'error' : 'status', text: `job ${ev.job.id} ${ev.job.status}${ev.job.recruited?.agentId ? ` → ${ev.job.recruited.agentId}` : ''}${ev.job.status === 'failed' && ev.job.result?.text ? ` — ${String(ev.job.result.text).slice(0, 120)}` : ''}` } : null;
42
- case 'project.report': return { type: 'status', text: `report written (${ev.by || 'executive'})` };
43
- default: return null;
44
- }
45
- }
46
-
47
- /** The run's lanes — one per task — folded from its events, for the pane. */
48
- export function teamLanes(prev, ev) {
49
- const lanes = prev ? { ...prev, tasks: { ...prev.tasks } } : { runId: ev.runId, team: '', status: 'running', tasks: {}, findings: 0 };
50
- switch (ev.type) {
51
- case 'run.started': lanes.team = ev.team; lanes.roles = ev.roles; break;
52
- case 'plan.ready': for (const t of ev.tasks || []) lanes.tasks[t.id] = { id: t.id, role: t.role, title: t.title, status: 'pending', findings: 0 }; break;
53
- case 'task.added': lanes.tasks[ev.taskId] = { id: ev.taskId, role: ev.role, title: ev.title, status: 'pending', findings: 0, ...(ev.kind ? { kind: ev.kind } : {}) }; break;
54
- case 'task.started': lanes.tasks[ev.taskId] = { ...(lanes.tasks[ev.taskId] || { id: ev.taskId, role: ev.role, title: ev.title }), status: 'running' }; break;
55
- case 'task.delta': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], text: ev.text }; break;
56
- case 'task.finding': lanes.findings += 1; if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], findings: (lanes.tasks[ev.taskId].findings || 0) + 1 }; break;
57
- case 'task.model': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], model: ev.model }; break;
58
- case 'task.routed': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], engine: ev.engine || null }; break;
59
- case 'task.scm': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], scm: { branch: ev.branch, commits: ev.commits || 0, head: ev.headAfter || ev.head } }; break;
60
- case 'task.handoff': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], model: ev.to, handoffs: (lanes.tasks[ev.taskId].handoffs || 0) + 1 }; break;
61
- case 'task.step': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], steps: (lanes.tasks[ev.taskId].steps || 0) + (ev.steps || []).length }; break;
62
- case 'task.tool': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], tools: (lanes.tasks[ev.taskId].tools || 0) + 1, lastTool: ev.text ? `${ev.name} ${ev.text}` : ev.name }; break;
63
- case 'run.usage': lanes.usage = ev.usage; break;
64
- case 'task.waiting': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], status: 'waiting', waitingOn: ev.threadId }; lanes.waiting = [...(lanes.waiting || []), ev.threadId]; break;
65
- case 'task.done': case 'task.failed': if (lanes.tasks[ev.taskId]) lanes.tasks[ev.taskId] = { ...lanes.tasks[ev.taskId], status: ev.status || 'ok', ms: ev.ms }; break;
66
- case 'board.thread-status': if (ev.status !== 'waiting' && lanes.waiting) lanes.waiting = lanes.waiting.filter((x) => x !== ev.threadId); break;
67
- case 'board.thread-removed': if (lanes.waiting) lanes.waiting = lanes.waiting.filter((x) => x !== ev.threadId); break;
68
- case 'run.waiting': lanes.waiting = [...(lanes.waiting || []), ev.threadId]; break;
69
- case 'run.done': lanes.status = ev.status; lanes.usage = ev.usage; break;
70
- default: break;
71
- }
72
- return lanes;
73
- }
1
+ function n(t){const s=t.role?`${t.role}`:"team";switch(t.type){case"run.started":return{type:"status",text:`team ${t.team}: ${(t.roles||[]).join(", ")}`};case"plan.ready":return{type:"status",text:`plan: ${(t.tasks||[]).length} task${(t.tasks||[]).length===1?"":"s"} (${t.by})`};case"task.added":return{type:"status",text:`${t.kind==="merge"?"the merge":t.title||t.taskId} is ${s}'s${(t.dependsOn||[]).length?` — after ${t.dependsOn.join(", ")}`:""}`};case"task.started":return{type:"tool",name:s,text:`${s} · ${t.title||t.taskId}${t.resumed?` (resumed, ${t.steps} steps so far)`:""}`};case"task.waiting":return{type:"status",text:`${s} is waiting on you — ${t.text||"a question on the board"}`};case"run.waiting":return{type:"status",text:`waiting on you — ${t.text||t.type||"a question on the board"}`};case"run.resumed":return{type:"status",text:`team ${t.team} resumed${(t.carried||[]).length?` (${t.carried.length} task${t.carried.length===1?"":"s"} carried over)`:""}`};case"board.post":return t.post&&t.post.kind!=="finding"?{type:"status",text:`${t.post.by} ${t.post.replyTo?"replied":"posted"} (${t.post.kind}): ${String(t.post.text||"").slice(0,120)}`}:null;case"board.decision":return{type:"status",text:`${t.by||"someone"} ${t.status} a post`};case"board.thread-removed":return{type:"status",text:`${t.by||"someone"} removed a thread from the board`};case"task.note":return{type:"status",text:`${s}: ${t.text}`};case"task.handoff":return{type:"status",text:`${s} handed off ${t.from?`from ${t.from} `:""}to ${t.to} by ${t.by||"person"}${t.reason?` — ${t.reason}`:""}`};case"task.step":return null;case"task.scored":return null;case"task.routed":return t.reasons?.length&&t.attempt===1?{type:"status",text:`${s} → ${t.engine?.id||"?"}${t.engine?.model?`/${t.engine.model}`:""} (${t.reasons.join("; ")})`}:null;case"task.scm":return t.commits?{type:"status",text:`${s} committed ${t.commits} on ${t.branch||"a branch"}${t.headAfter?` @ ${String(t.headAfter).slice(0,7)}`:""}`}:null;case"task.reappointed":return{type:"status",text:`${s} → ${t.model} (${(t.after||[]).join(", ")} unavailable${t.error?`: ${String(t.error).slice(0,120)}`:""})`};case"task.tool":return{type:"tool",name:t.name,text:`${s} ran ${t.name}${t.text?` — ${t.text}`:""}`};case"task.finding":return{type:"status",text:`${s}: ${String(t.finding?.text||"").slice(0,140)}`};case"task.done":return{type:"status",text:`${s} done · ${t.findings||0} finding${t.findings===1?"":"s"}`};case"task.failed":return{type:"error",text:`${s} ${t.status||"failed"}${t.error?` — ${t.error}`:""}`};case"run.merging":return{type:"status",text:`merging (${t.policy})`};case"run.done":return{type:"status",text:`team ${t.status}${t.usage?.spent?.tokens?` · ${t.usage.spent.tokens} tokens`:""}`};case"project.thinking":return{type:"status",text:`executive is ${t.what==="project_review"?"reviewing the round":"planning the jobs"}${t.model?` (${t.model})`:""}`};case"project.status":return{type:"status",text:`project ${t.status}${t.by?` (${t.by})`:""}`};case"project.decision":return t.kind==="answer"||t.kind==="status"?null:{type:"status",text:`${t.by||"executive"} · ${t.kind}: ${String(t.text||"").split(`
2
+ `)[0].slice(0,140)}`};case"job.posted":return{type:"status",text:`job posted: ${t.job?.title||t.job?.id||"?"}${t.job?.needs?.skills?.length?` (${t.job.needs.skills.join(", ")})`:""}`};case"job.updated":return t.job?.status&&["recruited","done","failed"].includes(t.job.status)?{type:t.job.status==="failed"?"error":"status",text:`job ${t.job.id} ${t.job.status}${t.job.recruited?.agentId?` → ${t.job.recruited.agentId}`:""}${t.job.status==="failed"&&t.job.result?.text?` ${String(t.job.result.text).slice(0,120)}`:""}`}:null;case"project.report":return{type:"status",text:`report written (${t.by||"executive"})`};default:return null}}function r(t,s){const a=t?{...t,tasks:{...t.tasks}}:{runId:s.runId,team:"",status:"running",tasks:{},findings:0};switch(s.type){case"run.started":a.team=s.team,a.roles=s.roles;break;case"plan.ready":for(const e of s.tasks||[])a.tasks[e.id]={id:e.id,role:e.role,title:e.title,status:"pending",findings:0};break;case"task.added":a.tasks[s.taskId]={id:s.taskId,role:s.role,title:s.title,status:"pending",findings:0,...s.kind?{kind:s.kind}:{}};break;case"task.started":a.tasks[s.taskId]={...a.tasks[s.taskId]||{id:s.taskId,role:s.role,title:s.title},status:"running"};break;case"task.delta":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],text:s.text});break;case"task.finding":a.findings+=1,a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],findings:(a.tasks[s.taskId].findings||0)+1});break;case"task.model":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],model:s.model});break;case"task.routed":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],engine:s.engine||null});break;case"task.scm":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],scm:{branch:s.branch,commits:s.commits||0,head:s.headAfter||s.head}});break;case"task.handoff":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],model:s.to,handoffs:(a.tasks[s.taskId].handoffs||0)+1});break;case"task.step":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],steps:(a.tasks[s.taskId].steps||0)+(s.steps||[]).length});break;case"task.tool":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],tools:(a.tasks[s.taskId].tools||0)+1,lastTool:s.text?`${s.name} ${s.text}`:s.name});break;case"run.usage":a.usage=s.usage;break;case"task.waiting":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],status:"waiting",waitingOn:s.threadId}),a.waiting=[...a.waiting||[],s.threadId];break;case"task.done":case"task.failed":a.tasks[s.taskId]&&(a.tasks[s.taskId]={...a.tasks[s.taskId],status:s.status||"ok",ms:s.ms});break;case"board.thread-status":s.status!=="waiting"&&a.waiting&&(a.waiting=a.waiting.filter(e=>e!==s.threadId));break;case"board.thread-removed":a.waiting&&(a.waiting=a.waiting.filter(e=>e!==s.threadId));break;case"run.waiting":a.waiting=[...a.waiting||[],s.threadId];break;case"run.done":a.status=s.status,a.usage=s.usage;break;default:break}return a}export{r as teamLanes,n as teamLine};
package/team-worklog.js CHANGED
@@ -1,149 +1,2 @@
1
- // The work log a task's thread as the record of the work, not just its posts.
2
- //
3
- // A task is a conversation (team-task.js): every step is on the record as it happens — the
4
- // prompt, the member's text, its reasoning when the model streams it, each tool call and
5
- // what came back — beside the attempts (which model, when, how it ended), the hand-offs, the
6
- // posts in its thread, and how the task ended. The board drew the posts alone, and a task
7
- // that failed before its first post looked like nothing had happened. This folds all of it
8
- // into ONE ordered timeline, the same in both clients, so what a member did, tried, was told
9
- // and produced is read in one place — by a person, by the next wave, and by whoever rates
10
- // the member (evidence first: calls made, findings kept, models burned, budget spent).
11
- //
12
- // Pure: a run record (team-record.js) in, entries out. No rendering here.
13
-
14
- import { isThought } from './team-task.js';
15
-
16
- export const WORKLOG_KINDS = Object.freeze(['attempt', 'prompt', 'note', 'thought', 'text', 'call', 'result', 'handoff', 'post', 'end']);
17
-
18
- const num = (v, d = 0) => (Number.isFinite(v) ? v : d);
19
- const str = (v) => (v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v));
20
-
21
- function parseArgs(raw) {
22
- if (raw && typeof raw === 'object') return raw;
23
- try { return JSON.parse(String(raw || '{}')); } catch { return { raw: String(raw || '') }; }
24
- }
25
-
26
- /** One line for a tool call: `find web_search "…"` — the dispatcher's action stands in front. */
27
- export function describeCall(call) {
28
- const name = call?.function?.name || call?.name || 'tool';
29
- const args = parseArgs(call?.function?.arguments ?? call?.arguments);
30
- const action = args?.action ? ` ${args.action}` : '';
31
- const inner = args?.args && typeof args.args === 'object' ? args.args : args;
32
- const first = inner && typeof inner === 'object' ? Object.entries(inner).find(([k, v]) => k !== 'action' && k !== 'args' && (typeof v === 'string' || typeof v === 'number')) : null;
33
- return `${name}${action}${first ? ` ${first[0]}=${JSON.stringify(String(first[1]).slice(0, 120))}` : ''}`;
34
- }
35
-
36
- /**
37
- * The timeline of one task: `[{ id, kind, at, by, attempt, ... }]`, oldest first. Every entry has
38
- * an `at`; a step recorded without one (an older build) inherits its attempt's, so the order
39
- * still holds. `by` is the role for what the member did, `runner` for the runner's lines, and
40
- * whoever posted for a post. `id` is STABLE across polls — the step's index on the record, the
41
- * post's id, the attempt's number — so a board can keep the row a person opened while the
42
- * log grows underneath it (a row keyed by its position remounted on every new step, and the
43
- * fold a reader had opened snapped shut every four seconds).
44
- */
45
- export function workLogFor(run, taskId) {
46
- const task = (run?.tasks || []).find((t) => t.id === taskId);
47
- if (!task) return [];
48
- const by = task.role || task.id;
49
- const out = [];
50
- const attempts = Array.isArray(task.attempts) ? task.attempts : [];
51
- const baseAt = num(task.startedAt, num(attempts[0]?.at, num(run?.startedAt, 0)));
52
- attempts.forEach((a, i) => out.push({ id: `attempt:${i + 1}`, kind: 'attempt', at: num(a.at, baseAt + i), by: 'runner', attempt: i + 1, model: a.label || a.engine?.label || a.engine?.model || a.model || '', modelId: a.model || '', engine: a.engine || null, continued: !!a.continued, status: a.status || null, error: a.error || null }));
53
-
54
- // Steps: stamped ones sort by their time; unstamped ones follow their attempt in order.
55
- const steps = Array.isArray(task.transcript) ? task.transcript : [];
56
- const calls = new Map();
57
- let lastAt = baseAt;
58
- let attemptOf = 1;
59
- steps.forEach((m, i) => {
60
- if (!m || !m.role) return;
61
- if (Number.isFinite(m.attempt)) attemptOf = m.attempt;
62
- else if (attempts.length) attemptOf = Math.max(attemptOf, 1 + attempts.findLastIndex((x) => num(x.at, 0) <= lastAt + 1));
63
- // Unstamped: after everything before it, and after its attempt began.
64
- const at = Number.isFinite(m.at) ? m.at : Math.max(lastAt, num(attempts[attemptOf - 1]?.at, 0)) + 1;
65
- lastAt = Math.max(lastAt, at);
66
- const attempt = attemptOf;
67
- if (m.role === 'user') {
68
- out.push({ id: `step:${i}`, kind: i === 0 ? 'prompt' : 'note', at, by: 'runner', attempt, text: str(m.content) });
69
- } else if (m.role === 'assistant') {
70
- if (isThought(m)) { out.push({ id: `step:${i}`, kind: 'thought', at, by, attempt, text: m.thought }); return; }
71
- if (typeof m.content === 'string' && m.content.trim()) out.push({ id: `step:${i}`, kind: 'text', at, by, attempt, text: m.content });
72
- for (const c of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
73
- const id = c.id || `c${i}`;
74
- calls.set(id, c);
75
- out.push({ id: `call:${id}`, kind: 'call', at, by, attempt, callId: id, name: c.function?.name || 'tool', args: parseArgs(c.function?.arguments), text: describeCall(c) });
76
- }
77
- } else if (m.role === 'tool') {
78
- const c = m.tool_call_id ? calls.get(m.tool_call_id) : null;
79
- out.push({ id: `step:${i}`, kind: 'result', at, by: 'tool', attempt, callId: m.tool_call_id || null, name: c?.function?.name || m.name || 'tool', text: str(m.content), error: /^error[:\s]/i.test(str(m.content)) });
80
- }
81
- });
82
-
83
- (Array.isArray(task.handoffs) ? task.handoffs : []).forEach((h, i) => out.push({ id: `handoff:${i}`, kind: 'handoff', at: num(h.at, lastAt), by: h.by || 'person', from: h.from || '', to: h.to || '', text: h.reason || '' }));
84
-
85
- // The thread's posts — a member's findings, a question, an answer, a decision, the runner's notes.
86
- const thread = (run?.threads?.threads || []).find((t) => t.taskId === taskId && t.kind === 'task');
87
- if (thread) for (const p of (run.threads.posts || []).filter((x) => x.threadId === thread.id)) out.push({ id: `post:${p.id}`, kind: 'post', at: num(p.at, lastAt), by: p.by || '', post: p, text: p.text || '' });
88
-
89
- // What the member is saying right now — the attempt's text before it is a step.
90
- const lastText = [...out].reverse().find((e) => e.kind === 'text');
91
- if (task.status === 'running' && task.text && task.text !== lastText?.text) out.push({ id: 'live', kind: 'text', at: num(run?.lastEventAt, lastAt + 1), by, attempt: attemptOf, text: task.text, live: true });
92
-
93
- if (task.endedAt || ['ok', 'failed', 'over-budget', 'stopped', 'waiting'].includes(task.status)) {
94
- // Findings: the task's count, or the finding posts in its thread — a member's board posts
95
- // count as its answer (team-run.js), and a record folded from an older run has only those.
96
- const findings = Math.max(num(task.findings, 0), out.filter((e) => e.kind === 'post' && e.post?.kind === 'finding').length);
97
- const tools = Math.max(num(task.tools, 0), out.filter((e) => e.kind === 'call').length);
98
- out.push({ id: 'end', kind: 'end', at: num(task.endedAt, num(run?.lastEventAt, lastAt + 2)), by: 'runner', status: task.status, error: task.error || null, ms: num(task.ms, 0), findings, tools, text: endText({ ...task, findings, tools }, attempts) });
99
- }
100
- return out.sort((a, b) => a.at - b.at || WORKLOG_KINDS.indexOf(a.kind) - WORKLOG_KINDS.indexOf(b.kind));
101
- }
102
-
103
- function endText(task, attempts) {
104
- const tried = attempts.length > 1 ? ` after ${attempts.length} models (${attempts.map((a) => a.label || a.engine?.label || a.engine?.model || a.model).join(' → ')})` : '';
105
- const s = task.status;
106
- if (s === 'ok') return `done${tried} · ${task.findings || 0} finding${task.findings === 1 ? '' : 's'} · ${task.tools || 0} tool call${task.tools === 1 ? '' : 's'}`;
107
- if (s === 'waiting') return 'waiting on a person';
108
- if (s === 'over-budget') return `stopped at the budget${tried}`;
109
- if (s === 'stopped') return `stopped${tried}`;
110
- return `failed${tried}${task.error ? `: ${task.error}` : ''}`;
111
- }
112
-
113
- /** The log as text — for a brief, a rating, a test: one line per entry, results shortened. */
114
- export function workLogText(entries, { resultChars = 200 } = {}) {
115
- return (entries || []).map((e) => {
116
- switch (e.kind) {
117
- case 'attempt': return `▸ attempt ${e.attempt}: ${e.model}${e.continued ? ' (continues)' : ''}`;
118
- case 'prompt': return `▸ task: ${e.text}`;
119
- case 'note': return `▸ runner → ${e.text}`;
120
- case 'thought': return `${e.by} (thinking): ${e.text}`;
121
- case 'text': return `${e.by}: ${e.text}`;
122
- case 'call': return `${e.by} → ${e.text}`;
123
- case 'result': return ` ← ${e.name}: ${e.text.length > resultChars ? `${e.text.slice(0, resultChars)}…` : e.text}`;
124
- case 'handoff': return `▸ handed from ${e.from} to ${e.to} by ${e.by}${e.text ? ` — ${e.text}` : ''}`;
125
- case 'post': return `${e.by} posted ${e.post?.kind || 'note'}: ${e.text}`;
126
- case 'end': return `▸ ${e.text}`;
127
- default: return '';
128
- }
129
- }).filter(Boolean).join('\n');
130
- }
131
-
132
- /**
133
- * What the log says about the work, as numbers — the evidence a rating starts from, before
134
- * any opinion (a judge's, a peer's, a person's) is added beside it.
135
- */
136
- export function workLogEvidence(entries) {
137
- const ev = { calls: 0, results: 0, resultErrors: 0, thoughts: 0, texts: 0, attempts: 0, handoffs: 0, posts: 0, findings: 0, requests: 0, decided: { approved: 0, rejected: 0 }, status: null, ms: 0 };
138
- for (const e of entries || []) {
139
- if (e.kind === 'call') ev.calls++;
140
- else if (e.kind === 'result') { ev.results++; if (e.error) ev.resultErrors++; }
141
- else if (e.kind === 'thought') ev.thoughts++;
142
- else if (e.kind === 'text') ev.texts++;
143
- else if (e.kind === 'attempt') ev.attempts++;
144
- else if (e.kind === 'handoff') ev.handoffs++;
145
- else if (e.kind === 'post') { ev.posts++; if (e.post?.kind === 'finding') ev.findings++; if (e.post?.kind === 'request') ev.requests++; if (e.post?.status === 'approved') ev.decided.approved++; if (e.post?.status === 'rejected') ev.decided.rejected++; }
146
- else if (e.kind === 'end') { ev.status = e.status; ev.ms = e.ms; ev.findings = Math.max(ev.findings, e.findings || 0); }
147
- }
148
- return ev;
149
- }
1
+ import{isThought as j}from"./team-task.js";const b=Object.freeze(["attempt","prompt","note","thought","text","call","result","handoff","post","end"]),i=(n,s=0)=>Number.isFinite(n)?n:s,$=n=>n==null?"":typeof n=="string"?n:JSON.stringify(n);function k(n){if(n&&typeof n=="object")return n;try{return JSON.parse(String(n||"{}"))}catch{return{raw:String(n||"")}}}function O(n){const s=n?.function?.name||n?.name||"tool",t=k(n?.function?.arguments??n?.arguments),a=t?.action?` ${t.action}`:"",o=t?.args&&typeof t.args=="object"?t.args:t,l=o&&typeof o=="object"?Object.entries(o).find(([p,h])=>p!=="action"&&p!=="args"&&(typeof h=="string"||typeof h=="number")):null;return`${s}${a}${l?` ${l[0]}=${JSON.stringify(String(l[1]).slice(0,120))}`:""}`}function v(n,s){const t=(n?.tasks||[]).find(e=>e.id===s);if(!t)return[];const a=t.role||t.id,o=[],l=Array.isArray(t.attempts)?t.attempts:[],p=i(t.startedAt,i(l[0]?.at,i(n?.startedAt,0)));l.forEach((e,r)=>o.push({id:`attempt:${r+1}`,kind:"attempt",at:i(e.at,p+r),by:"runner",attempt:r+1,model:e.label||e.engine?.label||e.engine?.model||e.model||"",modelId:e.model||"",engine:e.engine||null,continued:!!e.continued,status:e.status||null,error:e.error||null}));const h=Array.isArray(t.transcript)?t.transcript:[],m=new Map;let u=p,c=1;h.forEach((e,r)=>{if(!e||!e.role)return;Number.isFinite(e.attempt)?c=e.attempt:l.length&&(c=Math.max(c,1+l.findLastIndex(f=>i(f.at,0)<=u+1)));const d=Number.isFinite(e.at)?e.at:Math.max(u,i(l[c-1]?.at,0))+1;u=Math.max(u,d);const g=c;if(e.role==="user")o.push({id:`step:${r}`,kind:r===0?"prompt":"note",at:d,by:"runner",attempt:g,text:$(e.content)});else if(e.role==="assistant"){if(j(e)){o.push({id:`step:${r}`,kind:"thought",at:d,by:a,attempt:g,text:e.thought});return}typeof e.content=="string"&&e.content.trim()&&o.push({id:`step:${r}`,kind:"text",at:d,by:a,attempt:g,text:e.content});for(const f of Array.isArray(e.tool_calls)?e.tool_calls:[]){const x=f.id||`c${r}`;m.set(x,f),o.push({id:`call:${x}`,kind:"call",at:d,by:a,attempt:g,callId:x,name:f.function?.name||"tool",args:k(f.function?.arguments),text:O(f)})}}else if(e.role==="tool"){const f=e.tool_call_id?m.get(e.tool_call_id):null;o.push({id:`step:${r}`,kind:"result",at:d,by:"tool",attempt:g,callId:e.tool_call_id||null,name:f?.function?.name||e.name||"tool",text:$(e.content),error:/^error[:\s]/i.test($(e.content))})}}),(Array.isArray(t.handoffs)?t.handoffs:[]).forEach((e,r)=>o.push({id:`handoff:${r}`,kind:"handoff",at:i(e.at,u),by:e.by||"person",from:e.from||"",to:e.to||"",text:e.reason||""}));const y=(n?.threads?.threads||[]).find(e=>e.taskId===s&&e.kind==="task");if(y)for(const e of(n.threads.posts||[]).filter(r=>r.threadId===y.id))o.push({id:`post:${e.id}`,kind:"post",at:i(e.at,u),by:e.by||"",post:e,text:e.text||""});const A=[...o].reverse().find(e=>e.kind==="text");if(t.status==="running"&&t.text&&t.text!==A?.text&&o.push({id:"live",kind:"text",at:i(n?.lastEventAt,u+1),by:a,attempt:c,text:t.text,live:!0}),t.endedAt||["ok","failed","over-budget","stopped","waiting"].includes(t.status)){const e=Math.max(i(t.findings,0),o.filter(d=>d.kind==="post"&&d.post?.kind==="finding").length),r=Math.max(i(t.tools,0),o.filter(d=>d.kind==="call").length);o.push({id:"end",kind:"end",at:i(t.endedAt,i(n?.lastEventAt,u+2)),by:"runner",status:t.status,error:t.error||null,ms:i(t.ms,0),findings:e,tools:r,text:_({...t,findings:e,tools:r},l)})}return o.sort((e,r)=>e.at-r.at||b.indexOf(e.kind)-b.indexOf(r.kind))}function _(n,s){const t=s.length>1?` after ${s.length} models (${s.map(o=>o.label||o.engine?.label||o.engine?.model||o.model).join(" → ")})`:"",a=n.status;return a==="ok"?`done${t} · ${n.findings||0} finding${n.findings===1?"":"s"} · ${n.tools||0} tool call${n.tools===1?"":"s"}`:a==="waiting"?"waiting on a person":a==="over-budget"?`stopped at the budget${t}`:a==="stopped"?`stopped${t}`:`failed${t}${n.error?`: ${n.error}`:""}`}function M(n,{resultChars:s=200}={}){return(n||[]).map(t=>{switch(t.kind){case"attempt":return`▸ attempt ${t.attempt}: ${t.model}${t.continued?" (continues)":""}`;case"prompt":return`▸ task: ${t.text}`;case"note":return`▸ runner → ${t.text}`;case"thought":return`${t.by} (thinking): ${t.text}`;case"text":return`${t.by}: ${t.text}`;case"call":return`${t.by} → ${t.text}`;case"result":return` ← ${t.name}: ${t.text.length>s?`${t.text.slice(0,s)}…`:t.text}`;case"handoff":return`▸ handed from ${t.from} to ${t.to} by ${t.by}${t.text?` — ${t.text}`:""}`;case"post":return`${t.by} posted ${t.post?.kind||"note"}: ${t.text}`;case"end":return`▸ ${t.text}`;default:return""}}).filter(Boolean).join(`
2
+ `)}function N(n){const s={calls:0,results:0,resultErrors:0,thoughts:0,texts:0,attempts:0,handoffs:0,posts:0,findings:0,requests:0,decided:{approved:0,rejected:0},status:null,ms:0};for(const t of n||[])t.kind==="call"?s.calls++:t.kind==="result"?(s.results++,t.error&&s.resultErrors++):t.kind==="thought"?s.thoughts++:t.kind==="text"?s.texts++:t.kind==="attempt"?s.attempts++:t.kind==="handoff"?s.handoffs++:t.kind==="post"?(s.posts++,t.post?.kind==="finding"&&s.findings++,t.post?.kind==="request"&&s.requests++,t.post?.status==="approved"&&s.decided.approved++,t.post?.status==="rejected"&&s.decided.rejected++):t.kind==="end"&&(s.status=t.status,s.ms=t.ms,s.findings=Math.max(s.findings,t.findings||0));return s}export{b as WORKLOG_KINDS,O as describeCall,N as workLogEvidence,v as workLogFor,M as workLogText};
package/team.js CHANGED
@@ -1,302 +1 @@
1
- // A team, as data — roles with grants, a merge policy, a budget. Nothing runs here.
2
- //
3
- // The Notes co-writer swarm was one team, hard-wired: a planner, four roles appointed per
4
- // model, a shared board. The desktop was about to copy it, and every client would then hold
5
- // its own answer to "what is a researcher allowed to touch". So a team is declared once, in
6
- // this shape, and shared through the client-prefs document like a skill or a recipe: defined
7
- // in one client, invokable in the other at its next open.
8
- //
9
- // Two invariants are enforced here rather than trusted:
10
- // • A role's GRANTS name tool groups, never tools — and `page` is not grantable. A tab is
11
- // one person's; a team member acting on it is the one thing every guard was written to
12
- // stop. `none` is a legitimate grant: a writer needs no tools.
13
- // • A team has a BUDGET, or it is not a team (F8 O1). `validateTeam` refuses one without.
14
- //
15
- // Trust is derived, never declared (the skill-manifest rule): a stored `builtin` cannot
16
- // survive an `origin`, and a team a client stores as trusted is stored as nothing of the kind.
17
-
18
- import { validateBudget, normalizeBudget } from './budget.js';
19
- import { normalizeEngineSpec, validateEngineSpec, tierOf } from './engine.js';
20
-
21
- export const TEAM_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
22
- export const ROLE_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/i;
23
- export const ROLE_MODES = Object.freeze(['model', 'subagent', 'recipe']);
24
- export const ROLE_PREFERS = Object.freeze(['cheap', 'balanced', 'strong']);
25
- export const MERGE_POLICIES = Object.freeze(['judge', 'converge', 'concat', 'first']);
26
- export const PLAN_MODES = Object.freeze(['fixed', 'planner']);
27
- /**
28
- * The tool groups a role may hold. `mcp:<server>` narrows to one server; `mcp` is all of them.
29
- *
30
- * The work grants (architecture-pillars.md §14.2) are for an agent whose engine is a harness
31
- * running in a checkout: `shell` and `fs:write` say so explicitly instead of riding along
32
- * with the harness; `scm:read` reads the repo and its hub, `scm:push` pushes ITS OWN branch
33
- * (`cp/<project>/<job>`), `scm:pr` opens a pull request, and `scm:merge` is held by the
34
- * Gate — grantable only where the org's `gate.json` allows it. A chat-model role that holds
35
- * one of these holds nothing: only a harness engine can use them, and the bridge enforces it.
36
- */
37
- export const GRANTABLE = Object.freeze(['none', 'data', 'web', 'mcp', 'history', 'shell', 'fs:write', 'scm:read', 'scm:push', 'scm:pr', 'scm:merge']);
38
- export const WORK_GRANTS = Object.freeze(['shell', 'fs:write', 'scm:read', 'scm:push', 'scm:pr', 'scm:merge']);
39
- export const GRANT_RE = /^(none|data|web|history|mcp|mcp:[a-zA-Z0-9_.:-]{1,64}|shell|fs:write|scm:(read|push|pr|merge))$/;
40
- export const MAX_ROLES = 8;
41
- /** A role that stands for an agent from the pool: `agent` names it (agent.js `AGENT_ID_RE`). */
42
- export const AGENT_REF_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
43
-
44
- export class TeamError extends Error {
45
- constructor(code, message) { super(message); this.name = 'TeamError'; this.code = code; }
46
- }
47
-
48
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
49
-
50
- /** A role's grants, normalized: `none` alone means no tools; duplicates and `page` are dropped. */
51
- export function normalizeGrants(grants) {
52
- const list = (Array.isArray(grants) ? grants : typeof grants === 'string' ? [grants] : []).map((g) => String(g || '').trim()).filter(Boolean);
53
- const ok = [...new Set(list.filter((g) => GRANT_RE.test(g)))];
54
- if (!ok.length || ok.includes('none')) return ['none'];
55
- return ok;
56
- }
57
-
58
- export function validateTeam(team) {
59
- const errors = [];
60
- if (!isRecord(team)) return { ok: false, errors: ['team must be an object'] };
61
- if (!TEAM_NAME_RE.test(String(team.name || ''))) errors.push('name: a short identifier (letters, digits, _ -)');
62
- if (!Array.isArray(team.roles) || !team.roles.length) errors.push('roles: a non-empty array');
63
- else {
64
- if (team.roles.length > MAX_ROLES) errors.push(`roles: at most ${MAX_ROLES}`);
65
- const seen = new Set();
66
- team.roles.forEach((r, i) => {
67
- const w = `roles[${i}]`;
68
- if (!isRecord(r)) { errors.push(`${w}: must be an object`); return; }
69
- if (!ROLE_ID_RE.test(String(r.id || ''))) errors.push(`${w}.id: a short identifier`);
70
- else if (seen.has(r.id)) errors.push(`${w}.id: duplicate "${r.id}"`);
71
- seen.add(r.id);
72
- if (r.mode !== undefined && !ROLE_MODES.includes(r.mode)) errors.push(`${w}.mode: one of ${ROLE_MODES.join(', ')}`);
73
- if (r.prefer !== undefined && !ROLE_PREFERS.includes(r.prefer)) errors.push(`${w}.prefer: one of ${ROLE_PREFERS.join(', ')}`);
74
- if ((r.mode || 'model') === 'recipe' && !r.recipe) errors.push(`${w}.recipe: a recipe name is required in recipe mode`);
75
- if (r.agent !== undefined && r.agent !== null && !AGENT_REF_RE.test(String(r.agent))) errors.push(`${w}.agent: an agent id`);
76
- // A role that stands for an agent takes its prompt from the pool (agent.js resolveTeam);
77
- // a role that stands for nobody must say what it does.
78
- if ((r.mode || 'model') !== 'recipe' && !r.agent && !String(r.prompt || '').trim()) errors.push(`${w}.prompt: what this role does`);
79
- errors.push(...validateEngineSpec(r.engine, `${w}.engine`));
80
- const bad = (Array.isArray(r.grants) ? r.grants : []).filter((g) => !GRANT_RE.test(String(g)));
81
- if (bad.length) errors.push(`${w}.grants: not grantable: ${bad.join(', ')}${bad.some((g) => /^page/.test(String(g))) ? ' (a tab is one person\'s; a team may not act on it)' : ''}`);
82
- });
83
- }
84
- if (team.merge !== undefined && !MERGE_POLICIES.includes(team.merge)) errors.push(`merge: one of ${MERGE_POLICIES.join(', ')}`);
85
- if (team.plan !== undefined && !PLAN_MODES.includes(team.plan)) errors.push(`plan: one of ${PLAN_MODES.join(', ')}`);
86
- if (team.judge !== undefined && team.judge !== null && !(Array.isArray(team.roles) && team.roles.some((r) => r?.id === team.judge))) errors.push('judge: must name one of the roles');
87
- const b = validateBudget(team.budget);
88
- if (!b.ok) errors.push(...b.errors.map((e) => `budget: ${e}`));
89
- return { ok: errors.length === 0, errors };
90
- }
91
-
92
- /**
93
- * The stored form. Defaults filled, grants normalized, trust derived: `builtin` only when the
94
- * host says so, never from the record.
95
- */
96
- export function normalizeTeam(team, { builtin = false } = {}) {
97
- const v = validateTeam(team);
98
- if (!v.ok) throw new TeamError('INVALID', v.errors.join('; '));
99
- return {
100
- name: String(team.name),
101
- description: String(team.description || '').trim().slice(0, 300),
102
- plan: PLAN_MODES.includes(team.plan) ? team.plan : 'fixed',
103
- merge: MERGE_POLICIES.includes(team.merge) ? team.merge : (team.judge ? 'judge' : 'concat'),
104
- judge: team.judge || null,
105
- roles: team.roles.map((r) => ({
106
- id: String(r.id),
107
- name: String(r.name || r.id).slice(0, 60),
108
- mode: ROLE_MODES.includes(r.mode) ? r.mode : 'model',
109
- prefer: ROLE_PREFERS.includes(r.prefer) ? r.prefer : (r.engine ? tierOf(r.engine) : 'balanced'),
110
- ...(r.model ? { model: String(r.model) } : {}),
111
- ...(r.agent ? { agent: String(r.agent) } : {}),
112
- ...(r.engine ? { engine: normalizeEngineSpec(r.engine) } : {}),
113
- prompt: String(r.prompt || '').trim().slice(0, 4000),
114
- // A role that stands for an agent holds the agent's grants unless it narrows them: no
115
- // list means "the agent's", so the key is left out rather than stored as `none`.
116
- ...(r.agent && !(Array.isArray(r.grants) && r.grants.length) ? {} : { grants: normalizeGrants(r.grants) }),
117
- ...(r.recipe ? { recipe: String(r.recipe) } : {}),
118
- ...(Array.isArray(r.dependsOn) ? { dependsOn: r.dependsOn.map(String).filter((d) => d !== r.id) } : {}),
119
- // What agent.js resolveTeam fills from the pool; kept so the runner's roles carry it.
120
- ...(Array.isArray(r.skills) && r.skills.length ? { skills: r.skills.map(String).slice(0, 32) } : {}),
121
- ...(r.workdir ? { workdir: String(r.workdir).slice(0, 400) } : {}),
122
- ...(r.egress === 'redacted' || r.egress === 'delegated' ? { egress: r.egress } : {}),
123
- ...(r.memoryScope ? { memoryScope: String(r.memoryScope).slice(0, 120) } : {}),
124
- })),
125
- budget: normalizeBudget(team.budget),
126
- enabled: team.enabled !== false,
127
- ...(team.origin && isRecord(team.origin) ? { origin: { ...team.origin } } : {}),
128
- ...(builtin ? { builtin: true } : {}),
129
- ...(team.createdAt ? { createdAt: team.createdAt } : {}),
130
- };
131
- }
132
-
133
- export function defineTeam(team) { return Object.freeze(normalizeTeam(team)); }
134
-
135
- /** Which of a client's tool groups a role may hold — `(groupId, serverId?) => boolean`. */
136
- export function grantAllows(grants, groupId, serverId = '') {
137
- const g = normalizeGrants(grants);
138
- if (g.includes('none')) return false;
139
- if (groupId === 'page') return false;
140
- if (groupId === 'mcp') return g.includes('mcp') || (!!serverId && g.includes(`mcp:${serverId}`));
141
- return g.includes(groupId);
142
- }
143
-
144
- /**
145
- * The SCM ladder: `merge` ⊃ `pr` ⊃ `push` ⊃ `read` — a role that may open a PR may push the
146
- * branch the PR is from, and anyone who may push may read. `push` is the role's OWN branch
147
- * only; the bridge names it (`cp/<project>/<job>`) and refuses any other.
148
- */
149
- const SCM_LADDER = ['read', 'push', 'pr', 'merge'];
150
- export function scmAllows(grants, action) {
151
- const g = normalizeGrants(grants);
152
- const want = SCM_LADDER.indexOf(String(action || '').replace(/^scm:/, ''));
153
- if (want < 0 || g.includes('none')) return false;
154
- const held = Math.max(-1, ...g.filter((x) => x.startsWith('scm:')).map((x) => SCM_LADDER.indexOf(x.slice(4))));
155
- return held >= want;
156
- }
157
-
158
- /** One line a person reads per role: name · tier/model · grants · mode. */
159
- export function describeRole(r) {
160
- const who = r.model || r.prefer || 'balanced';
161
- const grants = (r.grants || ['none']).join(', ');
162
- return `${r.name || r.id}${r.agent ? ` (agent: ${r.agent})` : ''} — ${who}${r.mode && r.mode !== 'model' ? ` (${r.mode})` : ''} · tools: ${grants}`;
163
- }
164
-
165
- // ── Starters and the editor's form ───────────────────────────────────────────────────────
166
- //
167
- // A team is usually proposed in conversation, but a person's first team should not depend
168
- // on a model deciding to propose one. Both clients' Settings → Teams offer these as "Add
169
- // starter" and edit them on the same form as a blank team; `teamFromForm` is the one shaping
170
- // of that form into a team, so a field's meaning does not differ between clients.
171
-
172
- export const STARTER_TEAMS = Object.freeze([
173
- {
174
- name: 'research',
175
- description: 'Research a question from the web and your own notes, then write it up.',
176
- plan: 'fixed', merge: 'judge', judge: 'writer',
177
- roles: [
178
- { id: 'researcher', prompt: 'Research the request thoroughly. Search the web and the user\'s own history. Report each fact as a finding with where it came from; note disagreements between sources.', prefer: 'balanced', grants: ['web', 'data'] },
179
- { id: 'writer', prompt: 'Write the answer the user asked for from the board\'s findings, citing them. Say plainly what was not found.', prefer: 'strong', grants: ['none'] },
180
- ],
181
- budget: { tokens: 40000, ms: 300000 },
182
- },
183
- {
184
- name: 'review',
185
- description: 'Two independent reads of a draft, reconciled into one set of comments.',
186
- plan: 'fixed', merge: 'converge',
187
- roles: [
188
- { id: 'editor', prompt: 'Read the draft as an editor: structure, clarity, what is missing. One finding per issue, with the passage it refers to.', prefer: 'strong', grants: ['none'] },
189
- { id: 'checker', prompt: 'Read the draft as a fact-checker: every claim that could be wrong, with what you checked against. Use the user\'s history and the web.', prefer: 'balanced', grants: ['data', 'web'] },
190
- ],
191
- budget: { tokens: 30000, ms: 240000 },
192
- },
193
- // ── The engineering teams (architecture-pillars.md §12.2) — roles stand for the standing
194
- // agents in agent.js STARTER_AGENTS; a role's prompt is the agent's, its engine the agent's.
195
- // A feature that crosses repos recruits one Implementer per repo (the role says which via
196
- // its prompt); these starters name one.
197
- {
198
- name: 'feature',
199
- description: 'Architect plans, an Implementer builds on a branch, Reviewer and Tester check, Scribe writes it up. The Architect judges.',
200
- plan: 'planner', merge: 'judge', judge: 'architect',
201
- roles: [
202
- { id: 'architect', agent: 'architect' },
203
- { id: 'implementer', agent: 'implementer', dependsOn: ['architect'] },
204
- { id: 'reviewer', agent: 'reviewer', dependsOn: ['implementer'] },
205
- { id: 'tester', agent: 'tester', dependsOn: ['implementer'] },
206
- { id: 'scribe', agent: 'scribe', dependsOn: ['reviewer', 'tester'] },
207
- ],
208
- budget: { tokens: 400000, ms: 3600000 },
209
- },
210
- {
211
- name: 'fix',
212
- description: 'One Implementer fixes it on a branch, the Tester runs the guard, the Scribe notes it.',
213
- plan: 'fixed', merge: 'concat',
214
- roles: [
215
- { id: 'implementer', agent: 'implementer' },
216
- { id: 'tester', agent: 'tester', dependsOn: ['implementer'] },
217
- { id: 'scribe', agent: 'scribe', dependsOn: ['tester'] },
218
- ],
219
- budget: { tokens: 150000, ms: 1800000 },
220
- },
221
- {
222
- name: 'docs',
223
- description: 'The Architect decides what the docs should say; the Scribe proposes the text.',
224
- plan: 'fixed', merge: 'concat',
225
- roles: [
226
- { id: 'architect', agent: 'architect' },
227
- { id: 'scribe', agent: 'scribe', dependsOn: ['architect'] },
228
- ],
229
- budget: { tokens: 80000, ms: 900000 },
230
- },
231
- {
232
- name: 'release',
233
- description: 'The Tester runs the guard on the merged branch, Release bumps and asks before publishing, the Scribe records the version.',
234
- plan: 'fixed', merge: 'concat',
235
- roles: [
236
- { id: 'tester', agent: 'tester' },
237
- { id: 'release', agent: 'release', dependsOn: ['tester'] },
238
- { id: 'scribe', agent: 'scribe', dependsOn: ['release'] },
239
- ],
240
- budget: { tokens: 60000, ms: 1800000 },
241
- },
242
- ]);
243
-
244
- /**
245
- * A name as typed → the identifier a `/command` needs: "Research Team" → "research-team".
246
- * Used by the tool's save and by the form, so a model that names a team in prose is not
247
- * bounced for it; what cannot be shaped (nothing left) still fails validation.
248
- */
249
- export function slugTeamName(name) {
250
- return String(name || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').replace(/-+$/, '').slice(0, 64);
251
- }
252
-
253
- /** Fresh copies — a starter is a template, never the stored record. */
254
- export function starterTeams() {
255
- return STARTER_TEAMS.map((t) => ({ ...t, roles: t.roles.map((r) => ({ ...r, ...(r.grants ? { grants: [...r.grants] } : {}), ...(r.dependsOn ? { dependsOn: [...r.dependsOn] } : {}) })), budget: { ...t.budget } }));
256
- }
257
-
258
- /** A blank team for the editor: one role, the smallest budget that is still a budget. */
259
- export function blankTeam() {
260
- return { name: '', description: '', plan: 'fixed', merge: 'concat', judge: null, roles: [{ id: 'role1', prompt: '', prefer: 'balanced', grants: ['none'] }], budget: { tokens: 20000, ms: 300000 } };
261
- }
262
-
263
- /**
264
- * The editor's form → a team, or the errors. Grants come as text ("web, data, mcp:srv"),
265
- * the budget as numbers that may be blank; a blank judge under `merge: judge` is the last
266
- * role, which is the writer in every starter.
267
- */
268
- const grantsText = (g) => String(Array.isArray(g) ? g.join(',') : g || '').split(/[,\s]+/).map((x) => x.trim()).filter(Boolean);
269
- export function teamFromForm(form) {
270
- const roles = (Array.isArray(form.roles) ? form.roles : []).map((r) => ({
271
- id: String(r.id || '').trim(),
272
- name: String(r.name || '').trim() || undefined,
273
- prompt: String(r.prompt || ''),
274
- prefer: r.prefer || 'balanced',
275
- ...(r.model ? { model: String(r.model) } : {}),
276
- ...(r.agent ? { agent: String(r.agent).trim() } : {}),
277
- ...(r.engine ? { engine: r.engine } : {}),
278
- ...(Array.isArray(r.dependsOn) ? { dependsOn: r.dependsOn.map(String) } : {}),
279
- // Blank grants on a role that stands for an agent mean "the agent's"; on any other role
280
- // they mean none.
281
- ...(grantsText(r.grants).length ? { grants: grantsText(r.grants) } : r.agent ? {} : { grants: ['none'] }),
282
- }));
283
- const budget = {};
284
- for (const k of ['tokens', 'calls', 'ms', 'usd']) {
285
- const v = Number(form.budget?.[k]);
286
- if (form.budget?.[k] !== '' && form.budget?.[k] != null && Number.isFinite(v) && v > 0) budget[k] = v;
287
- }
288
- const merge = form.merge || 'concat';
289
- const team = {
290
- name: slugTeamName(form.name),
291
- description: String(form.description || '').trim(),
292
- plan: form.plan || 'fixed',
293
- merge,
294
- judge: merge === 'judge' ? (form.judge || roles[roles.length - 1]?.id || null) : null,
295
- roles,
296
- budget,
297
- enabled: form.enabled !== false,
298
- ...(form.createdAt ? { createdAt: form.createdAt } : {}),
299
- };
300
- const v = validateTeam(team);
301
- return v.ok ? { ok: true, team: normalizeTeam(team) } : { ok: false, errors: v.errors };
302
- }
1
+ import{validateBudget as y,normalizeBudget as x}from"./budget.js";import{normalizeEngineSpec as O,validateEngineSpec as j,tierOf as $}from"./engine.js";const k=/^[a-z][a-z0-9_-]{0,63}$/i,E=/^[a-z][a-z0-9_-]{0,31}$/i,c=Object.freeze(["model","subagent","recipe"]),l=Object.freeze(["cheap","balanced","strong"]),p=Object.freeze(["judge","converge","concat","first"]),g=Object.freeze(["fixed","planner"]),N=Object.freeze(["none","data","web","mcp","history","shell","fs:write","scm:read","scm:push","scm:pr","scm:merge"]),M=Object.freeze(["shell","fs:write","scm:read","scm:push","scm:pr","scm:merge"]),f=/^(none|data|web|history|mcp|mcp:[a-zA-Z0-9_.:-]{1,64}|shell|fs:write|scm:(read|push|pr|merge))$/,h=8,R=/^[a-z][a-z0-9_-]{0,63}$/i;class T extends Error{constructor(t,i){super(i),this.name="TeamError",this.code=t}}const u=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function m(e){const t=(Array.isArray(e)?e:typeof e=="string"?[e]:[]).map(n=>String(n||"").trim()).filter(Boolean),i=[...new Set(t.filter(n=>f.test(n)))];return!i.length||i.includes("none")?["none"]:i}function b(e){const t=[];if(!u(e))return{ok:!1,errors:["team must be an object"]};if(k.test(String(e.name||""))||t.push("name: a short identifier (letters, digits, _ -)"),!Array.isArray(e.roles)||!e.roles.length)t.push("roles: a non-empty array");else{e.roles.length>h&&t.push(`roles: at most ${h}`);const n=new Set;e.roles.forEach((s,o)=>{const r=`roles[${o}]`;if(!u(s)){t.push(`${r}: must be an object`);return}E.test(String(s.id||""))?n.has(s.id)&&t.push(`${r}.id: duplicate "${s.id}"`):t.push(`${r}.id: a short identifier`),n.add(s.id),s.mode!==void 0&&!c.includes(s.mode)&&t.push(`${r}.mode: one of ${c.join(", ")}`),s.prefer!==void 0&&!l.includes(s.prefer)&&t.push(`${r}.prefer: one of ${l.join(", ")}`),(s.mode||"model")==="recipe"&&!s.recipe&&t.push(`${r}.recipe: a recipe name is required in recipe mode`),s.agent!==void 0&&s.agent!==null&&!R.test(String(s.agent))&&t.push(`${r}.agent: an agent id`),(s.mode||"model")!=="recipe"&&!s.agent&&!String(s.prompt||"").trim()&&t.push(`${r}.prompt: what this role does`),t.push(...j(s.engine,`${r}.engine`));const a=(Array.isArray(s.grants)?s.grants:[]).filter(d=>!f.test(String(d)));a.length&&t.push(`${r}.grants: not grantable: ${a.join(", ")}${a.some(d=>/^page/.test(String(d)))?" (a tab is one person's; a team may not act on it)":""}`)})}e.merge!==void 0&&!p.includes(e.merge)&&t.push(`merge: one of ${p.join(", ")}`),e.plan!==void 0&&!g.includes(e.plan)&&t.push(`plan: one of ${g.join(", ")}`),e.judge!==void 0&&e.judge!==null&&!(Array.isArray(e.roles)&&e.roles.some(n=>n?.id===e.judge))&&t.push("judge: must name one of the roles");const i=y(e.budget);return i.ok||t.push(...i.errors.map(n=>`budget: ${n}`)),{ok:t.length===0,errors:t}}function S(e,{builtin:t=!1}={}){const i=b(e);if(!i.ok)throw new T("INVALID",i.errors.join("; "));return{name:String(e.name),description:String(e.description||"").trim().slice(0,300),plan:g.includes(e.plan)?e.plan:"fixed",merge:p.includes(e.merge)?e.merge:e.judge?"judge":"concat",judge:e.judge||null,roles:e.roles.map(n=>({id:String(n.id),name:String(n.name||n.id).slice(0,60),mode:c.includes(n.mode)?n.mode:"model",prefer:l.includes(n.prefer)?n.prefer:n.engine?$(n.engine):"balanced",...n.model?{model:String(n.model)}:{},...n.agent?{agent:String(n.agent)}:{},...n.engine?{engine:O(n.engine)}:{},prompt:String(n.prompt||"").trim().slice(0,4e3),...n.agent&&!(Array.isArray(n.grants)&&n.grants.length)?{}:{grants:m(n.grants)},...n.recipe?{recipe:String(n.recipe)}:{},...Array.isArray(n.dependsOn)?{dependsOn:n.dependsOn.map(String).filter(s=>s!==n.id)}:{},...Array.isArray(n.skills)&&n.skills.length?{skills:n.skills.map(String).slice(0,32)}:{},...n.workdir?{workdir:String(n.workdir).slice(0,400)}:{},...n.egress==="redacted"||n.egress==="delegated"?{egress:n.egress}:{},...n.memoryScope?{memoryScope:String(n.memoryScope).slice(0,120)}:{}})),budget:x(e.budget),enabled:e.enabled!==!1,...e.origin&&u(e.origin)?{origin:{...e.origin}}:{},...t?{builtin:!0}:{},...e.createdAt?{createdAt:e.createdAt}:{}}}function D(e){return Object.freeze(S(e))}function G(e,t,i=""){const n=m(e);return n.includes("none")||t==="page"?!1:t==="mcp"?n.includes("mcp")||!!i&&n.includes(`mcp:${i}`):n.includes(t)}const w=["read","push","pr","merge"];function B(e,t){const i=m(e),n=w.indexOf(String(t||"").replace(/^scm:/,""));return n<0||i.includes("none")?!1:Math.max(-1,...i.filter(o=>o.startsWith("scm:")).map(o=>w.indexOf(o.slice(4))))>=n}function F(e){const t=e.model||e.prefer||"balanced",i=(e.grants||["none"]).join(", ");return`${e.name||e.id}${e.agent?` (agent: ${e.agent})`:""} ${t}${e.mode&&e.mode!=="model"?` (${e.mode})`:""} · tools: ${i}`}const z=Object.freeze([{name:"research",description:"Research a question from the web and your own notes, then write it up.",plan:"fixed",merge:"judge",judge:"writer",roles:[{id:"researcher",prompt:"Research the request thoroughly. Search the web and the user's own history. Report each fact as a finding with where it came from; note disagreements between sources.",prefer:"balanced",grants:["web","data"]},{id:"writer",prompt:"Write the answer the user asked for from the board's findings, citing them. Say plainly what was not found.",prefer:"strong",grants:["none"]}],budget:{tokens:4e4,ms:3e5}},{name:"review",description:"Two independent reads of a draft, reconciled into one set of comments.",plan:"fixed",merge:"converge",roles:[{id:"editor",prompt:"Read the draft as an editor: structure, clarity, what is missing. One finding per issue, with the passage it refers to.",prefer:"strong",grants:["none"]},{id:"checker",prompt:"Read the draft as a fact-checker: every claim that could be wrong, with what you checked against. Use the user's history and the web.",prefer:"balanced",grants:["data","web"]}],budget:{tokens:3e4,ms:24e4}},{name:"feature",description:"Architect plans, an Implementer builds on a branch, Reviewer and Tester check, Scribe writes it up. The Architect judges.",plan:"planner",merge:"judge",judge:"architect",roles:[{id:"architect",agent:"architect"},{id:"implementer",agent:"implementer",dependsOn:["architect"]},{id:"reviewer",agent:"reviewer",dependsOn:["implementer"]},{id:"tester",agent:"tester",dependsOn:["implementer"]},{id:"scribe",agent:"scribe",dependsOn:["reviewer","tester"]}],budget:{tokens:4e5,ms:36e5}},{name:"fix",description:"One Implementer fixes it on a branch, the Tester runs the guard, the Scribe notes it.",plan:"fixed",merge:"concat",roles:[{id:"implementer",agent:"implementer"},{id:"tester",agent:"tester",dependsOn:["implementer"]},{id:"scribe",agent:"scribe",dependsOn:["tester"]}],budget:{tokens:15e4,ms:18e5}},{name:"docs",description:"The Architect decides what the docs should say; the Scribe proposes the text.",plan:"fixed",merge:"concat",roles:[{id:"architect",agent:"architect"},{id:"scribe",agent:"scribe",dependsOn:["architect"]}],budget:{tokens:8e4,ms:9e5}},{name:"release",description:"The Tester runs the guard on the merged branch, Release bumps and asks before publishing, the Scribe records the version.",plan:"fixed",merge:"concat",roles:[{id:"tester",agent:"tester"},{id:"release",agent:"release",dependsOn:["tester"]},{id:"scribe",agent:"scribe",dependsOn:["release"]}],budget:{tokens:6e4,ms:18e5}}]);function _(e){return String(e||"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/,"").slice(0,64)}function I(){return z.map(e=>({...e,roles:e.roles.map(t=>({...t,...t.grants?{grants:[...t.grants]}:{},...t.dependsOn?{dependsOn:[...t.dependsOn]}:{}})),budget:{...e.budget}}))}function q(){return{name:"",description:"",plan:"fixed",merge:"concat",judge:null,roles:[{id:"role1",prompt:"",prefer:"balanced",grants:["none"]}],budget:{tokens:2e4,ms:3e5}}}const A=e=>String(Array.isArray(e)?e.join(","):e||"").split(/[,\s]+/).map(t=>t.trim()).filter(Boolean);function C(e){const t=(Array.isArray(e.roles)?e.roles:[]).map(r=>({id:String(r.id||"").trim(),name:String(r.name||"").trim()||void 0,prompt:String(r.prompt||""),prefer:r.prefer||"balanced",...r.model?{model:String(r.model)}:{},...r.agent?{agent:String(r.agent).trim()}:{},...r.engine?{engine:r.engine}:{},...Array.isArray(r.dependsOn)?{dependsOn:r.dependsOn.map(String)}:{},...A(r.grants).length?{grants:A(r.grants)}:r.agent?{}:{grants:["none"]}})),i={};for(const r of["tokens","calls","ms","usd"]){const a=Number(e.budget?.[r]);e.budget?.[r]!==""&&e.budget?.[r]!=null&&Number.isFinite(a)&&a>0&&(i[r]=a)}const n=e.merge||"concat",s={name:_(e.name),description:String(e.description||"").trim(),plan:e.plan||"fixed",merge:n,judge:n==="judge"&&(e.judge||t[t.length-1]?.id)||null,roles:t,budget:i,enabled:e.enabled!==!1,...e.createdAt?{createdAt:e.createdAt}:{}},o=b(s);return o.ok?{ok:!0,team:S(s)}:{ok:!1,errors:o.errors}}export{R as AGENT_REF_RE,N as GRANTABLE,f as GRANT_RE,h as MAX_ROLES,p as MERGE_POLICIES,g as PLAN_MODES,E as ROLE_ID_RE,c as ROLE_MODES,l as ROLE_PREFERS,z as STARTER_TEAMS,k as TEAM_NAME_RE,T as TeamError,M as WORK_GRANTS,q as blankTeam,D as defineTeam,F as describeRole,G as grantAllows,m as normalizeGrants,S as normalizeTeam,B as scmAllows,_ as slugTeamName,I as starterTeams,C as teamFromForm,b as validateTeam};