@aiwuk/team 0.0.0-stage → 5.0.8

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 (117) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -2
  4. package/README.zh-CN.md +150 -0
  5. package/access.js +93 -0
  6. package/activity-summary.js +36 -0
  7. package/allocation.js +34 -0
  8. package/board-dispatch.js +141 -0
  9. package/brand.js +41 -0
  10. package/client/00-core-zh.js +284 -0
  11. package/client/01-i18n-en.js +265 -0
  12. package/client/02-styles.js +336 -0
  13. package/client/02b-stage-styles.js +65 -0
  14. package/client/03-shared.js +82 -0
  15. package/client/03b-collapsible-list.js +34 -0
  16. package/client/03c-config-panel.js +605 -0
  17. package/client/04-tool-cards.js +110 -0
  18. package/client/04a-dispatch-confirm.js +162 -0
  19. package/client/04b-more-cards.js +78 -0
  20. package/client/04c-model-picker.js +68 -0
  21. package/client/04d-confirm-dock.js +40 -0
  22. package/client/05-policy.js +217 -0
  23. package/client/06-usage.js +195 -0
  24. package/client/06b-model-source.js +67 -0
  25. package/client/07-board-parts.js +312 -0
  26. package/client/07a-role-theme.js +25 -0
  27. package/client/08-artifacts-report.js +375 -0
  28. package/client/08a-task-control.js +126 -0
  29. package/client/08b-stage-sprites.js +97 -0
  30. package/client/08c-stage-colors.js +55 -0
  31. package/client/08c-stage-scene.js +238 -0
  32. package/client/08d-stage-adapter.js +65 -0
  33. package/client/08e-stage-activity.js +83 -0
  34. package/client/09-team-board.js +181 -0
  35. package/client/09b-role-view.js +95 -0
  36. package/client/09c-model-tabs.js +20 -0
  37. package/client/10-overview.js +364 -0
  38. package/client/10b-onboarding.js +121 -0
  39. package/client/11-register.js +249 -0
  40. package/client.js +5053 -0
  41. package/config-projection.js +40 -0
  42. package/config.js +411 -0
  43. package/cordis.patch.yml +110 -0
  44. package/defaults.js +222 -0
  45. package/dispatch-confirm.js +293 -0
  46. package/dispatch-routes-store.js +31 -0
  47. package/failover.js +85 -0
  48. package/file-limits.js +119 -0
  49. package/free-worker.mjs +125 -0
  50. package/http-guard.js +14 -0
  51. package/i18n/roles.en.json +23 -0
  52. package/i18n/roles.zh.json +23 -0
  53. package/i18n/server.en.json +659 -0
  54. package/i18n/server.zh.json +659 -0
  55. package/i18n.js +34 -0
  56. package/icon.svg +7 -0
  57. package/index.js +113 -0
  58. package/integrations-config.js +301 -0
  59. package/integrations-messages.js +62 -0
  60. package/integrations.js +17 -0
  61. package/lead-route.js +30 -0
  62. package/locale/en.json +6 -0
  63. package/locale/zh.json +6 -0
  64. package/long-context.js +30 -0
  65. package/managed-config-route.js +49 -0
  66. package/managed-ref.js +38 -0
  67. package/managed-resolve.js +47 -0
  68. package/member-models.js +84 -0
  69. package/member-task-control.js +242 -0
  70. package/memory-backends.js +89 -0
  71. package/memory-embed.js +70 -0
  72. package/memory-hub.js +140 -0
  73. package/memory-openviking.js +101 -0
  74. package/memory-redact.js +34 -0
  75. package/memory-store.js +270 -0
  76. package/memory-sync-queue.js +28 -0
  77. package/model-policy.js +85 -0
  78. package/model-prefs.js +104 -0
  79. package/observability-api.js +76 -0
  80. package/observability-projection.js +116 -0
  81. package/onboarding.js +47 -0
  82. package/opencode.js +74 -0
  83. package/package.json +155 -4
  84. package/plan-gate.js +92 -0
  85. package/preset-guard.js +47 -0
  86. package/request-evidence.js +41 -0
  87. package/roles.js +105 -0
  88. package/roster-defaults.js +70 -0
  89. package/route-health.js +39 -0
  90. package/runtime-observability.js +78 -0
  91. package/runtime-view.js +21 -0
  92. package/scripts/memory-cli.mjs +138 -0
  93. package/search-engines.js +119 -0
  94. package/search.js +164 -0
  95. package/session-policy.js +59 -0
  96. package/session-rotation.js +206 -0
  97. package/settings-schema.js +40 -0
  98. package/shared/managed-config.js +84 -0
  99. package/single-model.js +44 -0
  100. package/skills/project-role-theme/SKILL.md +46 -0
  101. package/task-bridge.js +32 -0
  102. package/task-control-api.js +36 -0
  103. package/task-control-audit.js +60 -0
  104. package/team.js +101 -0
  105. package/teammate-context.js +120 -0
  106. package/teammate-wait.js +18 -0
  107. package/template.js +46 -0
  108. package/theme-plugin.js +172 -0
  109. package/theme.js +115 -0
  110. package/token-discipline.js +116 -0
  111. package/token-metrics.js +37 -0
  112. package/tool-names.js +10 -0
  113. package/tool-schema.js +23 -0
  114. package/transcribe.js +104 -0
  115. package/ui.js +113 -0
  116. package/usage.js +96 -0
  117. package/workflow.js +246 -0
@@ -0,0 +1,28 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ // Atomic disk snapshots; overflow is explicit and never silently drops older messages.
5
+ export function createSyncQueue(path, { max = 200, warn = () => {} } = {}) {
6
+ let rows = [], revoked = [];
7
+ if (path) {
8
+ try { const saved = JSON.parse(readFileSync(path, 'utf8')); rows = saved.rows ?? []; revoked = saved.revoked ?? []; }
9
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
10
+ }
11
+ const persist = () => {
12
+ if (!path) return;
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ const temp = `${path}.${randomUUID()}.tmp`;
15
+ writeFileSync(temp, JSON.stringify({ rows, revoked }), { mode: 0o600 }); renameSync(temp, path);
16
+ };
17
+ return {
18
+ push(row) {
19
+ if (rows.length >= max) { warn('OpenViking sync queue full; memory retained locally'); throw new Error('OpenViking sync queue full'); }
20
+ rows.push({ ...row, queueId: randomUUID() }); persist();
21
+ },
22
+ peek: (n = 40) => rows.slice(0, n),
23
+ ack(batch) { const ids = new Set(batch.map(r => r.queueId)); rows = rows.filter(r => !ids.has(r.queueId)); persist(); },
24
+ revoke(texts) { revoked = [...new Set([...revoked, ...texts])]; persist(); },
25
+ revoked: () => [...revoked],
26
+ size: () => rows.length,
27
+ };
28
+ }
@@ -0,0 +1,85 @@
1
+ import { m as modelText } from './i18n.js';
2
+ // The (profile-wide) model policy is applied to a resolved role before the failover state chooses a route.
3
+ // Entries are exact provider/model routes, or prefix rules ending in "*" that match either the full
4
+ // route or the bare model id (so "@cf/*" covers every provider). No lower-tier substitution.
5
+ export const routeKey = route => `${route.provider}/${route.model}`;
6
+ export const isPrefixRule = entry => entry.endsWith('*');
7
+ export function validateDisabledRoutes(value) {
8
+ if (!Array.isArray(value) || value.length > 2000) throw new Error(modelText('model-policy.311'));
9
+ for (const v of value) {
10
+ if (typeof v !== 'string' || !v.trim() || v.length > 512 || /[\r\n\0\s]/.test(v)) throw new Error(modelText('model-policy.312'));
11
+ if (isPrefixRule(v)) {
12
+ if (v.length < 3 || v.slice(0, -1).includes('*')) throw new Error(modelText('model-policy.313', { 0: v }));
13
+ } else if (!v.includes('/') || v.includes('*')) throw new Error(modelText('model-policy.314'));
14
+ }
15
+ return [...new Set(value)].sort();
16
+ }
17
+ /** Predicate over provider/model keys for a validated disabled list. */
18
+ export function routeMatcher(disabledRoutes = []) {
19
+ const exact = new Set(), prefixes = [];
20
+ for (const entry of validateDisabledRoutes(disabledRoutes)) (isPrefixRule(entry) ? prefixes.push(entry.slice(0, -1)) : exact.add(entry));
21
+ return key => {
22
+ if (exact.has(key)) return true;
23
+ const model = key.slice(key.indexOf('/') + 1);
24
+ return prefixes.some(p => key.startsWith(p) || model.startsWith(p));
25
+ };
26
+ }
27
+ export function applyModelPolicy(role, disabledRoutes = []) {
28
+ const disabled = routeMatcher(disabledRoutes);
29
+ if (role.unassigned || !role.provider || !role.model) return role;
30
+ const candidates = [{ provider: role.provider, model: role.model }, ...(role.fallbacks ?? [])];
31
+ const allowed = candidates.filter(r => !disabled(routeKey(r)));
32
+ if (!allowed.length) throw new Error(modelText('model-policy.315', { 0: role.displayName ?? role.id }));
33
+ const [primary, ...fallbacks] = allowed;
34
+ // A disabled long-context route is dropped; requests then stay on the allowed primary and backups.
35
+ const { longContext, ...rest } = role;
36
+ const keep = longContext && !disabled(routeKey(longContext)) ? { longContext } : {};
37
+ return { ...rest, ...primary, fallbacks, ...keep, policyExcluded: candidates.filter(r => disabled(routeKey(r))).map(routeKey) };
38
+ }
39
+ /** Roles as requests see them under the policy: excluded routes dropped, roles with no allowed route marked blocked. */
40
+ export function policyRoleView(roles, disabledRoutes = []) {
41
+ return roles.map(role => {
42
+ try { return applyModelPolicy(role, disabledRoutes); }
43
+ catch (error) { return { ...role, provider: undefined, model: undefined, fallbacks: [], blocked: error.message }; }
44
+ });
45
+ }
46
+ export function allocationPreview(roles, disabledRoutes) {
47
+ return roles.map(role => {
48
+ try {
49
+ const resolved = applyModelPolicy(role, disabledRoutes);
50
+ return { id: role.id, displayName: role.displayName ?? role.id,
51
+ status: resolved.provider && resolved.model ? 'eligible' : 'unassigned',
52
+ primary: resolved.provider && resolved.model ? routeKey(resolved) : null,
53
+ fallbacks: (resolved.fallbacks ?? []).map(routeKey) };
54
+ } catch (error) {
55
+ return { id: role.id, displayName: role.displayName ?? role.id, status: 'blocked', primary: null, fallbacks: [], reason: error.message };
56
+ }
57
+ });
58
+ }
59
+ export function modelPolicyRows(tiers, roles, disabledRoutes = [], catalog = []) {
60
+ const disabledList = validateDisabledRoutes(disabledRoutes);
61
+ const isDisabled = routeMatcher(disabledList);
62
+ const rows = new Map();
63
+ for (const entry of catalog) {
64
+ const slash = typeof entry === 'string' ? entry.indexOf('/') : -1;
65
+ const candidate = typeof entry === 'string'
66
+ ? { provider: entry.slice(0, slash), model: entry.slice(slash + 1) } : entry;
67
+ if (!candidate?.provider || !candidate.model || (typeof entry === 'string' && slash < 1)) continue;
68
+ const key = routeKey(candidate);
69
+ rows.set(key, { key, provider: candidate.provider, model: candidate.model,
70
+ tiers: candidate.tier ? [candidate.tier] : [], primaryFor: [], fallbackFor: [],
71
+ disabled: isDisabled(key), verified: false });
72
+ }
73
+ for (const role of roles) {
74
+ if (!role.provider || !role.model) continue;
75
+ for (const [index, route] of [{ provider: role.provider, model: role.model }, ...(role.fallbacks ?? [])].entries()) {
76
+ const key = routeKey(route);
77
+ const row = rows.get(key) ?? { key, provider: route.provider, model: route.model, tiers: [], primaryFor: [], fallbackFor: [], disabled: isDisabled(key), verified: false };
78
+ if (!row.tiers.includes(role.tier)) row.tiers.push(role.tier);
79
+ row[index === 0 ? 'primaryFor' : 'fallbackFor'].push(role.id);
80
+ rows.set(key, row);
81
+ }
82
+ }
83
+ // Catalog tier membership may contain unallocated IDs; don't invent a provider for them.
84
+ return { rows: [...rows.values()], tierCatalog: tiers, disabledRoutes: disabledList, allocations: allocationPreview(roles, disabledRoutes) };
85
+ }
package/model-prefs.js ADDED
@@ -0,0 +1,104 @@
1
+ import { m as modelText } from './i18n.js';
2
+ // Learned model preferences: every confirmed dispatch records, per capability, which primary and fallbacks the
3
+ // user chose. Later confirmation cards preselect a habit (a primary chosen at least twice and in most recent
4
+ // choices) unless that route keeps failing, and suggest saving it as the project default once it is settled.
5
+ // Nothing is applied silently: the card still asks, and the learned data can be cleared.
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ export const PREF_LIMITS = { keep: 20, window: 10, minCount: 2, settled: 3, unhealthyStrikes: 2 };
10
+ const routeKey = r => (r ? `${r.provider}/${r.model}` : null);
11
+ const parse = key => { const i = key.indexOf('/'); return { provider: key.slice(0, i), model: key.slice(i + 1) }; };
12
+ /** Habit for one capability from its recent choices, or undefined. Pure. */
13
+ export function habitOf(choices, limits = PREF_LIMITS) {
14
+ const recent = (choices ?? []).slice(-limits.window);
15
+ if (!recent.length) return undefined;
16
+ const counts = new Map();
17
+ for (const c of recent) counts.set(c.primary, (counts.get(c.primary) ?? 0) + 1);
18
+ const [primary, count] = [...counts].sort((a, b) => b[1] - a[1] || recent.findLastIndex(c => c.primary === b[0]) - recent.findLastIndex(c => c.primary === a[0]))[0];
19
+ if (count < limits.minCount || count * 2 < recent.length) return undefined;
20
+ const last = recent.findLast(c => c.primary === primary);
21
+ const tail = recent.slice(-limits.settled);
22
+ const settled = tail.length === limits.settled && tail.every(c => c.primary === primary && c.fallbacks.join() === last.fallbacks.join());
23
+ return { primary, fallbacks: last.fallbacks, count, of: recent.length, settled };
24
+ }
25
+ /** Health notes for routes: strikes from the failover layer; a route is unhealthy at PREF_LIMITS.unhealthyStrikes. */
26
+ export function unhealthyRoutes(health = {}, limits = PREF_LIMITS) {
27
+ // A credential failure (wrong or missing key) stays bad until fixed, so one is enough.
28
+ return Object.fromEntries(Object.entries(health).filter(([, h]) => h.cooling || h.credential || (h.strikes ?? 0) >= limits.unhealthyStrikes).map(([k, h]) => [k, h.strikes ?? 1]));
29
+ }
30
+ /**
31
+ * Apply learning to one offered row: preselect the habit when every route in it is offered and healthy, and
32
+ * move unhealthy fallbacks to the end. Returns a new row with `learned` notes for the card.
33
+ */
34
+ export function applyPreferences(row, habit, unhealthy) {
35
+ const offeredKeys = new Set([routeKey(row.primary), ...row.fallbacks.map(routeKey), ...row.candidates.map(routeKey)].filter(Boolean));
36
+ const learned = { unhealthy: Object.fromEntries(Object.entries(unhealthy).filter(([k]) => offeredKeys.has(k))) };
37
+ let primary = row.primary, fallbacks = row.fallbacks;
38
+ if (habit && offeredKeys.has(habit.primary) && !unhealthy[habit.primary] && habit.fallbacks.every(k => offeredKeys.has(k))) {
39
+ learned.habit = { count: habit.count, of: habit.of, settled: habit.settled };
40
+ if (habit.primary !== routeKey(primary) || habit.fallbacks.join() !== fallbacks.map(routeKey).join()) {
41
+ primary = parse(habit.primary); fallbacks = habit.fallbacks.filter(k => k !== habit.primary).map(parse); learned.preselected = true;
42
+ }
43
+ }
44
+ fallbacks = [...fallbacks.filter(f => !unhealthy[routeKey(f)]), ...fallbacks.filter(f => unhealthy[routeKey(f)])];
45
+ return { ...row, primary, fallbacks, learned };
46
+ }
47
+ export function createModelPrefs(root) {
48
+ const pathOf = project => join(root, 'model-prefs', `${createHash('sha256').update(project).digest('hex').slice(0, 32)}.json`);
49
+ async function read(project) {
50
+ try { const d = JSON.parse(await readFile(pathOf(project), 'utf8')); return d && typeof d.byCapability === 'object' ? d : { byCapability: {} }; }
51
+ catch (error) { if (error.code === 'ENOENT' || error instanceof SyntaxError) return { byCapability: {} }; throw error; }
52
+ }
53
+ async function write(project, data) {
54
+ const path = pathOf(project);
55
+ await mkdir(join(root, 'model-prefs'), { recursive: true });
56
+ const temp = `${path}.${randomUUID()}.tmp`;
57
+ try { await writeFile(temp, JSON.stringify(data, null, 2) + '\n'); await rename(temp, path); }
58
+ finally { await unlink(temp).catch(() => {}); }
59
+ }
60
+ let queue = Promise.resolve();
61
+ const serial = fn => (queue = queue.catch(() => {}).then(fn));
62
+ return {
63
+ read,
64
+ /** Habit per capability for a project. */
65
+ async habits(project) {
66
+ // Wait for queued records so a habit read right after a confirmation sees it (rename timing differs by OS).
67
+ await queue.catch(() => {});
68
+ const data = await read(project);
69
+ return Object.fromEntries(Object.entries(data.byCapability).map(([cap, v]) => [cap, habitOf(v.choices)]).filter(([, h]) => h));
70
+ },
71
+ /** Record the confirmed routes of one dispatch (each capability once). */
72
+ record(project, routes, at = Date.now()) {
73
+ return serial(async () => {
74
+ const data = await read(project);
75
+ const seen = new Set();
76
+ for (const r of routes) {
77
+ if (!r.primary || seen.has(r.capability)) continue;
78
+ seen.add(r.capability);
79
+ const entry = (data.byCapability[r.capability] ??= { choices: [] });
80
+ entry.choices = [...entry.choices, { primary: routeKey(r.primary), fallbacks: r.fallbacks.map(routeKey), changed: !!r.changed, at }].slice(-PREF_LIMITS.keep);
81
+ }
82
+ await write(project, data);
83
+ });
84
+ },
85
+ clear(project) { return serial(() => unlink(pathOf(project)).catch(error => { if (error.code !== 'ENOENT') throw error; })); },
86
+ };
87
+ }
88
+ const output = { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] };
89
+ /** Lead tool to show or clear what was learned for the current project. */
90
+ export function applyModelPrefsTool(ctx, team, prefs) {
91
+ ctx.effect(() => ctx.tools.register({ name: 'capability_model_prefs', output,
92
+ description: modelText('model-prefs.316'),
93
+ parameters: { type: 'object', properties: { action: { type: 'string', enum: ['read', 'clear'] } }, required: ['action'], additionalProperties: false },
94
+ async execute(args, exec) {
95
+ const project = await team.pin(exec.agent);
96
+ if (args.action === 'clear') {
97
+ if (ctx.agentTeams.tryMembership(exec.agent)?.role !== 'lead') throw new Error(modelText('model-prefs.317'));
98
+ await prefs.clear(project);
99
+ return JSON.stringify({ cleared: true, project });
100
+ }
101
+ return JSON.stringify({ project, habits: await prefs.habits(project) });
102
+ },
103
+ }));
104
+ }
@@ -0,0 +1,76 @@
1
+ import { runtimeBinding } from './runtime-observability.js';
2
+ import { safeId } from './activity-summary.js';
3
+ export const RUNTIME_PATH = '/api/capability-team.runtime';
4
+ export const RUNTIME_STREAM_PATH = `${RUNTIME_PATH}/stream`;
5
+ /** Epoch of the placeholder snapshot of a team lead whose runtime is not bound yet (a draft session before its first message). */
6
+ export const UNBOUND_EPOCH = 'unbound';
7
+ const UNBOUND_POLL_MS = 1000;
8
+ /** Empty but well-formed snapshot: the Team overview renders "no activity yet" instead of an error. */
9
+ export function emptySnapshot(leadSessionId) {
10
+ return { schemaVersion: 1, leadSessionId, epoch: UNBOUND_EPOCH, revision: 0, generatedAt: 0, coverage: { mode: 'unbound', since: 0 }, members: [], taskControl: null };
11
+ }
12
+ function resolve(ctx, request) {
13
+ const url = new URL(request.url), ids = url.searchParams.getAll('sessionId');
14
+ if (ids.length !== 1 || !safeId(ids[0]) || [...url.searchParams.keys()].some(k => k !== 'sessionId')) return { error: 400 };
15
+ // Never resolve/activate a session from user-controlled input.
16
+ const agent = ctx.get?.('agents')?.get(ids[0]);
17
+ if (!agent) return { error: 404 };
18
+ const teams = ctx.get?.('agentTeams');
19
+ if (!teams || !ctx.agentPresets) return { error: 503 };
20
+ const membership = teams.tryMembership(agent);
21
+ if (!membership || membership.role !== 'lead' || ctx.agentPresets?.composedPreset(agent.ctx) !== 'capability-team') return { error: 404 };
22
+ // A draft lead has no runtime until its first message: answer with an empty snapshot, not an error.
23
+ const current = () => runtimeBinding(agent);
24
+ const snapshot = () => { const binding = current(); return binding ? binding.runtime.snapshot(agent.id, teams.listMembers(agent)) : emptySnapshot(agent.id); };
25
+ return { current, agent, snapshot };
26
+ }
27
+ export function handleRuntime(ctx, request, stream = false) {
28
+ if (request.method !== 'GET') return new Response('method not allowed', { status: 405 });
29
+ // In production Connection refuses non-owner peers before invoking any registered fetch route.
30
+ let resolved;
31
+ try { resolved = resolve(ctx, request); } catch { return Response.json({ error: 503 }, { status: 503 }); }
32
+ if (resolved.error) return Response.json({ error: resolved.error }, { status: resolved.error, headers: { 'cache-control': 'no-store' } });
33
+ if (!stream) {
34
+ try { return Response.json(resolved.snapshot(), { headers: { 'cache-control': 'no-store' } }); }
35
+ catch { return Response.json({ error: 503 }, { status: 503 }); }
36
+ }
37
+ let dispose = () => {}, timer = null, poll = null, closed = false, abortCleanup = () => {};
38
+ const body = new ReadableStream({
39
+ start(controller) {
40
+ const encoder = new TextEncoder();
41
+ const close = () => { if (closed) return; closed = true; if (timer) clearTimeout(timer); if (poll) clearInterval(poll); dispose(); request.signal.removeEventListener('abort', close); controller.close(); };
42
+ const emit = () => {
43
+ timer = null; if (closed) return;
44
+ try { controller.enqueue(encoder.encode(`data: ${JSON.stringify(resolved.snapshot())}\n\n`)); }
45
+ catch { close(); }
46
+ };
47
+ emit();
48
+ const attach = binding => { dispose = binding.runtime.onChanged(() => { if (!closed && timer === null) timer = setTimeout(emit, 350); }); };
49
+ const bound = resolved.current();
50
+ if (bound) attach(bound);
51
+ else {
52
+ // Idle until the lead's runtime appears (first message), then push the real snapshot and follow its changes.
53
+ poll = setInterval(() => {
54
+ if (closed) return;
55
+ const binding = resolved.current();
56
+ if (!binding) return;
57
+ clearInterval(poll); poll = null; attach(binding); emit();
58
+ }, UNBOUND_POLL_MS);
59
+ poll.unref?.();
60
+ }
61
+ abortCleanup = () => request.signal.removeEventListener('abort', close);
62
+ request.signal.addEventListener('abort', close, { once: true });
63
+ if (request.signal.aborted) close();
64
+ },
65
+ cancel() { closed = true; if (timer) clearTimeout(timer); if (poll) clearInterval(poll); dispose(); abortCleanup(); },
66
+ });
67
+ return new Response(body, { headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-store', 'x-accel-buffering': 'no' } });
68
+ }
69
+ export function applyObservabilityApi(ctx) {
70
+ ctx.inject?.(['connection', 'agents', 'agentTeams', 'agentPresets'], scoped => {
71
+ for (const [path, stream] of [[RUNTIME_PATH, false], [RUNTIME_STREAM_PATH, true]]) {
72
+ // Host createSharedFetchHandler enforces peer.principal.kind === 'owner'.
73
+ scoped.connection.fetch.register({ path, methods: ['GET'], requestBody: 'buffered', fetch: request => Promise.resolve(handleRuntime(scoped, request, stream)) });
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,116 @@
1
+ import { t } from './i18n.js';
2
+ import { safeId, safeTarget, safeTime, summarizeActivity } from './activity-summary.js';
3
+ import { isolatedProjection } from './preset-guard.js';
4
+ import { OWN_USAGE_KEY, initOwnUsage, applyOwnUsage, ownUsageView } from './usage.js';
5
+ const schema = { parse(v) { if (!v || typeof v !== 'object' || Array.isArray(v)) throw new Error('Invalid observability state'); return v; } };
6
+ const n = x => typeof x === 'number' && x >= 0 ? Math.min(x, Number.MAX_SAFE_INTEGER) : 0;
7
+ const base = (header = {}, inherited = 0) => ({ sessionId: safeId(header.id) ?? 'unknown', cwd: header.cwd ?? null, root: header.cwd ?? null, inherited, cursor: -1 });
8
+ const ignored = (s, e) => !Number.isSafeInteger(e.seq) || e.seq < s.inherited || e.seq <= s.cursor;
9
+ const coord = x => Number.isSafeInteger(x) && x >= 0 ? x : null;
10
+ const callId = e => safeId(e.data?.message?.source?.callId ?? e.data?.message?.toolCallId);
11
+ const definition = (key, init, apply) => ({ key, stateVersion: 1,
12
+ stateSchema: { parse(value) {
13
+ schema.parse(value);
14
+ if (!Number.isSafeInteger(value.inherited) || value.inherited < 0 || !Number.isSafeInteger(value.cursor) || value.cursor < -1 || !value.view || value.view.schemaVersion !== 1) throw new Error('Invalid observability checkpoint');
15
+ if (key === 'capabilityTeamActivity' && (!Array.isArray(value.pending) || value.pending.length > 64 || !Array.isArray(value.view.recent) || value.view.recent.length > 8)) throw new Error('Invalid activity checkpoint');
16
+ if (key === 'capabilityTeamArtifacts' && (!Array.isArray(value.view.items) || value.view.items.length > 20 || !Array.isArray(value.view.changes) || value.view.changes.length > 10)) throw new Error('Invalid artifacts checkpoint');
17
+ if (key === 'capabilityTeamReport' && (!Array.isArray(value.pending) || value.pending.length > 64 || !value.view.usage || !value.view.counters)) throw new Error('Invalid report checkpoint');
18
+ if (Buffer.byteLength(JSON.stringify(value.view)) > 32768) throw new Error('Oversized observability checkpoint');
19
+ return value;
20
+ } }, init, apply, wire: { viewSchema: schema, view: s => s.view } });
21
+ const cwdChange = (s, e) => e.type === 'working-directory/change' ? { ...s, cwd: e.data?.cwd ?? e.data?.path ?? null, cursor: e.seq } : null;
22
+ export const activityDefinition = definition('capabilityTeamActivity', (h, i) => ({ ...base(h, i), pending: [], view: { schemaVersion: 1, phase: 'idle', current: null, recent: [], activeCount: 0, truncated: false } }), (s, e) => {
23
+ if (ignored(s, e)) return s;
24
+ const cwd = cwdChange(s, e); if (cwd) return cwd;
25
+ const d = e.data ?? {}; let pending = s.pending, recent = s.view.recent, phase = s.view.phase, truncated = s.view.truncated;
26
+ if (e.type === 'tool/call') {
27
+ const id = safeId(d.callId); if (!id) return s;
28
+ const item = { id: `${s.sessionId}:${e.seq}`, callId: id, callSeq: e.seq, ...summarizeActivity(d.name, d.arguments, s.cwd, s.root), startedAt: safeTime(e.time), endedAt: null, status: 'running', turn: coord(d.turn), step: coord(d.step) };
29
+ pending = [...pending.filter(p => p.callId !== id), item];
30
+ if (pending.length > 64) { pending = pending.slice(-64); truncated = true; }
31
+ const history = [item, ...recent.filter(p => p.callId !== id)];
32
+ if (history.length > 8) truncated = true;
33
+ recent = history.slice(0, 8); phase = 'tool';
34
+ } else if (e.type === 'tool/result') {
35
+ const id = callId(e), item = pending.find(p => p.callId === id); if (!item) return s;
36
+ pending = pending.filter(p => p.callId !== id);
37
+ const done = { ...item, endedAt: safeTime(e.time), status: d.message?.isError ? 'failed' : 'succeeded' };
38
+ const history = [done, ...recent.filter(p => p.id !== item.id)];
39
+ if (history.length > 8) truncated = true;
40
+ recent = history.slice(0, 8); phase = pending.length ? 'tool' : 'model';
41
+ } else if (e.type === 'step/start') phase = pending.length ? 'tool' : 'model';
42
+ else if (e.type === 'step/end') phase = pending.length ? 'tool' : 'settled';
43
+ else if (e.type === 'turn/end') {
44
+ const ids = new Set(pending.map(p => p.id));
45
+ recent = recent.map(p => ids.has(p.id) ? { ...p, endedAt: safeTime(e.time), status: d.reason?.kind === 'cancelled' || d.reason?.kind === 'interrupted' ? 'interrupted' : 'unknown' } : p);
46
+ pending = []; phase = 'settled';
47
+ } else return s;
48
+ return { ...s, cursor: e.seq, pending, view: { schemaVersion: 1, phase, current: recent[0] ?? null, recent, activeCount: pending.length, truncated } };
49
+ });
50
+ export const artifactsDefinition = definition('capabilityTeamArtifacts', (h, i) => ({ ...base(h, i), view: { schemaVersion: 1, items: [], changes: [], truncated: false } }), (s, e) => {
51
+ if (ignored(s, e)) return s;
52
+ const cwd = cwdChange(s, e); if (cwd) return cwd;
53
+ const d = e.data ?? {}; let { items, changes, truncated } = s.view;
54
+ if (e.type === 'workspace/changes') {
55
+ changes = [{ seq: e.seq, turn: coord(d.turn), at: safeTime(e.time) }, ...changes];
56
+ if (changes.length > 10) { changes = changes.slice(0, 10); truncated = true; }
57
+ } else if (e.type === 'deliverables/presented' && Array.isArray(d.files)) {
58
+ const additions = [];
59
+ for (let index = 0; index < Math.min(d.files.length, 256); index++) {
60
+ const file = d.files[index]; const relative = safeTarget(file?.path, s.cwd, 4096, s.root);
61
+ if (!relative || relative === t('observability-projection.318')) continue;
62
+ additions.push({ id: `${s.sessionId}:${e.seq}:${index}`, sessionId: s.sessionId, presentSeq: e.seq, fileIndex: index, turn: coord(d.turn), at: safeTime(e.time), path: file.path, displayPath: relative.slice(0, 240), changeRef: null });
63
+ }
64
+ items = [...additions, ...items];
65
+ if (items.length > 20 || d.files.length > 256) { items = items.slice(0, 20); truncated = true; }
66
+ // A UTF-8 byte ceiling, not merely a character limit.
67
+ while (Buffer.byteLength(JSON.stringify({ items, changes })) > 31000) { items.pop(); truncated = true; }
68
+ } else return s;
69
+ return { ...s, cursor: e.seq, view: { schemaVersion: 1, items, changes, truncated } };
70
+ });
71
+ const zeroUsage = () => ({ uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 });
72
+ export const reportDefinition = definition('capabilityTeamReport', (h, i) => ({ ...base(h, i), ownUsage: initOwnUsage(i), pending: [], lastTurn: null, view: { schemaVersion: 1, scope: 'own-session-events', firstTurnAt: null, lastTurnEndAt: null, openTurnAt: null, completedTurnMs: 0, turns: 0, steps: 0, stepsStarted: 0, usage: zeroUsage(), usageCoverage: { reportedSamples: 0, attemptsWithoutUsage: 0, coverage: 'unknown' }, counters: { hostRetriesStarted: 0, failedTurns: 0, sendMessageAccepted: 0, boardMutations: h?.origin === 'subagent' ? null : 0 }, truncated: false } }), (s, e) => {
73
+ if (ignored(s, e)) return s;
74
+ const relevant = ['request/header', 'turn/start', 'turn/end', 'step/start', 'step/end', 'assistant/attempt', 'assistant/message', 'llm/retry-started', 'tool/call', 'tool/result', 'team/task'];
75
+ if (!relevant.includes(e.type)) return s;
76
+ const d = e.data ?? {}, v = { ...s.view, usage: { ...s.view.usage }, counters: { ...s.view.counters } };
77
+ const next = { ...s, cursor: e.seq, view: v }; const time = safeTime(e.time);
78
+ const add = (obj, key, value) => { if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) v.truncated = true; const sum = obj[key] + n(value); if (!Number.isFinite(sum) || sum > Number.MAX_SAFE_INTEGER) v.truncated = true; obj[key] = n(sum); };
79
+ if (e.type === 'turn/start') { v.firstTurnAt ??= time; v.openTurnAt = time; }
80
+ if (e.type === 'turn/end') {
81
+ if (v.openTurnAt !== null && time !== null) add(v, 'completedTurnMs', Math.max(0, time - v.openTurnAt));
82
+ v.openTurnAt = null; v.lastTurnEndAt = time; next.pending = [];
83
+ if (d.reason?.kind === 'error') add(v.counters, 'failedTurns', 1);
84
+ }
85
+ if (e.type === 'step/start') { add(v, 'stepsStarted', 1); }
86
+ if (e.type === 'step/end') { add(v, 'steps', 1); if (s.lastTurn !== d.turn) add(v, 'turns', 1); next.lastTurn = d.turn; }
87
+ if (e.type === 'llm/retry-started') { add(v.counters, 'hostRetriesStarted', 1); }
88
+ if (e.type === 'tool/call' && safeId(d.callId)) {
89
+ next.pending = [...s.pending, { id: d.callId, send: d.name === 'send_message' }].slice(-64);
90
+ if (s.pending.length >= 64) v.truncated = true;
91
+ }
92
+ if (e.type === 'tool/result') {
93
+ const id = callId(e), p = s.pending.find(p => p.id === id);
94
+ if (p) { next.pending = s.pending.filter(p => p.id !== id); if (p.send && !d.message?.isError) add(v.counters, 'sendMessageAccepted', 1); }
95
+ }
96
+ if (e.type === 'team/task' && v.counters.boardMutations !== null) add(v.counters, 'boardMutations', 1);
97
+ next.ownUsage = applyOwnUsage(s.ownUsage, e);
98
+ v.usage = next.ownUsage.totals;
99
+ v.truncated ||= next.ownUsage.truncated;
100
+ v.usageCoverage = { reportedSamples: next.ownUsage.reportedSamples, attemptsWithoutUsage: next.ownUsage.attemptsWithoutUsage, coverage: next.ownUsage.coverage };
101
+ return next;
102
+ });
103
+ // Own-session attempt-aware fold. v4 dropped the `timing` view: turn/step wall times come from DSH's native `sessionStats` projection.
104
+ reportDefinition.stateVersion = 4;
105
+ export const ownUsageDefinition = {
106
+ key: OWN_USAGE_KEY, stateVersion: 1, stateSchema: { parse(v) { schema.parse(v); if (!Number.isSafeInteger(v.inherited) || !v.totals || !Array.isArray(v.routes)) throw new Error('Invalid own usage checkpoint'); return v; } },
107
+ init: (_header, inherited) => { const s = initOwnUsage(inherited); return { ...s, view: ownUsageView(s) }; },
108
+ apply: (s, e) => { const next = applyOwnUsage(s, e); return next === s ? s : { ...next, view: ownUsageView(next) }; },
109
+ wire: { viewSchema: schema, view: s => s.view },
110
+ };
111
+ export const observabilityDefinitions = [activityDefinition, artifactsDefinition, reportDefinition, ownUsageDefinition];
112
+ export function applyObservabilityProjections(ctx) {
113
+ ctx.inject?.(['sessionProjections'], scoped => {
114
+ for (const def of observabilityDefinitions) scoped.effect(() => scoped.sessionProjections.register(isolatedProjection(ctx, def)), `capability-team: ${def.key}`);
115
+ });
116
+ }
package/onboarding.js ADDED
@@ -0,0 +1,47 @@
1
+ // Profile-wide first-run status for the Web onboarding card: whether DSH's official Agent Teams service is
2
+ // running, the official bundle's selection state, and how far the live model catalog covers the default tiers.
3
+ // Read-only: enabling the bundle is a user click in the browser through DSH's own pluginManager Remote.
4
+ import { seed } from './config.js';
5
+ import { autoMatch } from './defaults.js';
6
+ export const ONBOARDING_PATH = '/api/capability-team.onboarding';
7
+ export const AGENT_TEAM_BUNDLE = '@deepseek-ai/dsh-experimental-agent-team-profile';
8
+ /** The official bundle row from pluginManager.listBundles(), reduced to what the card needs. */
9
+ export async function teamBundleStatus(pluginManager) {
10
+ if (typeof pluginManager?.listBundles !== 'function') return { known: false };
11
+ try {
12
+ const listed = await pluginManager.listBundles();
13
+ const rows = Array.isArray(listed) ? listed : listed?.ok ? listed.value : [];
14
+ const row = rows.find(b => b?.name === AGENT_TEAM_BUNDLE);
15
+ if (!row) return { known: true, listed: false };
16
+ return { known: true, listed: true, enabled: !!row.enabled, installed: !!row.installed, availability: row.availability ?? null, ...(row.error?.code ? { error: row.error.code } : {}) };
17
+ } catch (error) { return { known: false, error: String(error?.message ?? error).slice(0, 200) }; }
18
+ }
19
+ /** Tier coverage of the seed capabilities against the live catalog; no model is called. */
20
+ export async function tierCoverage(llm, capabilities = seed.capabilities) {
21
+ if (typeof llm?.listProviders !== 'function') return { known: false };
22
+ const entries = []; const errors = [];
23
+ for (const provider of llm.listProviders()) {
24
+ try { for (const m of await llm.listModels(provider.id)) entries.push({ provider: provider.id, model: m.id }); }
25
+ catch { errors.push(provider.id); }
26
+ }
27
+ const { report } = autoMatch(entries, capabilities);
28
+ const unassigned = report.filter(r => r.status === 'unassigned').map(r => r.capability);
29
+ return { known: true, models: entries.length, capabilities: report.length, covered: report.length - unassigned.length, unassigned, ...(errors.length ? { catalogErrors: errors } : {}) };
30
+ }
31
+ /** Everything the onboarding card shows; each part degrades to `known: false` instead of failing. */
32
+ export async function onboardingStatus(ctx) {
33
+ const selection = (() => { try { return ctx.get?.('agentDefaultModel')?.currentSelection?.(); } catch { return undefined; } })();
34
+ return {
35
+ teamServicePresent: !!ctx.get?.('agentTeams'),
36
+ bundle: await teamBundleStatus(ctx.get?.('pluginManager')),
37
+ coverage: await tierCoverage(ctx.get?.('llm')).catch(() => ({ known: false })),
38
+ ...(selection?.provider && selection?.model ? { defaultModel: `${selection.provider}/${selection.model}` } : {}),
39
+ };
40
+ }
41
+ export function applyOnboardingApi(ctx) {
42
+ ctx.inject?.(['connection'], scoped => scoped.connection.fetch.register({ path: ONBOARDING_PATH, methods: ['GET'], requestBody: 'buffered',
43
+ fetch: async () => {
44
+ try { return Response.json(await onboardingStatus(ctx), { headers: { 'cache-control': 'no-store' } }); }
45
+ catch { return Response.json({ code: 'UNAVAILABLE' }, { status: 503 }); }
46
+ } }));
47
+ }
package/opencode.js ADDED
@@ -0,0 +1,74 @@
1
+ import { m as modelText } from './i18n.js';
2
+ // Locating OpenCode and its free models on any platform. Nothing here names a user, a machine or a
3
+ // fixed model list: the executable comes from an env override or PATH, the models from OpenCode's own
4
+ // catalog cache, filtered to zero-cost, non-deprecated entries.
5
+ import { access, readFile, constants } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { posix, win32 } from 'node:path';
8
+
9
+ const ENV_EXE = 'CAPABILITY_TEAM_OPENCODE_EXECUTABLE';
10
+ const ENV_CATALOG = 'CAPABILITY_TEAM_OPENCODE_MODELS';
11
+ const pathApi = platform => (platform === 'win32' ? win32 : posix);
12
+
13
+ /** Candidate native executables, most specific first. Shell shims (.cmd/.ps1) are never returned. */
14
+ export function openCodeCandidates({ env = process.env, platform = process.platform, home = homedir() } = {}) {
15
+ const p = pathApi(platform);
16
+ const dirs = (env.PATH ?? env.Path ?? '').split(p.delimiter).filter(Boolean);
17
+ const list = [];
18
+ for (const dir of dirs) {
19
+ if (platform === 'win32') {
20
+ list.push(p.join(dir, 'opencode.exe'));
21
+ // npm on Windows puts a .cmd shim on PATH; the native binary sits in the package next to it.
22
+ list.push(p.join(dir, 'node_modules', 'opencode-ai', 'bin', 'opencode.exe'));
23
+ } else list.push(p.join(dir, 'opencode'));
24
+ }
25
+ // Location used by the official install script on every platform.
26
+ list.push(p.join(home, '.opencode', 'bin', platform === 'win32' ? 'opencode.exe' : 'opencode'));
27
+ return [...new Set(list)];
28
+ }
29
+
30
+ export async function findOpenCode({ env = process.env, platform = process.platform, home = homedir(), exists = defaultExists(platform) } = {}) {
31
+ if (env[ENV_EXE]) return env[ENV_EXE];
32
+ for (const file of openCodeCandidates({ env, platform, home })) if (await exists(file)) return file;
33
+ throw new Error(modelText('opencode.319', { 0: ENV_EXE }));
34
+ }
35
+ function defaultExists(platform) {
36
+ return file => access(file, platform === 'win32' ? constants.F_OK : constants.X_OK).then(() => true, () => false);
37
+ }
38
+
39
+ /** OpenCode keeps its model catalog under the XDG cache directory on all platforms. */
40
+ export function catalogFile({ env = process.env, platform = process.platform, home = homedir() } = {}) {
41
+ if (env[ENV_CATALOG]) return env[ENV_CATALOG];
42
+ const p = pathApi(platform);
43
+ return p.join(env.XDG_CACHE_HOME || p.join(home, '.cache'), 'opencode', 'models.json');
44
+ }
45
+
46
+ const isZero = v => v === undefined || v === 0;
47
+ /** Zero-cost, non-deprecated, tool-capable models of OpenCode's own provider, newest first. */
48
+ export function freeModelsFromCatalog(catalog) {
49
+ const models = catalog?.opencode?.models ?? {};
50
+ return Object.entries(models)
51
+ .filter(([, m]) => m && m.cost && m.cost.input === 0 && m.cost.output === 0 && isZero(m.cost.cache_read) && isZero(m.cost.cache_write)
52
+ && m.status !== 'deprecated' && m.tool_call !== false)
53
+ .sort(([a, x], [b, y]) => String(y.release_date ?? '').localeCompare(String(x.release_date ?? '')) || a.localeCompare(b))
54
+ .map(([id]) => `opencode/${id}`);
55
+ }
56
+
57
+ /**
58
+ * Models the free channel may use. A configured list only orders and narrows; every entry must still be
59
+ * free in the catalog, so a paid model can never be configured in by mistake.
60
+ */
61
+ export async function resolveFreeModels(configured = [], { file = catalogFile(), read = readFile } = {}) {
62
+ let catalog;
63
+ try { catalog = JSON.parse(await read(file, 'utf8')); } catch (error) {
64
+ throw new Error(modelText('opencode.320', { 0: file, 1: error.code ?? error.message, 2: ENV_CATALOG }));
65
+ }
66
+ const free = freeModelsFromCatalog(catalog);
67
+ if (!configured.length) {
68
+ if (!free.length) throw new Error(modelText('opencode.321'));
69
+ return free;
70
+ }
71
+ const rejected = configured.filter(m => !free.includes(m));
72
+ if (rejected.length) throw new Error(modelText('opencode.322', { 0: rejected.join('、'), 1: free.join('、') || modelText('opencode.323') }));
73
+ return configured;
74
+ }