@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,312 @@
1
+ function boardCards(roles, activity, allocations) {
2
+ const members = activity?.members ?? [];
3
+ const tasks = activity?.tasks ?? [];
4
+ const primary = new Map((allocations ?? []).map(a => [a.id, a.primary]));
5
+ const allocById = new Map((allocations ?? []).map(a => [a.id, a]));
6
+ const allocNames = new Map((allocations ?? []).map(a => [a.id, a.displayName]).filter(([_, name]) => Boolean(name)));
7
+ const used = new Set();
8
+ const cards = roles.map(role => {
9
+ const lead = role.id === 'lead' || role.capability === 'orchestrator';
10
+ const member = members.find(m => !used.has(m) && (lead ? (m.role === 'lead' || baseName(m.name) === 'lead') : (m.name === role.id || baseName(m.name) === role.id)));
11
+ if (member) used.add(member);
12
+ const resolvedName = (role.displayName && role.displayName !== role.id)
13
+ ? role.displayName
14
+ : (allocNames.get(role.id) ?? role.displayName ?? role.id);
15
+ return { id: role.id, name: resolvedName, lead, member, role };
16
+ });
17
+ for (const member of members) {
18
+ if (!used.has(member)) {
19
+ const lead = member.role === 'lead';
20
+ const bName = lead ? 'lead' : baseName(member.name);
21
+ const matchedRole = roles.find(r => r.id === bName || (lead && (r.id === 'lead' || r.capability === 'orchestrator')));
22
+ const resolvedName = (matchedRole?.displayName && matchedRole.displayName !== matchedRole.id)
23
+ ? matchedRole.displayName
24
+ : (allocNames.get(bName) ?? matchedRole?.displayName ?? member.name);
25
+ cards.push({ id: matchedRole?.id ?? member.name, name: resolvedName, lead, member, role: matchedRole ?? null });
26
+ }
27
+ }
28
+ return cards.map(card => {
29
+ const owner = card.member?.name ?? card.id;
30
+ const bId = card.id ? baseName(card.id) : '';
31
+ const mine = tasks.filter(task => (task.ownerName === owner || task.ownerName === card.id || (task.ownerName && baseName(task.ownerName) === bId)) && task.status !== 'completed');
32
+ const status = card.member?.status ?? 'inactive';
33
+ const task = mine.find(task => task.status === 'in_progress') ?? mine[0];
34
+ // Item 9: "configured" (role.provider/role.model, or this role's primary allocation) is distinct from
35
+ // "last request" (this plugin's own capabilityTeamUsage route, put on card.member.model by withTrustedRoutes;
36
+ // the host roster's model is never used). They are the
37
+ // same route most of the time, but failover/long-context switches or a stale config can make them differ —
38
+ // that difference must stay visible rather than being collapsed into one badge.
39
+ const configuredModel = primary.get(card.id) ?? (card.role?.provider ? `${card.role.provider}/${card.role.model}` : null);
40
+ const lastRequestModel = card.member?.model ? [card.member.provider, card.member.model].filter(Boolean).join('/') : null;
41
+ const fallbackRoutes = (Array.isArray(card.role?.fallbacks) ? card.role.fallbacks : []).map(routeText).filter(Boolean);
42
+ return { ...card, status,
43
+ model: lastRequestModel ?? configuredModel,
44
+ configuredModel, lastRequestModel,
45
+ // The Lead runs on the user's pick, so its configured route is only a default and is not contrasted.
46
+ modelDiffers: !card.lead && !!(lastRequestModel && configuredModel && lastRequestModel !== configuredModel),
47
+ lastRequestIsFallback: !!(lastRequestModel && fallbackRoutes.includes(lastRequestModel)),
48
+ // Shown model's source: 'request' (trusted route) or 'config' (configured primary, labelled as such).
49
+ modelSource: lastRequestModel ? 'request' : configuredModel ? 'config' : null,
50
+ routeWarn: routeOutsideConfig(lastRequestModel, card.role, allocById.get(card.id)),
51
+ liveModel: !!card.member?.model, task, queued: mine.length,
52
+ // A teammate is "delegated to" while it works or holds an in-progress task from the Lead.
53
+ delegated: !card.lead && (status === 'running' || status === 'provisioning' || task?.status === 'in_progress') };
54
+ });
55
+ }
56
+ /** Static model badge mapping: provider/model -> alias & rarity rank (legendary/epic/rare/cloth/default). */
57
+ function modelBadgeInfo(route, t) {
58
+ if (!route) return null;
59
+ const slash = route.indexOf('/');
60
+ const provider = slash > 0 ? route.slice(0, slash) : '';
61
+ const rawModel = slash > 0 ? route.slice(slash + 1) : route;
62
+ const p = provider.toLowerCase();
63
+ const m = rawModel.toLowerCase();
64
+
65
+ // Free / OpenCode channels use the cloth grade
66
+ if (p === 'opencode' || m.includes('free') || p.includes('free') || m.startsWith('free-')) {
67
+ return { alias: rawModel, grade: 'cloth', gradeLabel: t ? t('model.grade.cloth') : en['model.grade.cloth'] };
68
+ }
69
+ // claude-opus-* uses the legendary grade
70
+ if (m.includes('claude-opus') || m.includes('claude-3-opus') || m.includes('opus')) {
71
+ return { alias: rawModel, grade: 'legendary', gradeLabel: t ? t('model.grade.legendary') : en['model.grade.legendary'] };
72
+ }
73
+ // gpt-6.1-sol uses the epic grade
74
+ if (m.includes('gpt-6.1-sol') || m.includes('-sol')) {
75
+ return { alias: rawModel, grade: 'epic', gradeLabel: t ? t('model.grade.epic') : en['model.grade.epic'] };
76
+ }
77
+ // gemini-*-flash uses the rare grade
78
+ if (m.includes('gemini') && m.includes('flash')) {
79
+ return { alias: rawModel, grade: 'rare', gradeLabel: t ? t('model.grade.rare') : en['model.grade.rare'] };
80
+ }
81
+ // Unknown models retain their original name and default grade
82
+ return { alias: route, grade: 'default', gradeLabel: null };
83
+ }
84
+
85
+ /** Status bubble for cards: task subject + status text, or idle static lines. */
86
+ function statusBubble(card, t, live) {
87
+ const isRunning = card.status === 'running';
88
+ const task = card.task;
89
+ const memberId = card.member?.id;
90
+ const act = memberId ? live?.projections?.(memberId)?.capabilityTeamActivity : undefined;
91
+
92
+ // Inspect the capabilityTeamActivity projection
93
+ if (act && typeof act === 'object') {
94
+ if (act.schemaVersion !== 1) {
95
+ return h('div', { className: 'ctm-bd-bubble' },
96
+ h('span', { className: 'ctm-sub ctm-warn' }, t('version.unsupported')));
97
+ }
98
+ const phase = act.phase;
99
+ const cur = act.current;
100
+ const activeCount = typeof act.activeCount === 'number' ? act.activeCount : 0;
101
+ const isHistoryUnsettled = !isRunning && cur && cur.status === 'running';
102
+ const recentList = Array.isArray(act.recent) ? act.recent : [];
103
+
104
+ let headBadge = null;
105
+ let bodyContent = null;
106
+
107
+ if (isHistoryUnsettled) {
108
+ headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'warn' }, t('activity.unsettled'));
109
+ bodyContent = cur ? [cur.summary, cur.target ? ` (${cur.target})` : null].filter(Boolean).join('') : t('activity.unsettled');
110
+ } else if (phase === 'tool') {
111
+ headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'brand' },
112
+ activeCount > 1 ? fmt(t('activity.parallel'), { n: activeCount }) : (cur?.tool ? fmt(t('activity.tool'), { tool: cur.tool }) : t('live.bubbleRunning')));
113
+ const summaryText = cur?.summary ?? '';
114
+ const targetText = cur?.target ? ` ${cur.target}` : '';
115
+ bodyContent = summaryText || targetText ? `${summaryText}${targetText}` : (task?.subject ?? t('live.busyNoTask'));
116
+ } else if (phase === 'model' && activeCount === 0) {
117
+ headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, t('activity.processing'));
118
+ bodyContent = task?.subject ?? t('live.busyNoTask');
119
+ } else if (phase === 'idle') {
120
+ const idleText = label(t, `live.${card.status}`, card.status);
121
+ headBadge = h('span', { className: 'ctm-sub' }, idleText);
122
+ const qKey = card.role?.quoteKey ?? characterQuoteKey(card.role?.id ?? card.id);
123
+ const quote = t(qKey);
124
+ bodyContent = cur?.summary ? `${cur.summary} · “${quote}”` : `“${quote}”`;
125
+ }
126
+
127
+ if (headBadge && bodyContent) {
128
+ return h('div', { className: 'ctm-bd-bubble' },
129
+ h('div', { className: 'ctm-bd-bubble-head' },
130
+ headBadge,
131
+ task ? h('span', { className: 'ctm-sub' }, `#${task.id}`) : null),
132
+ h('div', { className: 'ctm-bd-bubble-body ctm-bd-subject' }, bodyContent),
133
+ recentList.length ? h('details', { className: 'ctm-act-recent' },
134
+ h('summary', null, fmt(t('activity.recent'), { n: recentList.length })),
135
+ h('div', { className: 'ctm-act-list' },
136
+ h(CollapsibleList, {
137
+ items: recentList,
138
+ limit: 5,
139
+ t,
140
+ renderItem: (item, idx) => h('div', { key: item.id ?? item.callId ?? idx, className: 'ctm-act-row' },
141
+ h('span', { title: item.target ?? '' }, `${item.tool ? item.tool + ': ' : ''}${item.summary ?? ''}${item.target ? ' ' + item.target : ''}`),
142
+ h('span', { className: 'ctm-badge', 'data-tone': item.status === 'succeeded' ? 'success' : item.status === 'failed' ? 'error' : undefined }, item.status)),
143
+ }))) : null);
144
+ }
145
+ }
146
+
147
+ if (task) {
148
+ const statusText = isRunning ? t('live.bubbleRunning') : t('live.bubbleWaiting');
149
+ return h('div', { className: 'ctm-bd-bubble', title: task.subject },
150
+ h('div', { className: 'ctm-bd-bubble-head' },
151
+ h('span', { className: 'ctm-badge', 'data-tone': isRunning ? 'brand' : 'warn' }, statusText),
152
+ h('span', { className: 'ctm-sub' }, `#${task.id}`),
153
+ card.queued > 1 ? h('span', { className: 'ctm-sub' }, `+${card.queued - 1}`) : null),
154
+ h('div', { className: 'ctm-bd-bubble-body ctm-bd-subject' }, task.subject));
155
+ }
156
+
157
+ if (isRunning) {
158
+ return h('div', { className: 'ctm-bd-bubble' },
159
+ h('div', { className: 'ctm-bd-bubble-head' },
160
+ h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, t('live.bubbleRunning'))),
161
+ h('div', { className: 'ctm-bd-bubble-body ctm-sub' }, t('live.busyNoTask')));
162
+ }
163
+
164
+ const qKey = card.role?.quoteKey ?? characterQuoteKey(card.role?.id ?? card.id);
165
+ const quote = t(qKey);
166
+ const idleText = label(t, `live.${card.status}`, card.status);
167
+ return h('div', { className: 'ctm-bd-bubble' },
168
+ h('div', { className: 'ctm-bd-bubble-head' },
169
+ h('span', { className: 'ctm-sub' }, idleText)),
170
+ h('div', { className: 'ctm-bd-bubble-body ctm-bd-quote' }, `“${quote}”`));
171
+ }
172
+
173
+ /** Context pressure & token budget meters for one board card. */
174
+ function cardMeters(card, live, usage, t) {
175
+ const memberId = card.member?.id;
176
+ const proj = memberId ? live?.projections?.(memberId) : undefined;
177
+
178
+ // 1) Context pressure: projectedTokens / contextWindow (or pressureTokens)
179
+ const cp = proj?.contextPressure;
180
+ const projected = cp?.projectedTokens ?? cp?.pressureTokens;
181
+ const contextWindow = cp?.contextWindow;
182
+ const hasPressure = typeof projected === 'number' && typeof contextWindow === 'number' && contextWindow > 0;
183
+
184
+ // 2) Budget meter: reused from usageRows / maxTokens, rendered only when maxTokens exists
185
+ const max = card.role?.maxTokens;
186
+ let budgetNode = null;
187
+ if (max && max > 0) {
188
+ const uRow = usage?.find(u => (memberId && u.id === memberId) || u.name === card.id || (card.lead && u.name === 'lead'));
189
+ const ownUsage = proj?.capabilityTeamUsage;
190
+ // Strict Unknown contract: a budget bar may only render from a row that is itself not Unknown
191
+ // (coverage !== 'unknown' and reportedSamples > 0); a truthy `totals` object alone is not enough.
192
+ const totals = !isUnknownUsage(uRow) ? uRow.totals : (ownUsage && !isUnknownUsage(ownUsage) ? ownUsage.totals : null);
193
+ if (totals) {
194
+ const used = billable(totals);
195
+ const bRatio = Math.min(1, used / max);
196
+ const bTone = used >= max ? 'error' : bRatio >= 0.8 ? 'warn' : undefined;
197
+ budgetNode = h('div', { className: 'ctm-bd-meter-row', key: 'budget' },
198
+ h('div', { className: 'ctm-bd-meter-labels' },
199
+ h('span', null, t('live.budgetTitle')),
200
+ h('span', { className: 'ctm-us-num' }, fmt(t('usage.budget'), { used: compact(used), max: compact(max) }))),
201
+ h('div', { className: 'ctm-us-meter', 'data-tone': bTone, role: 'presentation' },
202
+ h('i', { style: { width: `${(bRatio * 100).toFixed(1)}%` } })));
203
+ }
204
+ }
205
+
206
+ let cpNode = null;
207
+ if (hasPressure) {
208
+ const cpRatio = Math.min(1, Math.max(0, projected / contextWindow));
209
+ const cpTone = cpRatio >= 0.9 ? 'error' : cpRatio >= 0.8 ? 'warn' : undefined;
210
+ cpNode = h('div', { className: 'ctm-bd-meter-row', key: 'pressure' },
211
+ h('div', { className: 'ctm-bd-meter-labels' },
212
+ h('span', null, t('live.contextPressure')),
213
+ h('span', { className: 'ctm-us-num' }, `${(cpRatio * 100).toFixed(0)}%`)),
214
+ h('div', { className: 'ctm-us-meter', 'data-tone': cpTone, role: 'presentation' },
215
+ h('i', { style: { width: `${(cpRatio * 100).toFixed(1)}%` } })));
216
+ } else {
217
+ // Missing data shows unknown without false meters
218
+ cpNode = h('div', { className: 'ctm-bd-meter-row', key: 'pressure-unknown' },
219
+ h('div', { className: 'ctm-bd-meter-labels' },
220
+ h('span', null, t('live.contextPressure')),
221
+ h('span', { className: 'ctm-sub' }, t('live.contextUnknown'))));
222
+ }
223
+
224
+ return h('div', { className: 'ctm-bd-meters' }, cpNode, budgetNode);
225
+ }
226
+
227
+ /** Compute longest dependency chain in DAG with cycle detection; robust on empty graphs. */
228
+ function computeLongestChain(tasks) {
229
+ if (!Array.isArray(tasks) || !tasks.length) return [];
230
+ const taskMap = new Map();
231
+ for (const tk of tasks) if (tk?.id != null) taskMap.set(String(tk.id), tk);
232
+ if (!taskMap.size) return [];
233
+
234
+ const adj = new Map();
235
+ for (const id of taskMap.keys()) adj.set(id, []);
236
+ for (const tk of taskMap.values()) {
237
+ const v = String(tk.id);
238
+ const blockers = Array.isArray(tk.blockedBy)
239
+ ? tk.blockedBy
240
+ : (typeof tk.blockedBy === 'string' || typeof tk.blockedBy === 'number') && tk.blockedBy
241
+ ? [tk.blockedBy]
242
+ : [];
243
+ for (const b of blockers) {
244
+ const u = String(b);
245
+ if (taskMap.has(u)) {
246
+ adj.get(u).push(v);
247
+ }
248
+ }
249
+ }
250
+
251
+ function dfs(u, visiting) {
252
+ visiting.add(u);
253
+ let bestChild = [];
254
+ const neighbors = adj.get(u) || [];
255
+ for (const v of neighbors) {
256
+ if (visiting.has(v)) continue; // Skip back-edge to avoid duplicate nodes and cycles
257
+ const path = dfs(v, visiting);
258
+ if (path.length > bestChild.length) {
259
+ bestChild = path;
260
+ }
261
+ }
262
+ visiting.delete(u);
263
+ return [u, ...bestChild];
264
+ }
265
+
266
+ let longest = [];
267
+ for (const id of taskMap.keys()) {
268
+ const path = dfs(id, new Set());
269
+ if (path.length > longest.length) {
270
+ longest = path;
271
+ }
272
+ }
273
+ return longest;
274
+ }
275
+
276
+ /** Task dependency graph/list with highlighted longest dependency chain. */
277
+ function taskDependencyView(tasks, t) {
278
+ if (!tasks || !tasks.length) return null;
279
+
280
+ const longestChain = computeLongestChain(tasks);
281
+ const longestSet = new Set(longestChain);
282
+ const hasDeps = tasks.some(tk => {
283
+ if (Array.isArray(tk?.blockedBy)) return tk.blockedBy.length > 0;
284
+ return (typeof tk?.blockedBy === 'string' || typeof tk?.blockedBy === 'number') && String(tk.blockedBy).trim().length > 0;
285
+ });
286
+
287
+ return h('div', { className: 'ctm-bd-deps' },
288
+ h('div', { className: 'ctm-bd-deps-head' },
289
+ h('span', { className: 'ctm-label' }, t('live.taskDeps')),
290
+ longestChain.length >= 2 ? h('div', { className: 'ctm-bd-deps-chain', title: t('live.longestChain') },
291
+ h('strong', null, `${t('live.longestChain')}: `),
292
+ longestChain.map((id, idx) => h(React.Fragment, { key: id },
293
+ idx > 0 ? h('span', { className: 'ctm-sub' }, ' → ') : null,
294
+ h('span', { className: 'ctm-us-num' }, `#${id}`)))) : null),
295
+ !hasDeps ? h('p', { className: 'ctm-sub' }, t('live.noDeps'))
296
+ : h('div', { className: 'ctm-bd-deps-list' }, ...tasks.map(tk => {
297
+ const isLongest = longestSet.has(String(tk.id));
298
+ const blockers = Array.isArray(tk.blockedBy)
299
+ ? tk.blockedBy
300
+ : (typeof tk.blockedBy === 'string' || typeof tk.blockedBy === 'number') && tk.blockedBy
301
+ ? [tk.blockedBy]
302
+ : [];
303
+ return h('div', { key: tk.id, className: 'ctm-bd-dep-node', ...(isLongest ? { 'data-longest': '' } : {}) },
304
+ h('div', { className: 'ctm-bd-dep-top' },
305
+ h('span', { className: 'ctm-us-num' }, `#${tk.id}`),
306
+ h('span', { className: 'ctm-badge', 'data-tone': tk.status === 'completed' ? 'success' : tk.status === 'in_progress' ? 'brand' : undefined }, tk.status)),
307
+ h('span', { className: 'ctm-bd-dep-subj', title: tk.subject }, tk.subject),
308
+ blockers.length ? h('span', { className: 'ctm-bd-dep-blocked' }, `blockedBy: ${blockers.map(b => '#' + b).join(', ')}`) : null);
309
+ })));
310
+ }
311
+
312
+ /** Artifacts rack & Changes view (MVP-B). */
@@ -0,0 +1,25 @@
1
+ // ---------- role skins: neutral default names are localized; Three Kingdoms quotes apply only to that roster ----------
2
+ const NEUTRAL_ROLE_NAMES = { lead: 'Lead', architect: 'Architect', backend: 'Backend', frontend: 'Frontend', researcher: 'Researcher', writer: 'Writer', qa: 'QA' };
3
+ const THREE_KINGDOMS_IDS = ['zhuge-liang', 'lv-bu', 'diao-chan', 'zhao-yun', 'chen-lin', 'zhou-yu'];
4
+ /** Idle quote key of one role id; `heroes` marks a roster using the optional three-kingdoms theme. */
5
+ function characterQuoteKey(roleId, heroes = false) {
6
+ if (roleId === 'lead' || roleId === 'sun-quan') return heroes || roleId === 'sun-quan' ? 'live.quote.sun-quan' : 'live.quote.lead';
7
+ if (THREE_KINGDOMS_IDS.includes(roleId) || Object.hasOwn(NEUTRAL_ROLE_NAMES, roleId)) return `live.quote.${roleId}`;
8
+ return 'live.quote.default';
9
+ }
10
+ /** Roles with untouched neutral default names translated (`role.name.<id>`) and their theme-aware idle quote key. */
11
+ function localizeRoles(roles, t) {
12
+ const list = Array.isArray(roles) ? roles : [];
13
+ const heroes = list.some(r => THREE_KINGDOMS_IDS.includes(r?.id));
14
+ return list.map(r => {
15
+ if (!r || typeof r !== 'object') return r;
16
+ const key = `role.name.${r.id}`, text = typeof t === 'function' && NEUTRAL_ROLE_NAMES[r.id] === r.displayName ? t(key) : key;
17
+ return { ...r, ...(text !== key ? { displayName: text } : {}), quoteKey: characterQuoteKey(r.id, heroes) };
18
+ });
19
+ }
20
+ Object.assign(en, {
21
+ 'role.name.lead': 'Lead', 'role.name.architect': 'Architect', 'role.name.backend': 'Backend', 'role.name.frontend': 'Frontend',
22
+ 'role.name.researcher': 'Researcher', 'role.name.writer': 'Writer', 'role.name.qa': 'QA',
23
+ 'live.quote.lead': 'Goals set, work assigned.', 'live.quote.architect': 'Design and contracts first, then code.', 'live.quote.backend': 'APIs and data, ready when you are.',
24
+ 'live.quote.frontend': 'Pages and interactions, on it.', 'live.quote.researcher': 'Verify first, conclude second.', 'live.quote.writer': 'Docs ready to be written.', 'live.quote.qa': 'No evidence, not done.',
25
+ });