@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,40 @@
1
+ import { m as modelText } from './i18n.js';
2
+ // Client-visible per-session counter of successful team-configuration changes: capability_team_save / _import
3
+ // results and /capability-model-policy saves. The overview re-reads its configuration snapshot only when this
4
+ // number moves, so every tab stays current without running the (conversation-logged) status command on a timer.
5
+ import { isolatedProjection } from './preset-guard.js';
6
+ export const CONFIG_KEY = 'capabilityTeamConfig';
7
+ const SAVE_TOOLS = ['capability_team_save', 'capability_team_import'];
8
+ const POLICY_COMMAND = 'capability-model-policy';
9
+ const isMap = v => !!v && typeof v === 'object' && !Array.isArray(v);
10
+ const schema = (check, label) => ({ parse(value) { if (!check(value)) throw new Error(modelText('config-projection.047', { 0: label })); return value; } });
11
+ const isIds = v => Array.isArray(v) && v.length <= 64 && v.every(id => typeof id === 'string');
12
+ export const configDefinition = {
13
+ key: CONFIG_KEY,
14
+ stateVersion: 1,
15
+ stateSchema: schema(v => isMap(v) && Number.isSafeInteger(v.version) && isIds(v.pending), modelText('config-projection.048')),
16
+ init: () => ({ version: 0, pending: [] }),
17
+ apply(state, event) {
18
+ const d = event.data ?? {};
19
+ // Remember in-flight saves by id; a bounded list, since an id settles at most once.
20
+ if ((event.type === 'tool/call' && SAVE_TOOLS.includes(d.name)) || (event.type === 'command/run' && d.name === POLICY_COMMAND)) {
21
+ const id = event.type === 'tool/call' ? d.callId : d.commandId;
22
+ return typeof id === 'string' ? { ...state, pending: [...state.pending, id].slice(-64) } : state;
23
+ }
24
+ const id = event.type === 'tool/result' ? d.message?.toolCallId : event.type === 'command/done' ? d.commandId : undefined;
25
+ if (typeof id !== 'string' || !state.pending.includes(id)) return state;
26
+ const ok = event.type === 'tool/result' ? !d.message?.isError : d.kind === 'success';
27
+ return { version: state.version + (ok ? 1 : 0), pending: state.pending.filter(p => p !== id) };
28
+ },
29
+ wire: {
30
+ viewSchema: schema(v => isMap(v) && Number.isSafeInteger(v.version), modelText('config-projection.049')),
31
+ view: state => ({ version: state.version }),
32
+ },
33
+ };
34
+ /** Register the counter when the host composes the projection registry; the overview works without it. */
35
+ export function applyConfigProjection(ctx) {
36
+ if (typeof ctx.inject !== 'function') return;
37
+ ctx.inject(['sessionProjections'], scoped => {
38
+ scoped.effect(() => scoped.sessionProjections.register(isolatedProjection(ctx, configDefinition)), 'capability-team: config projection');
39
+ });
40
+ }
package/config.js ADDED
@@ -0,0 +1,411 @@
1
+ import { t, roleText, m as modelText } from './i18n.js';
2
+ // Team configuration is data: the seed below is only the initial default.
3
+ // Every project may override tiers, capabilities and roles through the
4
+ // conversational save tool; nothing here is consulted by name.
5
+ import { validateFileLimits } from './file-limits.js';
6
+ import { validateRotation } from './session-rotation.js';
7
+ import { validateSkillsPolicy } from './teammate-context.js';
8
+ import { validateTokenRules } from './token-discipline.js';
9
+ import { unroutableReason } from './defaults.js';
10
+ import { DEFAULT_ROLES, rosterBase, legacyRoleId } from './roster-defaults.js';
11
+ import { patchSingleModel, effectiveRoute, singleModelChanges } from './single-model.js';
12
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
13
+ const LINE_CONTROL = /[\x00-\x1f\x7f]/;
14
+ const TEXT_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
15
+ const SECTIONS = ['tiers', 'capabilities', 'roles'];
16
+ const CAPABILITY_KEYS = ['job', 'whenToUse', 'provider', 'model', 'tier', 'search', 'fallbacks', 'prefer', 'access', 'maxTokens', 'longContext', 'planFirst', 'skills'];
17
+ const ACCESS_KEYS = ['deny', 'editPaths', 'shell', 'preset'];
18
+ const FAILOVER_KEYS = ['retries', 'initialDelayMs', 'maxDelayMs', 'cooldownMs', 'retryCodes', 'failoverCodes', 'failoverStatuses'];
19
+ const MAX_FALLBACKS = 3;
20
+ const ROLE_KEYS = ['displayName', 'capability', 'description'];
21
+ const WORKFLOW_KEYS = ['mode', 'analysisBudget', 'directBudget', 'freeTools', 'freeModels', 'analysisTools', 'coordinationTools', 'fileLimits', 'confirmDispatch', 'rotation', 'enforceWriteScopes', 'tokens'];
22
+ const FREE_TOOL_LEVELS = ['none', 'read', 'write'];
23
+ const WORKFLOW_MODES = ['enforce', 'remind', 'off'];
24
+ export const CONFIRM_MODES = ['always', 'on-change', 'never'];
25
+ const isMap = v => !!v && typeof v === 'object' && !Array.isArray(v);
26
+ function deepFreeze(value) {
27
+ for (const child of Object.values(value)) if (child && typeof child === 'object') deepFreeze(child);
28
+ return Object.freeze(value);
29
+ }
30
+ export const seed = deepFreeze({
31
+ name: t('config.050'),
32
+ // Same-route retries first (DSH provider retry policy, then `retries` more), then same-tier backups.
33
+ failover: {
34
+ retries: 2, initialDelayMs: 1000, maxDelayMs: 15000, cooldownMs: 60000,
35
+ retryCodes: ['RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN'],
36
+ failoverCodes: ['RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN', 'QUOTA', 'ACCOUNT_QUOTA', 'AUTH', 'INVALID_CREDENTIAL', 'MISSING_CREDENTIAL', 'NO_ADAPTER'],
37
+ failoverStatuses: [401, 403, 404, 408, 429, 500, 502, 503, 504],
38
+ },
39
+ // Lead dispatch workflow: every new user request is analysed and assigned before execution.
40
+ // Tools not listed in either list count as execution (shell, file writes, search, generic subagents).
41
+ workflow: {
42
+ mode: 'enforce',
43
+ analysisBudget: 6, // read-only steps allowed before a dispatch plan exists
44
+ directBudget: 3, // execution steps allowed when the Lead decides to handle a request alone
45
+ // Tools of the free OpenCode model: none (text only) / read (read+search project) / write (also
46
+ // create and edit project files). Never shell, web or outside paths. Project files reach the free
47
+ // model's cloud from `read` up, so the default is none.
48
+ freeTools: 'none',
49
+ // Show the user each assigned role's model and fallbacks before a delegate plan takes effect:
50
+ // always / on-change (the first plan of a session, then only when a route changes) / never.
51
+ confirmDispatch: 'on-change',
52
+ // Edits outside the write scopes of the board tasks a teammate holds are refused (DSH itself only warns).
53
+ enforceWriteScopes: true,
54
+ // Optional order of free models; empty = every free model in OpenCode's catalog, newest first.
55
+ // Entries are re-checked against the catalog at run time, so a paid model can never be listed in.
56
+ freeModels: [],
57
+ // Read-only lookups, including session history, so the Lead can orient itself before planning.
58
+ analysisTools: ['read', 'glob', 'grep', 'read_image', 'session_search', 'session_event_search', 'session_event_read'],
59
+ coordinationTools: ['spawn_teammate', 'send_message', 'wait_agent', 'list_agents', 'interrupt_agent',
60
+ 'team_task_create', 'team_task_get', 'team_task_list', 'team_task_update', 'ask_user_question',
61
+ 'capability_team_read', 'capability_model_catalog', 'capability_team_save', 'capability_team_export', 'capability_team_import',
62
+ 'skill', 'todo_write', 'get_goal', 'create_goal', 'update_goal', 'exit_plan_mode', 'present',
63
+ 'job_list', 'job_output', 'job_kill', 'working_directory'],
64
+ },
65
+ // No channel or model is fixed here: tiers and routes are matched from each user's DSH catalog
66
+ // at runtime (defaults.js). `prefer` only orders vendors inside the matched tier.
67
+ tiers: { high: [], upper: [], medium: [], low: [] },
68
+ capabilities: {
69
+ // whenToUse tells the Lead when to pick a capability; job says what it does.
70
+ orchestrator: { job: roleText('config.051'), tier: 'high', search: true, prefer: ['claude', 'gpt', 'gemini'] },
71
+ 'technical-director': { job: roleText('config.052'), whenToUse: roleText('config.053'), tier: 'upper', search: false, prefer: ['gpt', 'claude', 'gemini'] },
72
+ backend: { job: roleText('config.054'), whenToUse: roleText('config.055'), tier: 'upper', search: false, prefer: ['gpt', 'claude', 'deepseek', 'qwen', 'kimi', 'glm'] },
73
+ frontend: { job: roleText('config.056'), whenToUse: roleText('config.057'), tier: 'medium', search: false, prefer: ['gemini', 'claude', 'gpt'] },
74
+ // access: tools a role may not use, whether it may run commands, and the project paths it may edit.
75
+ research: { job: roleText('config.058'), whenToUse: roleText('config.059'), tier: 'medium', search: true, prefer: ['gemini', 'gpt', 'grok'],
76
+ // read-only is DSH's own sandbox preset: once it is in effect the session may run read-only commands.
77
+ access: { editPaths: [], shell: false, preset: 'read-only' } },
78
+ writer: { job: roleText('config.060'), whenToUse: roleText('config.061'), tier: 'low', search: false, prefer: ['gpt', 'claude', 'gemini', 'deepseek', 'qwen'],
79
+ access: { editPaths: ['**/*.md', '**/*.mdx', '**/*.txt', 'docs/**'], shell: false } },
80
+ qa: { job: roleText('config.062'), whenToUse: roleText('config.063'), tier: 'upper', search: false, prefer: ['gpt', 'claude', 'gemini'] },
81
+ },
82
+ roles: structuredClone(DEFAULT_ROLES), // roster-defaults.js; the hero roster is the optional three-kingdoms theme
83
+ });
84
+ function text(value, max, label, multiline = false) {
85
+ if (typeof value !== 'string' || !value.trim() || value.length > max || (multiline ? TEXT_CONTROL : LINE_CONTROL).test(value)) {
86
+ throw new Error(modelText('config.064', { 0: label, 1: max }));
87
+ }
88
+ return value.trim();
89
+ }
90
+ function entries(map, label) {
91
+ if (!isMap(map)) throw new Error(modelText('config.065', { 0: label }));
92
+ for (const id of Object.keys(map)) if (!ID.test(id)) throw new Error(modelText('config.066', { 0: label, 1: id }));
93
+ return Object.entries(map);
94
+ }
95
+ function onlyKeys(value, allowed, label) {
96
+ if (!isMap(value)) throw new Error(modelText('config.067', { 0: label }));
97
+ const extra = Object.keys(value).filter(k => !allowed.includes(k));
98
+ if (extra.length) throw new Error(modelText('config.068', { 0: label, 1: extra.join(', ') }));
99
+ }
100
+ /** Validate one complete effective configuration and return a normalized copy. */
101
+ export function validateConfig(config, { trustedRoutes } = {}) {
102
+ const name = text(config.name ?? seed.name, 80, modelText('config.069'));
103
+ const tierDrift = [];
104
+ const tiers = {};
105
+ for (const [id, models] of entries(config.tiers, modelText('config.070'))) {
106
+ if (!Array.isArray(models) || models.length > 200) throw new Error(modelText('config.071', { 0: id }));
107
+ const list = models.map(m => text(m, 200, modelText('config.072', { 0: id })));
108
+ if (new Set(list).size !== list.length) throw new Error(modelText('config.073', { 0: id }));
109
+ tiers[id] = list;
110
+ }
111
+ const capabilities = {};
112
+ for (const [id, c] of entries(config.capabilities, modelText('config.074'))) {
113
+ onlyKeys(c, CAPABILITY_KEYS, modelText('config.075', { 0: id }));
114
+ if (!Object.hasOwn(tiers, c.tier)) throw new Error(modelText('config.076', { 0: id, 1: c.tier }));
115
+ if (c.search !== undefined && typeof c.search !== 'boolean') throw new Error(modelText('config.077', { 0: id }));
116
+ if (c.planFirst !== undefined && c.planFirst !== null && typeof c.planFirst !== 'boolean') throw new Error(modelText('config.078', { 0: id }));
117
+ if (c.prefer !== undefined && (!Array.isArray(c.prefer) || c.prefer.length > 20)) throw new Error(modelText('config.079', { 0: id }));
118
+ const common = { job: text(c.job, 2000, modelText('config.080', { 0: id }), true),
119
+ ...(c.whenToUse === undefined || c.whenToUse === null ? {} : { whenToUse: text(c.whenToUse, 500, modelText('config.081', { 0: id })) }), tier: c.tier, search: c.search === true, prefer: (c.prefer ?? []).map(v => text(v, 40, modelText('config.082', { 0: id }))),
120
+ ...(c.access === undefined || c.access === null ? {} : { access: validateAccess(c.access, modelText('config.083', { 0: id })) }),
121
+ ...(c.planFirst === true ? { planFirst: true } : {}),
122
+ // How much of DSH's skill catalog a teammate with this capability sees (teammate-context.js); compact by default.
123
+ ...(c.skills === undefined || c.skills === null ? {} : { skills: validateSkillsPolicy(c.skills, modelText('config.084', { 0: id })) }),
124
+ // Per-session budget of billable tokens (uncached input + cache write + output) for each member with this capability.
125
+ ...(c.maxTokens === undefined || c.maxTokens === null ? {} : { maxTokens: count(c.maxTokens, 1000, 1e10, modelText('config.085', { 0: id })) }) };
126
+ if (c.provider === undefined && c.model === undefined) {
127
+ if (c.fallbacks?.length) throw new Error(modelText('config.086', { 0: id }));
128
+ if (c.longContext) throw new Error(modelText('config.087', { 0: id }));
129
+ capabilities[id] = { ...common, fallbacks: [] };
130
+ continue;
131
+ }
132
+ const model = text(c.model, 200, modelText('config.088', { 0: id }));
133
+ const trust = { trusted: trustedRoutes?.get(id), capability: id, drift: tierDrift };
134
+ adoptIntoTier(tiers, c.tier, model, modelText('config.089', { 0: id }), trust);
135
+ const provider = text(c.provider, 200, modelText('config.090', { 0: id }));
136
+ capabilities[id] = { ...common, provider, model,
137
+ fallbacks: validateFallbacks(c.fallbacks ?? [], { provider, model }, tiers, c.tier, modelText('config.091', { 0: id }), trust),
138
+ ...(c.longContext === undefined || c.longContext === null ? {} : { longContext: validateLongContext(c.longContext, { provider, model }, tiers, c.tier, modelText('config.092', { 0: id }), trust) }) };
139
+ }
140
+ if (!capabilities.orchestrator) throw new Error(modelText('config.093'));
141
+ const roles = {}; const names = new Set();
142
+ for (const [id, r] of entries(config.roles, modelText('config.094'))) {
143
+ onlyKeys(r, ROLE_KEYS, modelText('config.095', { 0: id }));
144
+ if (!Object.hasOwn(capabilities, r.capability)) throw new Error(modelText('config.096', { 0: id, 1: r.capability }));
145
+ if ((id === 'lead') !== (r.capability === 'orchestrator')) throw new Error(modelText('config.097'));
146
+ const displayName = text(r.displayName, 40, modelText('config.098', { 0: id }));
147
+ if (names.has(displayName)) throw new Error(modelText('config.099', { 0: displayName }));
148
+ names.add(displayName);
149
+ roles[id] = { displayName, capability: r.capability, ...(r.description === undefined ? {} : { description: text(r.description, 2000, modelText('config.100', { 0: id }), true) }) };
150
+ }
151
+ if (!roles.lead) throw new Error(modelText('config.101'));
152
+ return { name, failover: validateFailover(config.failover ?? seed.failover), workflow: validateWorkflow(config.workflow ?? seed.workflow), tiers, capabilities, roles, ...(tierDrift.length ? { tierDrift } : {}), ...patchSingleModel(undefined, config.singleModel ?? null) };
153
+ }
154
+ /**
155
+ * An explicitly chosen model may not sit in another tier (no cross-tier routes), but a model the automatic
156
+ * classifier left ungraded is accepted and recorded in the chosen tier: the user's choice is its grading.
157
+ * Besides that, only models no team member can use (unroutableReason), disabled routes and the live catalog
158
+ * check limit what can be chosen; a project can still enable such a model by listing it in `tiers` itself.
159
+ * A route already stored for this capability (trust.trusted) was valid when saved, while grading is recomputed
160
+ * from a catalog that keeps changing: it stays usable and is reported in `tierDrift`, never a load failure.
161
+ */
162
+ function adoptIntoTier(tiers, tier, model, label, trust) {
163
+ if (tiers[tier].includes(model)) return;
164
+ const trusted = trust?.trusted?.has(model);
165
+ const elsewhere = Object.keys(tiers).find(t => t !== tier && tiers[t].includes(model));
166
+ if (elsewhere) {
167
+ if (!trusted) throw new Error(modelText('config.102', { 0: label, 1: model, 2: elsewhere, 3: tier }));
168
+ trust.drift.push({ capability: trust.capability, model, tier, gradedAs: elsewhere });
169
+ return;
170
+ }
171
+ const refused = trusted ? undefined : unroutableReason(model);
172
+ if (refused) throw new Error(modelText('config.103', { 0: label, 1: model, 2: refused, 3: tier }));
173
+ tiers[tier].push(model);
174
+ }
175
+ /** Same-tier route taken once a session's next prompt reaches thresholdTokens; never another tier. */
176
+ function validateLongContext(lc, primary, tiers, tier, label, trust) {
177
+ onlyKeys(lc, ['thresholdTokens', 'provider', 'model'], label);
178
+ const route = { provider: text(lc.provider, 200, modelText('config.104', { 0: label })), model: text(lc.model, 200, modelText('config.105', { 0: label })) };
179
+ adoptIntoTier(tiers, tier, route.model, label, trust);
180
+ if (route.provider === primary.provider && route.model === primary.model) throw new Error(modelText('config.106', { 0: label }));
181
+ return { thresholdTokens: count(lc.thresholdTokens, 8000, 10000000, modelText('config.107', { 0: label })), ...route };
182
+ }
183
+ /** Access policy: denied tool names (or prefix*), shell on/off, and project-relative globs that may be edited (absent = any). */
184
+ function validateAccess(a, label) {
185
+ onlyKeys(a, ACCESS_KEYS, label);
186
+ if (a.shell !== undefined && typeof a.shell !== 'boolean') throw new Error(modelText('config.108', { 0: label }));
187
+ const deny = toolNames(a.deny ?? [], modelText('config.109', { 0: label }));
188
+ for (const name of deny) if (!/^[A-Za-z0-9_.:-]+\*?$/.test(name)) throw new Error(modelText('config.110', { 0: label, 1: name }));
189
+ let editPaths = null;
190
+ if (a.editPaths !== undefined && a.editPaths !== null) {
191
+ if (!Array.isArray(a.editPaths) || a.editPaths.length > 50) throw new Error(modelText('config.111', { 0: label }));
192
+ editPaths = [...new Set(a.editPaths.map(v => text(v, 200, modelText('config.112', { 0: label })).split('\\').join('/').replace(/^\.\//, '')))];
193
+ for (const v of editPaths) {
194
+ if (v.startsWith('/') || /^[A-Za-z]:/.test(v) || v.startsWith('~') || v.split('/').includes('..')) throw new Error(modelText('config.113', { 0: label, 1: v }));
195
+ }
196
+ }
197
+ let preset;
198
+ if (a.preset !== undefined && a.preset !== null) {
199
+ preset = text(a.preset, 64, modelText('config.114', { 0: label }));
200
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(preset) || ['custom', 'auto'].includes(preset)) throw new Error(modelText('config.115', { 0: label }));
201
+ }
202
+ return { deny, editPaths, shell: a.shell !== false, ...(preset ? { preset } : {}) };
203
+ }
204
+ function toolNames(list, label) {
205
+ if (!Array.isArray(list) || list.length > 100) throw new Error(modelText('config.116', { 0: label }));
206
+ return [...new Set(list.map(n => text(n, 80, label)))];
207
+ }
208
+ function oneOf(value, allowed, label) {
209
+ if (!allowed.includes(value)) throw new Error(modelText('config.117', { 0: label, 1: allowed.join(' / ') }));
210
+ return value;
211
+ }
212
+ function validateWorkflow(w) {
213
+ onlyKeys(w, WORKFLOW_KEYS, modelText('config.118'));
214
+ const merged = { ...seed.workflow, ...w };
215
+ if (!WORKFLOW_MODES.includes(merged.mode)) throw new Error(modelText('config.119', { 0: WORKFLOW_MODES.join(' / ') }));
216
+ const out = {
217
+ mode: merged.mode,
218
+ analysisBudget: count(merged.analysisBudget, 0, 50, modelText('config.120')),
219
+ directBudget: count(merged.directBudget, 0, 50, modelText('config.121')),
220
+ freeTools: oneOf(merged.freeTools, FREE_TOOL_LEVELS, modelText('config.122')),
221
+ freeModels: toolNames(merged.freeModels, modelText('config.123')),
222
+ analysisTools: toolNames(merged.analysisTools, modelText('config.124')),
223
+ coordinationTools: toolNames(merged.coordinationTools, modelText('config.125')),
224
+ fileLimits: validateFileLimits(merged.fileLimits),
225
+ confirmDispatch: oneOf(merged.confirmDispatch, CONFIRM_MODES, modelText('config.126')),
226
+ rotation: validateRotation(merged.rotation),
227
+ tokens: validateTokenRules(merged.tokens),
228
+ enforceWriteScopes: (() => { if (typeof merged.enforceWriteScopes !== 'boolean') throw new Error(modelText('config.127')); return merged.enforceWriteScopes; })(),
229
+ };
230
+ const both = out.analysisTools.filter(n => out.coordinationTools.includes(n));
231
+ if (both.length) throw new Error(modelText('config.128', { 0: both.join(', ') }));
232
+ return out;
233
+ }
234
+ /** Ordered same-tier backups: never another tier, never the primary twice. */
235
+ function validateFallbacks(list, primary, tiers, tier, label, trust) {
236
+ if (!Array.isArray(list) || list.length > MAX_FALLBACKS) throw new Error(modelText('config.129', { 0: label, 1: MAX_FALLBACKS }));
237
+ const seen = new Set([`${primary.provider}\u0000${primary.model}`]);
238
+ return list.map((f, i) => {
239
+ onlyKeys(f, ['provider', 'model'], modelText('config.130', { 0: label, 1: i + 1 }));
240
+ const route = { provider: text(f.provider, 200, modelText('config.131', { 0: label })), model: text(f.model, 200, modelText('config.132', { 0: label })) };
241
+ adoptIntoTier(tiers, tier, route.model, modelText('config.133', { 0: label }), trust);
242
+ const key = `${route.provider}\u0000${route.model}`;
243
+ if (seen.has(key)) throw new Error(modelText('config.134', { 0: label, 1: route.provider, 2: route.model }));
244
+ seen.add(key);
245
+ return route;
246
+ });
247
+ }
248
+ function count(value, min, max, label) {
249
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(modelText('config.135', { 0: label, 1: min, 2: max }));
250
+ return value;
251
+ }
252
+ function codes(list, label) {
253
+ if (!Array.isArray(list) || list.length > 50) throw new Error(modelText('config.136', { 0: label }));
254
+ return list.map(c => text(c, 64, label));
255
+ }
256
+ function validateFailover(f) {
257
+ onlyKeys(f, FAILOVER_KEYS, modelText('config.137'));
258
+ const merged = { ...seed.failover, ...f };
259
+ const out = {
260
+ retries: count(merged.retries, 0, 10, modelText('config.138')),
261
+ initialDelayMs: count(merged.initialDelayMs, 100, 60000, modelText('config.139')),
262
+ maxDelayMs: count(merged.maxDelayMs, 100, 120000, modelText('config.140')),
263
+ cooldownMs: count(merged.cooldownMs, 5000, 600000, modelText('config.141')),
264
+ retryCodes: codes(merged.retryCodes, modelText('config.142')),
265
+ failoverCodes: codes(merged.failoverCodes, modelText('config.143')),
266
+ failoverStatuses: merged.failoverStatuses.map(s => count(s, 400, 599, modelText('config.144'))),
267
+ };
268
+ if (out.initialDelayMs > out.maxDelayMs) throw new Error(modelText('config.145'));
269
+ return out;
270
+ }
271
+ // null in a stored override removes the entry; objects merge field by field.
272
+ function layer(base = {}, over = {}) {
273
+ const out = { ...base };
274
+ for (const [id, value] of Object.entries(over ?? {})) {
275
+ if (value === null) delete out[id];
276
+ else out[id] = isMap(value) && isMap(base[id]) ? { ...base[id], ...value } : value;
277
+ }
278
+ return out;
279
+ }
280
+ /** Capability id → models its stored override routes to. Only these may outlive a change in catalog grading. */
281
+ function storedRoutes(overrides) {
282
+ return new Map(Object.entries(overrides?.capabilities ?? {}).filter(([, c]) => isMap(c)).map(([id, c]) => [id, new Set([
283
+ c.model, ...(Array.isArray(c.fallbacks) ? c.fallbacks.map(f => f?.model) : []), c.longContext?.model,
284
+ ].filter(m => typeof m === 'string'))]));
285
+ }
286
+ /**
287
+ * A stored primary without its own fallbacks inherits the automatic ones, which are recomputed from the catalog.
288
+ * Drop those the override makes invalid (the primary itself, or a model of another tier) instead of failing.
289
+ */
290
+ function withoutStaleInherited(capabilities, overrides, tiers) {
291
+ const out = { ...capabilities };
292
+ for (const [id, over] of Object.entries(overrides ?? {})) {
293
+ const c = out[id];
294
+ if (!isMap(over) || !isMap(c) || over.fallbacks !== undefined || typeof over.model !== 'string' || !Array.isArray(c.fallbacks)) continue;
295
+ const inTier = Array.isArray(tiers[c.tier]) ? tiers[c.tier] : [];
296
+ out[id] = { ...c, fallbacks: c.fallbacks.filter(f => !(f?.model === c.model && f?.provider === c.provider) && inTier.includes(f?.model)) };
297
+ }
298
+ return out;
299
+ }
300
+ /**
301
+ * Merge project overrides onto the base configuration and validate the result.
302
+ * `trustedFrom` names the overrides whose stored routes are tolerated when the catalog has regraded them: loading
303
+ * passes the file itself and saving passes the previous file, so a new or changed route is still checked strictly.
304
+ */
305
+ export function effectiveConfig(overrides = {}, base = seed, { trustedFrom } = {}) {
306
+ const tiers = layer(base.tiers, overrides.tiers);
307
+ return validateConfig({
308
+ name: overrides.name ?? base.name, singleModel: overrides.singleModel,
309
+ failover: { ...base.failover, ...(overrides.failover ?? {}) },
310
+ workflow: { ...(base.workflow ?? seed.workflow), ...(overrides.workflow ?? {}) },
311
+ tiers,
312
+ capabilities: withoutStaleInherited(layer(base.capabilities, overrides.capabilities), overrides.capabilities, tiers),
313
+ roles: layer(rosterBase(base.roles, overrides), overrides.roles), // pre-4.0 saves keep the hero roster
314
+ }, trustedFrom ? { trustedRoutes: storedRoutes(trustedFrom) } : {});
315
+ }
316
+ /** Base configuration from preset config: bindings may rename or add seed roles. */
317
+ export function baseConfig(config = {}) {
318
+ if (config.bindings === undefined) return seed;
319
+ if (!isMap(config.bindings)) throw new Error(modelText('config.146'));
320
+ const roles = Object.fromEntries(Object.entries(config.bindings).map(([id, b]) => [id, isMap(b) ? { ...b } : b]));
321
+ return deepFreeze(validateConfig({ ...seed, roles }));
322
+ }
323
+ /**
324
+ * Layer one automatic catalog match (defaults.js autoMatch) onto a base: detected tiers are added,
325
+ * and only capabilities without an explicit route in the base receive the detected primary and backups.
326
+ */
327
+ export function withAutoRoutes(base, match) {
328
+ const tiers = { ...base.tiers };
329
+ for (const [tier, models] of Object.entries(match.tiers)) tiers[tier] = [...new Set([...(tiers[tier] ?? []), ...models])];
330
+ const capabilities = {};
331
+ for (const [id, c] of Object.entries(base.capabilities)) {
332
+ const route = match.routes[id];
333
+ capabilities[id] = c.provider === undefined && route ? { ...c, ...route } : c;
334
+ }
335
+ return deepFreeze(validateConfig({ ...base, tiers, capabilities }));
336
+ }
337
+ /** Upgrade the earlier names/descriptions/routes file format in memory. */
338
+ export function migrateProject(raw) {
339
+ if (!isMap(raw)) throw new Error(modelText('config.147'));
340
+ const { revision = 0, name, failover, workflow, roster, singleModel, names = {}, descriptions = {}, routes = {}, tiers = {}, capabilities = {}, roles = {} } = raw;
341
+ const legacyRoles = {};
342
+ for (const id of new Set([...Object.keys(names), ...Object.keys(descriptions)])) {
343
+ legacyRoles[id] = { ...(names[id] === undefined ? {} : { displayName: names[id] }), ...(descriptions[id] === undefined ? {} : { description: descriptions[id] }) };
344
+ }
345
+ return { revision, ...(roster === undefined ? {} : { roster }), ...(singleModel == null ? {} : { singleModel }), ...(name === undefined ? {} : { name }), ...(failover === undefined ? {} : { failover }), ...(workflow === undefined ? {} : { workflow }), tiers: { ...tiers }, capabilities: { ...routes, ...capabilities }, roles: { ...legacyRoles, ...roles } };
346
+ }
347
+ /**
348
+ * Apply one conversational patch to stored overrides.
349
+ * Per entry: an object merges fields, null reverts to the default, false removes the entry.
350
+ */
351
+ export function applyPatch(overrides, patch) {
352
+ const next = { revision: overrides.revision ?? 0, name: overrides.name, ...(overrides.roster === undefined ? {} : { roster: overrides.roster }), ...patchSingleModel(overrides.singleModel, patch.singleModel) };
353
+ if (patch.name !== undefined) next.name = text(patch.name, 80, modelText('config.148'));
354
+ if (next.name === undefined) delete next.name;
355
+ // failover is one object: fields merge, null restores the preset policy.
356
+ const failover = patch.failover === null ? undefined : patch.failover === undefined ? overrides.failover : { ...(overrides.failover ?? {}), ...patch.failover };
357
+ if (patch.failover !== undefined && patch.failover !== null) onlyKeys(patch.failover, FAILOVER_KEYS, modelText('config.149'));
358
+ if (failover !== undefined) next.failover = failover;
359
+ // workflow follows the same rule: fields merge, null restores the preset policy.
360
+ const workflow = patch.workflow === null ? undefined : patch.workflow === undefined ? overrides.workflow : { ...(overrides.workflow ?? {}), ...patch.workflow };
361
+ if (patch.workflow !== undefined && patch.workflow !== null) onlyKeys(patch.workflow, WORKFLOW_KEYS, modelText('config.150'));
362
+ if (workflow !== undefined) next.workflow = workflow;
363
+ for (const section of SECTIONS) {
364
+ next[section] = { ...(overrides[section] ?? {}) };
365
+ if (patch[section] === undefined) continue;
366
+ for (const [id, value] of entries(patch[section], section)) {
367
+ if (value === null) delete next[section][id];
368
+ else if (value === false) next[section][id] = null;
369
+ else next[section][id] = isMap(value) && isMap(next[section][id]) ? { ...next[section][id], ...value } : value;
370
+ }
371
+ }
372
+ return next;
373
+ }
374
+ /** Optional capability policies a role carries: present only when its capability sets them. */
375
+ const rolePolicies = c => ({ ...(c.access ? { access: c.access } : {}), ...(c.maxTokens ? { maxTokens: c.maxTokens } : {}),
376
+ ...(c.longContext ? { longContext: c.longContext } : {}), ...(c.planFirst ? { planFirst: true } : {}) });
377
+ /** One model-facing row per role, with the route its capability resolves to. */
378
+ export function roleView(effective) {
379
+ return Object.entries(effective.roles).map(([id, r]) => {
380
+ const c = effective.capabilities[r.capability]; const route = effectiveRoute(c, effective.singleModel);
381
+ return { id, displayName: r.displayName, capability: r.capability, description: r.description ?? c.job, ...(c.whenToUse ? { whenToUse: c.whenToUse } : {}), provider: route.provider, model: route.model, tier: c.tier, search: c.search, fallbacks: route.fallbacks, ...rolePolicies(c), ...(route.singleModel ? { singleModel: true } : {}) };
382
+ });
383
+ }
384
+ const SECTION_LABEL = { tiers: t('config.151'), capabilities: t('config.152'), roles: t('config.153') };
385
+ /** Human-readable list of every field that differs between two effective configurations. */
386
+ export function describeChanges(before, after) {
387
+ const lines = [...(before.name === after.name ? [] : [t('config.154', { 0: before.name, 1: after.name })]), ...singleModelChanges(before.singleModel, after.singleModel)];
388
+ const fa = JSON.stringify(before.failover); const fb = JSON.stringify(after.failover);
389
+ if (fa !== fb) lines.push(t('config.155', { 0: fa, 1: fb }));
390
+ const wa = JSON.stringify(before.workflow); const wb = JSON.stringify(after.workflow);
391
+ if (wa !== wb) lines.push(t('config.156', { 0: wa, 1: wb }));
392
+ for (const section of SECTIONS) {
393
+ for (const id of new Set([...Object.keys(before[section]), ...Object.keys(after[section])])) {
394
+ const a = JSON.stringify(before[section][id]); const b = JSON.stringify(after[section][id]);
395
+ if (a !== b) lines.push(`${SECTION_LABEL[section]} ${id}:${a ?? t('config.157')} → ${b ?? t('config.158')}`);
396
+ }
397
+ }
398
+ return lines;
399
+ }
400
+ /** Resolve the native Team identity to one configured role; unknown members fail with guidance. */
401
+ export function resolveRole(membership, effective) {
402
+ const name = !membership || membership.role === 'lead' ? 'lead' : membership.name;
403
+ // A rotation successor `<role>-r<N>` keeps its role's model, access and budget.
404
+ const base = name.replace(/-r\d+$/, '');
405
+ const own = Object.hasOwn(effective.roles, name) || !Object.hasOwn(effective.roles, base) ? name : base;
406
+ const id = legacyRoleId(own, effective.roles) ?? legacyRoleId(base, effective.roles) ?? own; // `lv-bu(-rN)` routes as backend
407
+ const role = Object.hasOwn(effective.roles, id) ? effective.roles[id] : undefined;
408
+ if (!role) throw new Error(modelText('config.159', { 0: id }));
409
+ const c = effective.capabilities[role.capability]; const r = effectiveRoute(c, effective.singleModel);
410
+ return { id, displayName: role.displayName, capability: role.capability, description: role.description ?? c.job, provider: r.provider, model: r.model, tier: c.tier, search: c.search, fallbacks: r.fallbacks, failover: effective.failover, ...rolePolicies(c), ...(r.unassigned ? { unassigned: true } : {}), ...(r.singleModel ? { singleModel: true } : {}) };
411
+ }
@@ -0,0 +1,110 @@
1
+ - insert:
2
+ # Web cards for the capability_* tools (client.js); the Host half is empty.
3
+ - id: capability-team-ui
4
+ name: '@aiwuk/team'
5
+ - id: preset-capability-team
6
+ name: '@deepseek-ai/dsh-agent-preset'
7
+ config:
8
+ id: capability-team
9
+ name: AIWUK Team
10
+ description: A Lead and capability-routed specialists with project-configurable roles and models, using native teams and task boards.
11
+ order: 5
12
+ plugins:
13
+ - id: persona
14
+ name: '@deepseek-ai/dsh-persona'
15
+ config:
16
+ prefix: |
17
+ You work in a capability team. Your role comes from the native team identity and project team configuration; subroles must not claim to be the Lead.
18
+ - id: capability-team-routing
19
+ name: '@aiwuk/team/roles'
20
+ config:
21
+ bindings:
22
+ # Neutral default roster; the Three Kingdoms skin is applied per project via capability_team_import { theme: three-kingdoms }.
23
+ lead: { displayName: Lead, capability: orchestrator }
24
+ architect: { displayName: Architect, capability: technical-director }
25
+ backend: { displayName: Backend, capability: backend }
26
+ frontend: { displayName: Frontend, capability: frontend }
27
+ researcher: { displayName: Researcher, capability: research }
28
+ writer: { displayName: Writer, capability: writer }
29
+ qa: { displayName: QA, capability: qa }
30
+ - id: agent-instructions
31
+ name: '@deepseek-ai/dsh-agent-instructions'
32
+ config:
33
+ maxBytes: 65536
34
+ - id: time-context
35
+ name: '@deepseek-ai/dsh-time-context'
36
+ - id: tool-bash
37
+ name: '@deepseek-ai/dsh-tool-bash'
38
+ disabled: !!js process.platform === 'win32'
39
+ - id: tool-pwsh
40
+ name: '@deepseek-ai/dsh-tool-pwsh'
41
+ disabled: !!js process.platform !== 'win32'
42
+ - id: tool-fs
43
+ name: '@deepseek-ai/dsh-tool-fs'
44
+ - id: tool-fs-search
45
+ name: '@deepseek-ai/dsh-tool-fs-search'
46
+ config:
47
+ sampleOverCapGlobResults: false
48
+ - id: tool-jobs
49
+ name: '@deepseek-ai/dsh-tool-jobs'
50
+ - id: tool-schedule
51
+ name: '@deepseek-ai/dsh-tool-schedule'
52
+ - id: skill-filesystem
53
+ name: '@deepseek-ai/dsh-skill-filesystem'
54
+ - id: tool-skill
55
+ name: '@deepseek-ai/dsh-tool-skill'
56
+ - id: command-goal
57
+ name: '@deepseek-ai/dsh-command-goal'
58
+ - id: tool-goal
59
+ name: '@deepseek-ai/dsh-tool-goal'
60
+ - id: planning
61
+ name: cordis:group
62
+ group: true
63
+ isolate:
64
+ planMode: true
65
+ config:
66
+ - id: plan-mode
67
+ name: '@deepseek-ai/dsh-plan-mode'
68
+ config:
69
+ section: |
70
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
71
+
72
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
73
+
74
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
75
+
76
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
77
+
78
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
79
+
80
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
81
+ - id: compaction
82
+ name: cordis:group
83
+ group: true
84
+ isolate:
85
+ compaction: true
86
+ toolResultPruner: true
87
+ config:
88
+ - id: compaction-basic
89
+ name: '@deepseek-ai/dsh-compaction-basic'
90
+ - id: command-compact
91
+ name: '@deepseek-ai/dsh-command-compact'
92
+ - id: tool-result-pruner
93
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
94
+ config:
95
+ thresholdChars: 8192
96
+ headChars: 4096
97
+ tailChars: 1024
98
+ - id: tool-ask-user
99
+ name: '@deepseek-ai/dsh-tool-ask-user'
100
+ - id: tool-todo
101
+ name: '@deepseek-ai/dsh-tool-todo'
102
+ config:
103
+ allowParallelInProgress: true
104
+ - id: tool-web
105
+ name: '@deepseek-ai/dsh-tool-web'
106
+ config:
107
+ fetch: true
108
+ searchTimeoutMs: 60000
109
+ - id: present
110
+ name: '@deepseek-ai/dsh-tool-present'