@v587d/capital-generation 2.10.0 → 2.10.2

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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +20 -10
  3. package/chart-ui/package.json +1 -1
  4. package/config/client.js +36 -29
  5. package/config/client.src.cjs +228 -142
  6. package/config/index.js +8 -2
  7. package/config/package.json +1 -1
  8. package/lib/agents/selected-skills-provider.d.ts +29 -10
  9. package/lib/agents/selected-skills-provider.js +262 -139
  10. package/lib/agents/skill-usage.d.ts +22 -0
  11. package/lib/agents/skill-usage.js +92 -0
  12. package/lib/capital-settings.d.ts +12 -1
  13. package/lib/capital-settings.js +23 -3
  14. package/lib/index.d.ts +6 -3
  15. package/lib/index.js +10 -19
  16. package/lib/root-extras-entry.d.ts +2 -5
  17. package/lib/root-extras-entry.js +1 -3
  18. package/lib/selected-skills-entry.d.ts +14 -0
  19. package/lib/selected-skills-entry.js +43 -0
  20. package/lib/web-retriever/local-fetch.d.ts +1 -1
  21. package/lib/web-retriever/local-fetch.js +1 -1
  22. package/lib/web-retriever/wind-client.d.ts +1 -1
  23. package/lib/web-retriever/wind-client.js +1 -1
  24. package/package.json +11 -2
  25. package/preset/capital-generation/agent.patch.yml +34 -16
  26. package/preset/capital-generation/skills/capital-data-query/SKILL.md +19 -0
  27. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -0
  28. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +3 -0
  29. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +7 -9
  30. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +3 -0
  31. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -0
  32. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +3 -0
  33. package/selected-skills/README.md +8 -6
  34. package/selected-skills/ROUTING.md +88 -0
  35. package/selected-skills/catalog.js +32 -121
  36. package/selected-skills/filter.js +70 -0
  37. package/selected-skills/routing.js +1059 -0
  38. package/watchlist/package.json +1 -1
@@ -1,24 +1,39 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { SkillProviderControl } from '@deepseek-ai/dsh-skill';
3
3
  import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
4
- import { type PolicyContext } from './root-tool-policy.js';
4
+ import { type AgentLike, type PolicyContext } from './root-tool-policy.js';
5
5
  export type SelectedSkillStatus = 'experimental' | 'watchlist';
6
6
  export type SelectedSkillCatalogEntry = {
7
7
  key: string;
8
8
  name: string;
9
+ label: {
10
+ zh: string;
11
+ en: string;
12
+ };
13
+ repository: string;
9
14
  status: SelectedSkillStatus;
10
15
  tags: readonly string[];
11
16
  };
12
17
  export declare const SELECTED_SKILL_CATALOG: readonly SelectedSkillCatalogEntry[];
13
- export type SelectedSkillKey = typeof SELECTED_SKILL_CATALOG[number]['key'];
14
- export type SelectedSkillSelection = Partial<Record<SelectedSkillKey, boolean>>;
18
+ /** Roles a preset row may declare. data_collector / visualization have no method set. */
19
+ export declare const SELECTED_SKILL_ENTRY_ROLES: readonly ["main", "data_junior", "web_retriever", "data_analyst", "data_collector", "visualization_specialist"];
20
+ export type SelectedSkillRole = typeof SELECTED_SKILL_ENTRY_ROLES[number];
21
+ export type SelectedSkillSelection = Partial<Record<string, boolean>>;
15
22
  export type SelectedSkillSelectionSource = SelectedSkillSelection | (() => SelectedSkillSelection);
16
- type RuntimeSummary = {
17
- name: string;
18
- description: string;
23
+ export type SelectedSkillsMode = 'auto' | 'manual';
24
+ /** Mode + manual switches. `auto` exposes every method the role owns and ignores the switches. */
25
+ export type SelectedSkillAccess = SelectedSkillSelectionSource | {
26
+ selection: SelectedSkillSelectionSource;
27
+ mode: SelectedSkillsMode | (() => SelectedSkillsMode);
19
28
  };
20
- /** Sole index renderer; descriptions use runtime frontmatter (what the model sees), not card summaries. */
21
- export declare function renderTaskSkillIndex(taskId: string, summaries: readonly RuntimeSummary[]): string | undefined;
29
+ /** Names this role may load, independent of the user's switches. One filter lives in routing.js. */
30
+ export declare function selectedSkillNamesForRole(role: SelectedSkillRole): Set<string>;
31
+ /** Enabled names for one role, read at call time (settings are a live preference). */
32
+ export declare function enabledSelectedSkillNames(role: SelectedSkillRole, selection: SelectedSkillSelection): Set<string>;
33
+ export declare function readSelectedSkillSelection(source: SelectedSkillSelectionSource): SelectedSkillSelection;
34
+ export declare function readSelectedSkillsMode(access: SelectedSkillAccess): SelectedSkillsMode;
35
+ /** Names the role may load right now: auto = the whole role set; manual = switches only. */
36
+ export declare function availableSelectedSkillNames(role: SelectedSkillRole, access: SelectedSkillAccess): Set<string>;
22
37
  type FileSystemSkillConfig = {
23
38
  providerName: string;
24
39
  includeDefaultRoots: false;
@@ -27,6 +42,10 @@ type FileSystemSkillConfig = {
27
42
  };
28
43
  type FileSystemSkillSource = Pick<FileSystemSkillProvider, 'list' | 'get' | 'dispose'>;
29
44
  type FileSystemSkillFactory = (ctx: Context, control: SkillProviderControl, config: FileSystemSkillConfig) => FileSystemSkillSource;
30
- /** Registers selected community experiments on the Capital root Agent only. */
31
- export declare function registerRootSelectedSkills(ctx: PolicyContext, selection: SelectedSkillSelectionSource, createFilesystemProvider?: FileSystemSkillFactory): void;
45
+ /** Read-time router for one role: only cards whose snapshot actually exists are promised to the model. */
46
+ export declare function selectedSkillEntriesForRole(role: SelectedSkillRole, access: SelectedSkillAccess): SelectedSkillCatalogEntry[];
47
+ /** Role prompt: a short, imperative router. No upstream prose and no variable interpolation. */
48
+ export declare function renderSelectedSkillsBrief(role: SelectedSkillRole, access: SelectedSkillAccess, agent?: AgentLike): string;
49
+ /** Registers the role-scoped method catalog on the configured Capital preset Agent only. */
50
+ export declare function registerSelectedSkills(ctx: PolicyContext, role: SelectedSkillRole, access: SelectedSkillAccess, createFilesystemProvider?: FileSystemSkillFactory): void;
32
51
  export {};
@@ -1,135 +1,243 @@
1
1
  import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
2
- import { readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
4
3
  import { fileURLToPath } from 'node:url';
4
+ import { readPresetClaim } from '../preset-claim.js';
5
+ import { ROLE_PRESET_IDS, ROOT_PRESET_ID } from '../preset-ids.js';
5
6
  import { agentSessionId, isRootAgent } from './root-tool-policy.js';
6
- // Catalog is package data outside the TS root; the runtime import keeps lib resolving it beside pinned snapshots.
7
+ import { clearSelectedSkillUsage, recordSelectedSkillUsage, resetSelectedSkillUsage, skillUsageNamesForSession, skillUsageRows, trackSelectedSkillUsage, } from './skill-usage.js';
8
+ // Catalog/routing are package data outside the TS root; runtime imports keep lib resolving beside pinned snapshots.
7
9
  // @ts-expect-error catalog.js is intentionally maintained as package data.
8
- import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS, SELECTED_SKILL_TASK_IDS, selectedSkillsForTask } from '../../selected-skills/catalog.js';
10
+ import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS } from '../../selected-skills/catalog.js';
11
+ // @ts-expect-error routing.js is intentionally maintained as package data.
12
+ import { SELECTED_SKILL_ROLES, SELECTED_SKILL_ROUTING as ROUTING, selectedSkillNamesForRole as routingNamesForRole, selectedSkillRoleKey, selectedSkillUseWhen } from '../../selected-skills/routing.js';
9
13
  export const SELECTED_SKILL_CATALOG = CATALOG;
14
+ /** Roles a preset row may declare. data_collector / visualization have no method set. */
15
+ export const SELECTED_SKILL_ENTRY_ROLES = [
16
+ 'main', 'data_junior', 'web_retriever', 'data_analyst', 'data_collector', 'visualization_specialist',
17
+ ];
18
+ const ROUTING_BY_NAME = ROUTING;
10
19
  const PROVIDER_NAME = 'capital-selected-skills';
11
- const CAPITAL_PRESET_ID = 'capital-generation';
12
- // Derive roots from the catalog: a hardcoded list silently hides checked boxes (upstream layouts differ).
20
+ const BRIEF_HEADING = '# 外部精选方法(用户已启用)';
21
+ const AUTO_BRIEF_HEADING = '# 外部精选方法(自动模式:本角色全部方法)';
22
+ /** The role prompt is a router, not the 68-skill catalog; the full list stays in the host catalog. */
23
+ const BRIEF_MAX = 12;
24
+ /** Derive roots from the catalog: a hardcoded list silently hides checked boxes (upstream layouts differ). */
13
25
  const SKILL_ROOTS = Object.entries(SELECTED_SKILL_SKILL_DIRS).map(([repository, skillsDir]) => fileURLToPath(new URL(`../../selected-skills/${repository}/${skillsDir}`, import.meta.url)));
14
- /** Built-in task skill root; the index is appended at read time, never written to disk:
15
- * installs may be read-only, watchers would churn, and tests pin frontmatter and size. */
16
- const TASK_SKILL_ROOT = fileURLToPath(new URL('../../preset/capital-generation/skills', import.meta.url));
17
- /** Smaller rank wins: 150 beats the same-layer custom root (300). The agent scope usually
18
- * shadows anyway; both are cheap insurance because failure is silent. */
19
- const TASK_SKILL_SHADOW_RANK = 150;
20
- const INDEX_HEADING = '## 附:本轮可用的外部精选 Skill(索引)';
21
- /** Conflict precedence and the three untouchable boundaries must live inside the block. */
22
- const INDEX_FRAME = [
23
- '以下是你(用户)在设置里启用的外部精选 Skill,按能力标签与本次任务关联后列出。它们是**方法论材料**,',
24
- '需要正文时照常用 `skill` 工具按名加载。它们**不改变**本 skill 的证据门、工具委派路径、输出通道与报告形状;',
25
- '与本 skill 冲突时**以本 skill 为准**。这里只给名称与摘要,摘要不等于全文,未加载前不要照它行事。',
26
- ];
27
- function readFrontmatter(raw) {
28
- const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
29
- if (match === null)
30
- return undefined;
31
- const fields = new Map();
32
- for (const line of match[1].split(/\r?\n/)) {
33
- const field = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
34
- if (field !== null)
35
- fields.set(field[1], field[2].trim());
36
- }
37
- return { fields, body: raw.slice(match[0].length) };
26
+ const ROLE_PRESET = {
27
+ main: ROOT_PRESET_ID,
28
+ data_junior: ROLE_PRESET_IDS.junior,
29
+ web_retriever: ROLE_PRESET_IDS.retriever,
30
+ data_analyst: ROLE_PRESET_IDS.analyst,
31
+ data_collector: ROLE_PRESET_IDS.collector,
32
+ visualization_specialist: ROLE_PRESET_IDS.specialist,
33
+ };
34
+ const ENTRY_BY_KEY = new Map(SELECTED_SKILL_CATALOG.map((entry) => [entry.key, entry]));
35
+ const ROLE_LABELS = SELECTED_SKILL_ROLES;
36
+ const CATALOG_NAMES = new Set(SELECTED_SKILL_CATALOG.map(({ name }) => name));
37
+ const shortSession = (id) => id.replace(/^session-/, '').slice(0, 8);
38
+ const roleLabel = (role) => ROLE_LABELS[role]?.zh ?? role;
39
+ /** Names this role may load, independent of the user's switches. One filter lives in routing.js. */
40
+ export function selectedSkillNamesForRole(role) {
41
+ return new Set(routingNamesForRole(role, SELECTED_SKILL_CATALOG));
38
42
  }
39
- /** Unreadable means no shadow, never a missing skill: the fs provider still serves the original. */
40
- function loadTaskSkillBases(root = TASK_SKILL_ROOT) {
41
- const bases = new Map();
42
- for (const name of SELECTED_SKILL_TASK_IDS) {
43
- try {
44
- const raw = readFileSync(join(root, name, 'SKILL.md'), 'utf8');
45
- const parsed = readFrontmatter(raw);
46
- const description = parsed?.fields.get('description');
47
- if (parsed === undefined || description === undefined || description === '')
48
- continue;
49
- bases.set(name, { name, description, content: parsed.body.trim(), directory: join(root, name) });
50
- }
51
- catch {
52
- }
43
+ /** Enabled names for one role, read at call time (settings are a live preference). */
44
+ export function enabledSelectedSkillNames(role, selection) {
45
+ const allowed = selectedSkillNamesForRole(role);
46
+ const names = new Set();
47
+ for (const [key, enabled] of Object.entries(selection)) {
48
+ if (enabled !== true)
49
+ continue;
50
+ const entry = ENTRY_BY_KEY.get(key);
51
+ if (entry !== undefined && allowed.has(entry.name))
52
+ names.add(entry.name);
53
53
  }
54
- return bases;
54
+ return names;
55
55
  }
56
- /** Sole index renderer; descriptions use runtime frontmatter (what the model sees), not card summaries. */
57
- export function renderTaskSkillIndex(taskId, summaries) {
58
- if (summaries.length === 0)
59
- return undefined;
60
- return [
61
- INDEX_HEADING,
62
- '',
63
- ...INDEX_FRAME,
64
- '',
65
- ...summaries.map((entry) => `- \`${entry.name}\`: ${entry.description}`),
66
- ].join('\n');
56
+ export function readSelectedSkillSelection(source) {
57
+ return typeof source === 'function' ? source() : source;
67
58
  }
68
- function createFilesystemSkillProvider(ctx, control, config) {
69
- return new FileSystemSkillProvider(ctx, control, config);
59
+ function isAccess(value) {
60
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && 'selection' in value;
70
61
  }
71
- function selectedNames(selection) {
72
- return new Set(SELECTED_SKILL_CATALOG
73
- .filter(({ key }) => selection[key] === true)
74
- .map(({ name }) => name));
62
+ export function readSelectedSkillsMode(access) {
63
+ if (!isAccess(access))
64
+ return 'manual';
65
+ const mode = typeof access.mode === 'function' ? access.mode() : access.mode;
66
+ return mode === 'auto' ? 'auto' : 'manual';
75
67
  }
76
- function readSelection(source) {
77
- return typeof source === 'function' ? source() : source;
68
+ /** Names the role may load right now: auto = the whole role set; manual = switches only. */
69
+ export function availableSelectedSkillNames(role, access) {
70
+ return readSelectedSkillsMode(access) === 'auto'
71
+ ? selectedSkillNamesForRole(role)
72
+ : enabledSelectedSkillNames(role, readSelectedSkillSelection(isAccess(access) ? access.selection : access));
78
73
  }
79
- // Fingerprint only the catalog key set: refresh the cache only when the selection changes.
74
+ /** Fingerprint only the catalog key set: refresh the cache only when the selection changes. */
80
75
  function selectionFingerprint(selection) {
81
76
  return SELECTED_SKILL_CATALOG.map(({ key }) => (selection[key] === true ? '1' : '0')).join('');
82
77
  }
78
+ /** Fingerprint the mode separately: switches cannot change an auto catalog. */
79
+ function accessFingerprint(access) {
80
+ if (readSelectedSkillsMode(access) === 'auto')
81
+ return 'auto';
82
+ return `manual:${selectionFingerprint(readSelectedSkillSelection(isAccess(access) ? access.selection : access))}`;
83
+ }
83
84
  // `agent/pre-step` belongs to dsh-agent; attach via the injected Agent context so scope filtering admits it.
84
85
  function onPreStepEvent(ctx, listener) {
85
86
  const events = ctx;
86
87
  return events.on?.('agent/pre-step', listener);
87
88
  }
88
- function strictCapitalPreset(ctx, agent) {
89
- try {
90
- const presets = ctx.get?.('agentPresets');
91
- return typeof presets?.composedPreset === 'function'
92
- && presets.composedPreset(agent.ctx) === CAPITAL_PRESET_ID;
89
+ function createFilesystemSkillProvider(ctx, control, config) {
90
+ return new FileSystemSkillProvider(ctx, control, config);
91
+ }
92
+ function catalogPath(entry) {
93
+ const dir = SELECTED_SKILL_SKILL_DIRS[entry.repository];
94
+ if (dir === undefined)
95
+ return '';
96
+ return fileURLToPath(new URL(`../../selected-skills/${entry.repository}/${dir}/${entry.name}/SKILL.md`, import.meta.url));
97
+ }
98
+ /** Read-time router for one role: only cards whose snapshot actually exists are promised to the model. */
99
+ export function selectedSkillEntriesForRole(role, access) {
100
+ const available = availableSelectedSkillNames(role, access);
101
+ const rank = (name) => {
102
+ const routing = ROUTING_BY_NAME[name];
103
+ const canonical = (routing?.canonicalFor.length ?? 0) > 0 ? 0 : 2;
104
+ const primary = routing?.primaryRole === selectedSkillRoleKey(role) ? 0 : 1;
105
+ return canonical + primary;
106
+ };
107
+ return SELECTED_SKILL_CATALOG
108
+ .filter((entry) => available.has(entry.name) && existsSync(catalogPath(entry)))
109
+ // Canonical, primary-role methods first: the 12-line prompt is a router, not a catalog order sample.
110
+ .sort((left, right) => rank(left.name) - rank(right.name));
111
+ }
112
+ /** Usage is read from the tool-result ledger, never from the model's own claim. */
113
+ function usageSection(role, agent) {
114
+ if (agent === undefined)
115
+ return [];
116
+ const sessionId = agentSessionId(agent);
117
+ if (sessionId === undefined)
118
+ return [];
119
+ if (selectedSkillRoleKey(role) === 'main') {
120
+ const rows = skillUsageRows(sessionId);
121
+ if (rows.length === 0)
122
+ return [];
123
+ return [
124
+ '## 本次已实际加载的精选方法(程序记录)',
125
+ '以下名单来自成功返回正文的加载记录(工具结果或 `/name` 注入),包含子 Agent 实际加载的方法;不依赖模型自报。',
126
+ ...rows.map((row) => {
127
+ const who = row.role === 'main' ? roleLabel(row.role) : `${roleLabel(row.role)}(${shortSession(row.sessionId)})`;
128
+ return `- ${who}:${row.names.map((name) => `\`${name}\``).join('、')}`;
129
+ }),
130
+ '最终答复必须有一节「使用的方法」,逐条点名以上每一个方法,并说明它支撑了哪个判断;'
131
+ + '不得点名未加载的条目,也不得用「参考了相关方法」代替。本轮没有加载就不要写这一节。',
132
+ ];
93
133
  }
94
- catch {
95
- return false;
134
+ const names = skillUsageNamesForSession(sessionId);
135
+ if (names.length === 0)
136
+ return [];
137
+ return [
138
+ `本次你已实际加载:${names.map((name) => `\`${name}\``).join('、')}。`,
139
+ `回传结论时在末尾附一行 \`skills_used: ${names.join(', ')}\`;主 Agent 以程序记录为准,这一行是给读者看的摘要。`,
140
+ ];
141
+ }
142
+ /** Role prompt: a short, imperative router. No upstream prose and no variable interpolation. */
143
+ export function renderSelectedSkillsBrief(role, access, agent) {
144
+ // The main row is mounted in the root preset; a generic child may inherit that composition.
145
+ // Main methods stay on the root Agent, so its prompt must not promise them to a child either.
146
+ if (role === 'main' && agent !== undefined && !isRootAgent(agent))
147
+ return '';
148
+ const mode = readSelectedSkillsMode(access);
149
+ const entries = selectedSkillEntriesForRole(role, access);
150
+ const usage = usageSection(role, agent);
151
+ // A child may have loaded a method while this role has none enabled; the citation
152
+ // feedback still has to reach the root, so usage alone is enough to render.
153
+ if (entries.length === 0 && usage.length === 0)
154
+ return '';
155
+ const key = selectedSkillRoleKey(role);
156
+ const roleRule = key === 'main'
157
+ ? '需要计算、建模或统计时交给 data_junior;你的目录只含主角色可用的方法,只属于 data_junior / web_retriever 的方法不在其中,不代表不存在——用户点名时交给对应角色按名加载,不要判为不存在,也不要改用通用方法自己算。'
158
+ : key === 'data_junior'
159
+ ? '只执行方法中的计算、表格与统计;结论与最终写作留给主 Agent。'
160
+ : '只做取证、文档深读与核验;不做投资判断。';
161
+ const shown = entries.slice(0, BRIEF_MAX);
162
+ const layerRule = '这些是方法层(金融分析框架),不是流程协议:内置 `capital-*` 协议负责流程、证据门、工具委派与输出形状,'
163
+ + '方法层负责分析步骤、公式与检查清单。两者正交,任务同时命中时都要加载——先用协议定流程,再用方法定分析;'
164
+ + '不得用协议替代方法,也不得用方法改写协议。';
165
+ const lines = [mode === 'auto' ? AUTO_BRIEF_HEADING : BRIEF_HEADING, ''];
166
+ if (entries.length > 0) {
167
+ lines.push(mode === 'auto'
168
+ ? '自动模式已按你的执行角色开放全部方法。进入分析或取证前,先按每条末尾的「何时使用」判断本任务需要哪几条;'
169
+ + '匹配的用 `skill` 按名加载正文,再按它的步骤执行;只加载与本任务直接相关的 1–2 条,不要一次搬空目录。'
170
+ : '以下条目是用户在设置里显式启用的方法。进入分析或取证前先逐条判断本任务是否需要这类方法;'
171
+ + '匹配的先用 `skill` 按名加载正文,再按它的步骤执行;只加载与本任务直接相关的 1–2 条。', layerRule, '加载后不得把摘要当全文,也不得声称用过未加载的条目;真正用过的要在最终答复里点名并说明用途。'
172
+ + '全都不匹配就不要提,也不要为了用而用。', roleRule, '', ...shown.map((entry) => `- \`${entry.name}\`(${entry.label.zh}):${ROUTING_BY_NAME[entry.name]?.useWhen.zh ?? entry.name}`));
173
+ if (entries.length > shown.length) {
174
+ lines.push(mode === 'auto'
175
+ ? `- 本角色共 ${entries.length} 颗可用,这里按常用度列前 ${shown.length} 颗;完整目录见本次会话的技能目录。`
176
+ : `- 另有 ${entries.length - shown.length} 颗已启用,见本次会话的技能目录。`);
177
+ }
178
+ else if (mode === 'auto') {
179
+ lines.push(`- 本角色共 ${entries.length} 颗可用,已全部列出。`);
180
+ }
96
181
  }
182
+ if (usage.length > 0)
183
+ lines.push('', ...usage);
184
+ return lines.join('\n');
97
185
  }
98
- /** Registers selected community experiments on the Capital root Agent only. */
99
- export function registerRootSelectedSkills(ctx, selection, createFilesystemProvider = createFilesystemSkillProvider) {
100
- // Static callers keep the all-off fast path; the plugin passes a reader so live toggles apply
101
- // at the next step (list/get filter by selection, pre-step refreshes the visible catalog).
102
- if (typeof selection !== 'function' && selectedNames(selection).size === 0)
186
+ /** Registers the role-scoped method catalog on the configured Capital preset Agent only. */
187
+ export function registerSelectedSkills(ctx, role, access, createFilesystemProvider = createFilesystemSkillProvider) {
188
+ const presetId = ROLE_PRESET[role];
189
+ const allowed = selectedSkillNamesForRole(role);
190
+ // data_collector / visualization own no methods; an empty provider would only add noise.
191
+ if (allowed.size === 0)
192
+ return;
193
+ // Static callers keep the all-off fast path; a live reader (the plugin) must register even when
194
+ // every switch is off now, or a later enable/auto flip would never surface without a new Agent.
195
+ const staticAccess = typeof access === 'function'
196
+ ? undefined
197
+ : isAccess(access)
198
+ ? (typeof access.selection === 'function' || typeof access.mode === 'function' ? undefined : access)
199
+ : access;
200
+ if (staticAccess !== undefined && readSelectedSkillsMode(staticAccess) === 'manual'
201
+ && availableSelectedSkillNames(role, staticAccess).size === 0)
103
202
  return;
104
203
  const listenCtx = ctx.root ?? ctx;
105
204
  if (typeof listenCtx.on !== 'function')
106
205
  return;
107
- // One registration per root Agent: both event paths can observe the same Agent.
206
+ // One registration per Agent: both event paths can observe the same Agent.
108
207
  const registered = new WeakSet();
109
208
  const registerFor = async (agent) => {
110
209
  if (!agent || registered.has(agent))
111
210
  return;
112
- if (!isRootAgent(agent) || !strictCapitalPreset(ctx, agent))
211
+ // The row is mounted only in this role preset, but the root listener sees every Agent.
212
+ const claim = readPresetClaim(ctx, agent);
213
+ if (claim.claim !== presetId)
214
+ return;
215
+ // Main methods stay on the root Agent: a generic child may inherit the root preset claim.
216
+ if (role === 'main' && !isRootAgent(agent))
113
217
  return;
114
218
  registered.add(agent);
219
+ const usageRole = selectedSkillRoleKey(role);
220
+ const rootAgent = role === 'main' && isRootAgent(agent);
221
+ const sessionId = agentSessionId(agent);
222
+ trackSelectedSkillUsage(agent, usageRole);
115
223
  try {
116
224
  const agentCtx = agent.ctx;
117
225
  if (!agentCtx || typeof agentCtx.inject !== 'function') {
118
226
  registered.delete(agent);
119
- ctx.logger?.warn?.('capital-generation: selected skills are unavailable (root Agent cannot inject skills)');
227
+ ctx.logger?.warn?.('capital-generation: selected skills are unavailable (agent cannot inject skills)');
120
228
  return;
121
229
  }
122
230
  // agent/created is serial: DSH awaits listeners, so this inject lands before the first prompt.
123
231
  await agentCtx.inject(['skills'], (injectedCtx) => {
124
232
  const skills = injectedCtx.skills;
125
233
  if (typeof skills?.registerProvider !== 'function') {
126
- ctx.logger?.warn?.('capital-generation: selected skills are unavailable (root Agent has no skills registry)');
234
+ ctx.logger?.warn?.('capital-generation: selected skills are unavailable (agent has no skills registry)');
127
235
  return;
128
236
  }
129
237
  let filesystemProvider;
130
238
  let control;
131
- // Register even when all toggles are off; otherwise a later enable would never surface. Pre-step aligns settings.
132
- let served = selectionFingerprint(readSelection(selection));
239
+ // Register even when all toggles are off; a later enable must surface without a new Agent.
240
+ let served = accessFingerprint(access);
133
241
  injectedCtx.effect(() => {
134
242
  const unregister = skills.registerProvider((registration) => {
135
243
  control = registration;
@@ -139,64 +247,25 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
139
247
  customSkillDirs: SKILL_ROOTS,
140
248
  watch: true,
141
249
  });
142
- const taskBases = loadTaskSkillBases();
143
- // Summaries from the last `list()`; `get()` reuses them instead of rescanning dirs.
144
- let runtimeSummaries = [];
145
- const indexFor = (taskId) => {
146
- const byName = new Map(runtimeSummaries.map((entry) => [entry.name, entry]));
147
- const summaries = selectedSkillsForTask(taskId, [...byName.keys()])
148
- .map((entry) => byName.get(entry.name))
149
- .filter((entry) => entry !== undefined);
150
- return renderTaskSkillIndex(taskId, summaries);
151
- };
152
250
  const provider = {
153
251
  name: PROVIDER_NAME,
154
252
  async list(options) {
155
253
  // Read at call time: toggles are settings, not fixed at Agent creation.
156
- const enabled = selectedNames(readSelection(selection));
157
- // All-off is the default: skip snapshot dirs (no watcher starts) and shadow nothing, matching no-provider output.
158
- if (enabled.size === 0) {
159
- runtimeSummaries = [];
254
+ const available = availableSelectedSkillNames(role, access);
255
+ if (available.size === 0)
160
256
  return [];
161
- }
162
257
  const result = await filesystemProvider.list(options);
163
258
  const candidates = Array.isArray(result) ? result : result.candidates;
164
- const selected = candidates.filter((candidate) => enabled.has(candidate.name));
165
- runtimeSummaries = selected.map((candidate) => ({ name: candidate.name, description: candidate.description }));
166
- // Shadow a task skill only when it has an index; otherwise the fs provider's original wins.
167
- const shadows = [...taskBases.values()]
168
- .filter((base) => indexFor(base.name) !== undefined)
169
- .map((base) => ({
170
- name: base.name,
171
- description: base.description,
172
- invocation: { modelInvocable: true, userInvocable: true },
173
- provider: PROVIDER_NAME,
174
- source: 'custom',
175
- rank: TASK_SKILL_SHADOW_RANK,
176
- locator: { kind: 'task-skill', name: base.name },
177
- resourceBase: { kind: 'directory', path: base.directory },
178
- }));
179
- const merged = [...selected, ...shadows];
180
- return Array.isArray(result) ? merged : { ...result, candidates: merged };
259
+ const selected = candidates
260
+ .filter((candidate) => available.has(candidate.name))
261
+ .map((candidate) => ({ ...candidate, description: selectedSkillUseWhen(candidate.name, 'zh') || candidate.description }));
262
+ return Array.isArray(result) ? selected : { ...result, candidates: selected };
181
263
  },
182
264
  get(candidate, options) {
183
- const base = taskBases.get(candidate.name);
184
- if (base !== undefined) {
185
- const index = indexFor(base.name);
186
- const content = index === undefined ? base.content : `${base.content}\n\n${index}`;
187
- return Promise.resolve({
188
- name: base.name,
189
- description: base.description,
190
- invocation: { modelInvocable: true, userInvocable: true },
191
- source: 'custom',
192
- provider: PROVIDER_NAME,
193
- resourceBase: { kind: 'directory', path: base.directory },
194
- path: join(base.directory, 'SKILL.md'),
195
- content,
196
- });
197
- }
198
- if (!selectedNames(readSelection(selection)).has(candidate.name)) {
199
- throw new Error(`selected skill is not enabled: ${candidate.name}`);
265
+ // The catalog filter and the body read must use the same role/selection gate.
266
+ const available = availableSelectedSkillNames(role, access);
267
+ if (!available.has(candidate.name)) {
268
+ throw new Error(`selected skill is not enabled for ${role}: ${candidate.name}`);
200
269
  }
201
270
  return filesystemProvider.get(candidate, options);
202
271
  },
@@ -204,10 +273,9 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
204
273
  return provider;
205
274
  });
206
275
  // Invalidate before next() so this turn's snapshot sees the change regardless of listener order.
207
- // A settings read failure only degrades this hook.
208
276
  const stopPreStep = onPreStepEvent(injectedCtx, (_payload, next) => {
209
277
  try {
210
- const fingerprint = selectionFingerprint(readSelection(selection));
278
+ const fingerprint = accessFingerprint(access);
211
279
  if (fingerprint !== served) {
212
280
  served = fingerprint;
213
281
  control?.invalidate();
@@ -218,14 +286,69 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
218
286
  }
219
287
  return next();
220
288
  });
289
+ // The ledger is the only source for `skills_used`: a name is recorded after the host
290
+ // returned the body, so a rejected or disabled name never gets cited.
291
+ const usageCtx = injectedCtx;
292
+ if (typeof usageCtx.on !== 'function') {
293
+ ctx.logger?.warn?.('capital-generation: selected skill usage cannot be recorded (agent context has no event API)');
294
+ }
295
+ const stopToolResult = usageCtx.on?.('tools/result', (exec, result) => {
296
+ if (exec?.name !== 'skill')
297
+ return;
298
+ const outcome = result;
299
+ if (outcome?.isError === true || outcome?.value?.provider !== PROVIDER_NAME)
300
+ return;
301
+ if (typeof outcome?.value?.name === 'string' && CATALOG_NAMES.has(outcome.value.name)) {
302
+ recordSelectedSkillUsage(agent, usageRole, outcome.value.name);
303
+ }
304
+ });
305
+ const stopSession = usageCtx.on?.('session/event', (session, event) => {
306
+ if (sessionId !== undefined && session?.id !== sessionId)
307
+ return;
308
+ const type = event?.type;
309
+ const data = event?.data;
310
+ if (type === 'user/message') {
311
+ const source = data?.source;
312
+ // A new user turn starts a new citation window; child settlements do not.
313
+ if (source?.kind === 'user' && rootAgent && sessionId !== undefined)
314
+ resetSelectedSkillUsage(sessionId);
315
+ if (source?.kind === 'skill-invocation' && typeof source.name === 'string' && CATALOG_NAMES.has(source.name)) {
316
+ recordSelectedSkillUsage(agent, usageRole, source.name);
317
+ }
318
+ return;
319
+ }
320
+ if (type !== 'tool/ptc-dispatch')
321
+ return;
322
+ if (data?.name !== 'skill' || data.isError === true)
323
+ return;
324
+ let args = data.arguments;
325
+ if (typeof args === 'string') {
326
+ try {
327
+ args = JSON.parse(args);
328
+ }
329
+ catch {
330
+ return;
331
+ }
332
+ }
333
+ const name = args?.name;
334
+ if (typeof name === 'string' && CATALOG_NAMES.has(name)) {
335
+ recordSelectedSkillUsage(agent, usageRole, name);
336
+ }
337
+ });
221
338
  return () => {
222
339
  if (typeof stopPreStep === 'function')
223
340
  stopPreStep();
341
+ if (typeof stopToolResult === 'function')
342
+ stopToolResult();
343
+ if (typeof stopSession === 'function')
344
+ stopSession();
345
+ if (rootAgent && sessionId !== undefined)
346
+ clearSelectedSkillUsage(sessionId);
224
347
  unregister();
225
348
  void filesystemProvider?.dispose();
226
349
  filesystemProvider = undefined;
227
350
  };
228
- }, `capital-generation: selected skills for ${agentSessionId(agent) ?? 'root'}`);
351
+ }, `capital-generation: selected skills for ${role} / ${agentSessionId(agent) ?? 'agent'}`);
229
352
  });
230
353
  }
231
354
  catch (error) {
@@ -235,11 +358,11 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
235
358
  }
236
359
  };
237
360
  const stop = listenCtx.on('agent/created', ({ agent }) => registerFor(agent));
238
- // Second path: GUI creates with the default preset, then selects Capital. `agent/created`
239
- // already fired and apply() runs inside that remount, so `agent-preset/selected` arrives after mount.
361
+ // GUI creates with the default preset, then selects Capital. `agent/created` already fired and
362
+ // apply() runs inside that remount, so `agent-preset/selected` arrives after mount.
240
363
  const onPresetEvent = listenCtx.on;
241
364
  const stopPreset = onPresetEvent?.('agent-preset/selected', (sessionId, agentPreset) => {
242
- if (agentPreset !== CAPITAL_PRESET_ID)
365
+ if (agentPreset !== presetId)
243
366
  return;
244
367
  const agents = ctx.get?.('agents');
245
368
  const agent = agents?.get?.(String(sessionId));
@@ -253,5 +376,5 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
253
376
  stop();
254
377
  if (typeof stopPreset === 'function')
255
378
  stopPreset();
256
- }, 'capital-generation: selected skill root listener');
379
+ }, 'capital-generation: selected skill role listeners');
257
380
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Programmatic selected-skill ledger: a load counts only after the host returned the
3
+ * selected provider's body. The root prompt reads the tree; the report script re-derives
4
+ * the same facts from durable session logs, so model self-report is never the source.
5
+ */
6
+ import { type AgentLike } from './root-tool-policy.js';
7
+ export interface SkillUsageRow {
8
+ readonly sessionId: string;
9
+ readonly parentSession?: string;
10
+ readonly role: string;
11
+ readonly names: readonly string[];
12
+ }
13
+ /** Register a role Agent before it loads anything, so the tree edge survives an empty session. */
14
+ export declare function trackSelectedSkillUsage(agent: AgentLike | undefined, role: string): void;
15
+ export declare function recordSelectedSkillUsage(agent: AgentLike | undefined, role: string, name: string): void;
16
+ /** A new user turn drops the previous turn's names but keeps the tree edges. */
17
+ export declare function resetSelectedSkillUsage(rootSessionId: string): void;
18
+ /** Agent teardown: the whole subtree's records are process-local and must not outlive it. */
19
+ export declare function clearSelectedSkillUsage(rootSessionId: string): void;
20
+ export declare function clearAllSelectedSkillUsage(): void;
21
+ export declare function skillUsageRows(rootSessionId: string): SkillUsageRow[];
22
+ export declare function skillUsageNamesForSession(sessionId: string): string[];