@chatpanel/events 0.100.0 → 0.102.0

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/index.js CHANGED
@@ -238,7 +238,7 @@ export { workLogFor, workLogText, workLogEvidence, describeCall, WORKLOG_KINDS }
238
238
  export { normalizeRequest, subtaskFromRequest, takeUp, takeUpLine, holdsGrants, jobFromSubtask, extendDependents, taskTree, threadRows, MAX_SUBTASKS, MAX_DEPTH, MIN_TAKEUP_FIT } from './team-subtask.js';
239
239
  export { teamLine, teamLanes } from './team-trail.js';
240
240
  // The org, derived (F8 §17): roles as cards, starters whole, a team's health and shape, the roster, one colour per agent.
241
- export { promoteRoles, starterTeam, missingStarters, teamHealth, teamShape, describeTeamShape, whereItWorks, rosterRows, agentKind, agentHue, agentColor, agentInitials, roleCardId, cardNumbers, upsertAgents, TEAM_SHAPES, ROSTER_KINDS } from './team-org.js';
241
+ export { promoteRoles, starterTeam, missingStarters, teamHealth, teamShape, describeTeamShape, whereItWorks, rosterRows, agentKind, agentHue, agentColor, agentInitials, roleCardId, cardNumbers, upsertAgents, soloTeam, teamsWithSolos, builtinOrg, isBuiltin, grantChoices, grantsFromChoices, skillChoices, GRANT_INFO, SOLO_BUDGET, TEAM_SHAPES, ROSTER_KINDS } from './team-org.js';
242
242
  export { observeInbox, observeStrip, runLanes, spendRows, engineStrip } from './team-observe.js';
243
243
  export { mcpDispatchProvider, MCP_TOOL_NAME } from './mcp-dispatch.js';
244
244
  export { createManifest, ManifestError, SOURCES } from './manifest.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.100.0",
3
+ "version": "0.102.0",
4
4
  "description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/slash-commands.js CHANGED
@@ -40,7 +40,9 @@ function recipeItem(recipe) {
40
40
  // A saved TEAM answers to a slash the same way: `/research <request>` is a request to run
41
41
  // it, and the `team` tool does the rest.
42
42
  function teamItem(team) {
43
- return { type: 'team', command: team.name || '', icon: '🧑‍🤝‍🧑', description: team.description || 'Agent team', team };
43
+ // A solo team (team-org.js soloTeam) IS an agent, invokable on its own: drawn as one.
44
+ const solo = !!team.origin?.agent;
45
+ return { type: 'team', command: team.name || '', icon: solo ? '🧑‍💻' : '🧑‍🤝‍🧑', description: team.description || (solo ? 'Agent' : 'Agent team'), team, ...(solo ? { agent: team.origin.agent } : {}) };
44
46
  }
45
47
 
46
48
  /** Skills that are switched on. Absence of the flag means enabled (older records have none). */
@@ -119,6 +121,7 @@ export function matchSlashTeam(text, teams = []) {
119
121
  /** What the model receives for a team command: a request to run it, never a prompt expansion. */
120
122
  export function teamInvocationText(team, args = '') {
121
123
  const a = String(args || '').trim();
124
+ if (team.origin?.agent) return `Run the agent "${team.name}" (a one-role team of that name)${a ? ` on this request: ${a}` : ''}. Use the team tool; if the request is unclear, ask first.`;
122
125
  return `Run the saved team "${team.name}"${a ? ` on this request: ${a}` : ''}. Use the team tool; if the request is unclear, ask first.`;
123
126
  }
124
127
 
package/team-org.js CHANGED
@@ -13,8 +13,8 @@
13
13
  // Nothing here renders. A client maps `columns` to boxes and arrows, `hue` to a colour, and
14
14
  // `kind` to a word; the SVG is its own.
15
15
 
16
- import { normalizeTeam, validateTeam, starterTeams, TeamError } from './team.js';
17
- import { normalizeAgent, engineOf, starterAgents, STARTER_AGENTS, ASSISTANT_ID } from './agent.js';
16
+ import { normalizeTeam, validateTeam, starterTeams, TeamError, GRANT_RE } from './team.js';
17
+ import { normalizeAgent, engineOf, starterAgents, assistantAgent, STARTER_AGENTS, ASSISTANT_ID } from './agent.js';
18
18
 
19
19
  const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
20
20
  const poolList = (pool) => (Array.isArray(pool) ? pool : []).filter((a) => a && a.id);
@@ -309,3 +309,134 @@ export function upsertAgents(pool, cards) {
309
309
  const add = (Array.isArray(cards) ? cards : []).filter((c) => c && c.id);
310
310
  return [...list.map((a) => add.find((c) => c.id === a.id) || a), ...add.filter((c) => !list.some((a) => a.id === c.id))];
311
311
  }
312
+
313
+ /**
314
+ * An agent, INVOKABLE on its own: a one-role team named after it (`/researcher`, "ask the
315
+ * researcher to…"), the role standing for the card, the answer the role's own (`merge:
316
+ * first`), under a default budget since a team without one does not run (O1). `origin.agent`
317
+ * marks it so a client can draw it as an agent, not a team. Never stored — derived from the
318
+ * pool every time, so a card edit lands at once and a deleted card takes its command with it.
319
+ */
320
+ export const SOLO_BUDGET = Object.freeze({ tokens: 40000, ms: 300000 });
321
+ export function soloTeam(agent, { budget = SOLO_BUDGET } = {}) {
322
+ const a = agent && agent.id ? agent : null;
323
+ if (!a || a.enabled === false) return null;
324
+ return normalizeTeam({
325
+ name: String(a.id).toLowerCase(),
326
+ description: `Just ${a.name || a.id}${a.purpose ? ` — ${a.purpose}` : ''}`,
327
+ plan: 'fixed', merge: 'first',
328
+ roles: [{ id: String(a.id).toLowerCase().slice(0, 32), agent: a.id }],
329
+ budget: { ...budget },
330
+ origin: { agent: a.id },
331
+ });
332
+ }
333
+
334
+
335
+ /**
336
+ * The teams a chat can run: the saved ones, then a solo team per enabled pool agent whose
337
+ * id no saved team already claims. What the slash menu, the `team` tool and "run …" all read.
338
+ */
339
+ export function teamsWithSolos(teams = [], pool = [], opts = {}) {
340
+ const org = builtinOrg(teams, pool);
341
+ const saved = org.teams;
342
+ const taken = new Set(saved.map((t) => String(t.name).toLowerCase()));
343
+ const solos = [];
344
+ for (const a of org.pool) {
345
+ if (taken.has(String(a.id).toLowerCase()) || !(Array.isArray(a.appliesTo) ? a.appliesTo : ['jobs']).includes('jobs')) continue;
346
+ const t = soloTeam(a, opts);
347
+ if (t) { solos.push(t); taken.add(t.name); }
348
+ }
349
+ // The Assistant too: `/assistant <request>` runs the chat's own model as a one-role team —
350
+ // on the board, on a scorecard — where a plain turn is neither.
351
+ if (!taken.has(ASSISTANT_ID)) { const t = soloTeam(assistantAgent(), opts); if (t) solos.push({ ...t, builtin: true }); }
352
+ return [...saved, ...solos];
353
+ }
354
+
355
+ /**
356
+ * THE BUILT-IN ORG — the standing agents and the starter teams ship with the product, present
357
+ * and runnable without a click (the mock's "built-in" chips), and DERIVED, never stored: the
358
+ * `agents` and `teams` sections hold only what a person made, edited or switched off. A saved
359
+ * record with a built-in's id or name REPLACES it (an edit, or `enabled: false`), so the
360
+ * sections stay small and a product update reaches every unedited built-in.
361
+ *
362
+ * • every starter team a person has not saved is here, promoted (its roles as cards) and
363
+ * marked `builtin`, with the cards it stands on;
364
+ * • every starter agent not in the pool is here, marked `builtin`;
365
+ * • the Assistant stays the fixed card it is (agent.js assistantAgent).
366
+ * Returns `{ teams, pool }` — what every list, the slash menu and the runner read.
367
+ */
368
+ export function builtinOrg(teams = [], pool = []) {
369
+ const savedTeams = (Array.isArray(teams) ? teams : []).filter((t) => t && t.name);
370
+ const savedNames = new Set(savedTeams.map((t) => String(t.name).toLowerCase()));
371
+ const outPool = [...poolList(pool)];
372
+ const have = () => new Set(outPool.map((a) => String(a.id)));
373
+ const outTeams = [...savedTeams];
374
+ for (const st of starterTeams()) {
375
+ if (savedNames.has(st.name)) continue;
376
+ const { team, agents } = starterTeam(st.name, outPool, { now: () => 0 });
377
+ outTeams.push({ ...team, builtin: true });
378
+ const ids = have();
379
+ for (const a of agents) if (!ids.has(a.id)) { const { createdAt, ...card } = a; outPool.push({ ...card, builtin: true }); }
380
+ }
381
+ const ids = have();
382
+ for (const a of starterAgents()) if (!ids.has(a.id)) outPool.push({ ...a, builtin: true });
383
+ return { teams: outTeams, pool: outPool };
384
+ }
385
+
386
+ /** Is this record the product's, not a person's — drawn with the built-in chip, toggled rather than deleted. */
387
+ export const isBuiltin = (rec) => !!rec?.builtin;
388
+
389
+ /**
390
+ * The GRANTS a person can give, as choices — not a free field of ids nobody has seen. Grouped
391
+ * the way they read: what the agent may reach, which connected servers, what work it may do
392
+ * (only an agent-tool engine can use those). `servers` are the connected MCP servers
393
+ * (`{ id, name? }`), each its own `mcp:<id>` choice under the umbrella `mcp`.
394
+ */
395
+ export const GRANT_INFO = Object.freeze({
396
+ data: ['Your data', 'notes, meetings and past chats — search and read'],
397
+ web: ['Web', 'search the web and fetch pages'],
398
+ history: ['Chat history', 'search past chats only'],
399
+ mcp: ['Every connected server', 'all MCP servers you connected; pick servers below to narrow'],
400
+ shell: ['Run commands', 'a shell in its working directory'],
401
+ 'fs:write': ['Write files', 'edit files in its working directory'],
402
+ 'scm:read': ['Read the repository', 'the checkout and its hub'],
403
+ 'scm:push': ['Push its branch', 'its own cp/<project>/<job> branch, never main'],
404
+ 'scm:pr': ['Open a pull request', ''],
405
+ 'scm:merge': ['Merge', 'only where the org\'s gate allows it'],
406
+ });
407
+ export function grantChoices({ servers = [] } = {}) {
408
+ const item = (id) => ({ id, label: GRANT_INFO[id]?.[0] || id, hint: GRANT_INFO[id]?.[1] || '' });
409
+ const groups = [
410
+ { id: 'reach', label: 'May reach', items: ['data', 'web', 'history', 'mcp'].map(item) },
411
+ ];
412
+ const srv = (Array.isArray(servers) ? servers : []).filter((s) => s && s.id);
413
+ if (srv.length) groups.push({ id: 'servers', label: 'Connected servers', items: srv.map((s) => ({ id: `mcp:${s.id}`, label: s.name || s.id, hint: 'this server only' })) });
414
+ groups.push({ id: 'work', label: 'May do (agent-tool engines only)', items: ['shell', 'fs:write', 'scm:read', 'scm:push', 'scm:pr', 'scm:merge'].map(item) });
415
+ return groups;
416
+ }
417
+
418
+ /** Grants from checked ids: nothing checked is `none`; `mcp` swallows the per-server picks. */
419
+ export function grantsFromChoices(checked = []) {
420
+ const set = new Set((Array.isArray(checked) ? checked : []).filter((g) => g && g !== 'none' && GRANT_RE.test(String(g))));
421
+ if (set.has('mcp')) for (const g of [...set]) if (g.startsWith('mcp:')) set.delete(g);
422
+ return set.size ? [...set] : ['none'];
423
+ }
424
+
425
+ /**
426
+ * The SKILLS a person can attach, from the skills they have: the shared `skills` section
427
+ * (written or installed, `{ command, name?, description?, enabled? }`) and any names an
428
+ * agent already carries that are not there (kept, marked `missing`, so a card never loses a
429
+ * skill the list forgot). A skill's id is its command — what `skill_open` and `fit` match on.
430
+ */
431
+ export function skillChoices(skills = [], { current = [] } = {}) {
432
+ const out = [];
433
+ const seen = new Set();
434
+ for (const s of Array.isArray(skills) ? skills : []) {
435
+ const id = String(s?.command || s?.name || '').trim();
436
+ if (!id || seen.has(id) || s?.enabled === false) continue;
437
+ seen.add(id);
438
+ out.push({ id, label: s.name && s.name !== id ? `${s.name} (/${id})` : `/${id}`, hint: String(s.description || '').slice(0, 120) });
439
+ }
440
+ for (const id of Array.isArray(current) ? current : []) if (id && !seen.has(id)) { seen.add(id); out.push({ id, label: id, hint: 'not among your skills now', missing: true }); }
441
+ return out;
442
+ }