@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.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/team-trail.js
CHANGED
|
@@ -1,73 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|