@zhushanwen/pi-subagent-workflow 5.0.2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/agents/{reviewer.md → code-reviewer.md} +20 -3
  2. package/agents/context-builder.md +5 -0
  3. package/agents/doc-reviewer.md +9 -2
  4. package/agents/explorer.md +5 -0
  5. package/agents/general-purpose.md +5 -0
  6. package/agents/oracle.md +20 -4
  7. package/agents/orchestrator.md +6 -1
  8. package/agents/planner.md +5 -0
  9. package/agents/researcher.md +5 -0
  10. package/agents/worker.md +5 -0
  11. package/package.json +6 -4
  12. package/src/execution/__tests__/agent-registry.test.ts +189 -119
  13. package/src/execution/__tests__/crash-recovery.test.ts +0 -1
  14. package/src/execution/__tests__/execute-options-mapper.test.ts +4 -4
  15. package/src/execution/__tests__/index-session-start.test.ts +0 -1
  16. package/src/execution/__tests__/model-resolver.test.ts +20 -0
  17. package/src/execution/__tests__/session-start-reaper.test.ts +0 -2
  18. package/src/execution/__tests__/subprocess-agent-runner.test.ts +1 -1
  19. package/src/execution/agent-registry.ts +92 -169
  20. package/src/execution/execute-options-mapper.ts +2 -2
  21. package/src/execution/model-config-service.ts +13 -34
  22. package/src/execution/model-resolver.ts +5 -3
  23. package/src/execution/subagent-service.ts +9 -6
  24. package/src/execution/subprocess-agent-runner.ts +3 -2
  25. package/src/index.ts +4 -25
  26. package/src/injectors/__tests__/subagent-list-injector.test.ts +266 -14
  27. package/src/injectors/__tests__/workflow-list-injector.test.ts +236 -32
  28. package/src/injectors/subagent-list-injector.ts +99 -48
  29. package/src/injectors/workflow-list-injector.ts +65 -50
  30. package/src/interface/__tests__/detectors.test.ts +100 -43
  31. package/src/interface/__tests__/subagent-tool-prompt.test.ts +8 -12
  32. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +163 -0
  33. package/src/interface/__tests__/workflow-tool-prompt.test.ts +55 -9
  34. package/src/interface/subagent-tool.ts +7 -4
  35. package/src/interface/tool-workflow-script.ts +27 -10
  36. package/src/interface/tool-workflow.ts +174 -81
  37. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  38. package/src/orchestration/__tests__/config-loader.test.ts +124 -40
  39. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +33 -2
  40. package/src/orchestration/__tests__/lifecycle.test.ts +59 -2
  41. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +116 -51
  42. package/src/orchestration/__tests__/script-lint.test.ts +167 -1
  43. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  44. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +69 -0
  45. package/src/orchestration/__tests__/worker-script-builder.test.ts +51 -1
  46. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +2 -2
  47. package/src/orchestration/__tests__/workflows-e2e.test.ts +177 -24
  48. package/src/orchestration/agent-opts-resolver.ts +51 -101
  49. package/src/orchestration/args-validator.ts +127 -0
  50. package/src/orchestration/config-loader.ts +63 -94
  51. package/src/orchestration/error-recovery.ts +6 -9
  52. package/src/orchestration/launcher.ts +77 -41
  53. package/src/orchestration/lifecycle.ts +7 -0
  54. package/src/orchestration/models/ports.ts +0 -14
  55. package/src/orchestration/models/run-spec.ts +24 -1
  56. package/src/orchestration/models/types.ts +15 -8
  57. package/src/orchestration/models/workflow-script-registry.ts +3 -0
  58. package/src/orchestration/models/workflow-script.ts +10 -14
  59. package/src/orchestration/script-lint.ts +159 -0
  60. package/src/orchestration/worker-host.ts +5 -0
  61. package/src/orchestration/worker-script-builder.ts +15 -4
  62. package/src/orchestration/workflow-script-registry-impl.ts +34 -29
  63. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  64. package/src/shared/__tests__/resource-discovery.test.ts +167 -7
  65. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  66. package/src/shared/agent-ref.ts +36 -0
  67. package/src/shared/meta-parser.ts +257 -0
  68. package/src/shared/resource-discovery.ts +88 -2
  69. package/src/shared/resource-meta.ts +60 -0
  70. package/workflows/README.md +2 -2
  71. package/workflows/_shared/agent-refs.cjs +40 -0
  72. package/workflows/chain.js +30 -6
  73. package/workflows/map-reduce.js +33 -5
  74. package/workflows/parallel.js +34 -7
  75. package/workflows/review-fix-loop-utils.cjs +23 -103
  76. package/workflows/review-fix-loop.js +121 -58
  77. package/workflows/scatter-gather.js +30 -6
@@ -115,13 +115,13 @@ describe("mapToExecuteOptions (D-A2)", () => {
115
115
  expect(result.thinkingLevel).toBeUndefined();
116
116
  });
117
117
 
118
- it("systemPromptFiles → appendSystemPrompt 映射 (M2)", () => {
119
- const opts: AgentCallOpts = { ...baseOpts, systemPromptFiles: ["/tmp/a.md"] };
118
+ it("appendSystemPrompt → appendSystemPrompt 透传", () => {
119
+ const opts: AgentCallOpts = { ...baseOpts, appendSystemPrompt: ["SO instruction content"] };
120
120
  const result = mapToExecuteOptions(opts);
121
- expect(result.appendSystemPrompt).toEqual(["/tmp/a.md"]);
121
+ expect(result.appendSystemPrompt).toEqual(["SO instruction content"]);
122
122
  });
123
123
 
124
- it("systemPromptFiles 不传 → appendSystemPrompt undefined", () => {
124
+ it("appendSystemPrompt 不传 → appendSystemPrompt undefined", () => {
125
125
  const result = mapToExecuteOptions(baseOpts);
126
126
  expect(result.appendSystemPrompt).toBeUndefined();
127
127
  });
@@ -101,7 +101,6 @@ vi.mock("../subagent-service.ts", () => ({
101
101
  vi.mock("../model-config-service.ts", () => ({
102
102
  ModelConfigService: class {
103
103
  initModel = vi.fn();
104
- getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
105
104
  setCtxModel = vi.fn();
106
105
  },
107
106
  getModelConfigService: () => null,
@@ -278,6 +278,20 @@ describe("resolveModel — not-found error suggests similar models (B)", () => {
278
278
  expect(msg).toMatch(/models\.json/);
279
279
  expect(msg).not.toMatch(/Similar available models/); // auth 错误不列 model
280
280
  });
281
+
282
+ it("P1: resolves model passed with ':max' suffix (THINKING_ORDER includes 'max')", () => {
283
+ const m = makeModel({ id: "mock-model", provider: "p1", reasoning: true, thinkingLevelMap: { max: 4 } });
284
+ const reg = makeRegistry([m]);
285
+ const r = resolveModel(
286
+ { name: "worker", systemPrompt: "" },
287
+ reg,
288
+ { model: "p1/mock-model:max" },
289
+ ctxModel,
290
+ );
291
+ expect(r.model).toBeTruthy();
292
+ expect(r.model.id).toBe("mock-model");
293
+ expect(r.thinkingLevel).toBe("max");
294
+ });
281
295
  });
282
296
 
283
297
  describe("availableThinkingLevels", () => {
@@ -292,6 +306,12 @@ describe("availableThinkingLevels", () => {
292
306
  availableThinkingLevels({ reasoning: true, thinkingLevelMap: { off: 0, high: 2, low: 1, xhigh: 3 } }),
293
307
  ).toEqual(["off", "low", "high", "xhigh"]);
294
308
  });
309
+
310
+ it("P2: includes 'max' as the last level (THINKING_ORDER SSOT derived)", () => {
311
+ const levels = availableThinkingLevels({ reasoning: true, thinkingLevelMap: { off: 0, low: 1, high: 2, xhigh: 3, max: 4 } });
312
+ expect(levels).toEqual(["off", "low", "high", "xhigh", "max"]);
313
+ expect(levels.slice(-3)).toEqual(["high", "xhigh", "max"]);
314
+ });
295
315
  });
296
316
 
297
317
  // ============================================================
@@ -72,8 +72,6 @@ const { mockInitModel, mockInitSession, mockSetUiRequestHandler, mockSetModelCon
72
72
  vi.mock("../model-config-service.ts", () => ({
73
73
  ModelConfigService: class {
74
74
  initModel = mockInitModel;
75
- // F-4/D-003: index.ts 复用 modelService.getAgentRegistry(),stub 返回最小结构
76
- getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
77
75
  },
78
76
  getModelConfigService: () => null,
79
77
  setModelConfigService: mockSetModelConfigService,
@@ -59,7 +59,7 @@ function makeBaseOpts(): AgentCallOpts {
59
59
  timeoutMs: undefined,
60
60
  skill: undefined,
61
61
  skillPath: undefined,
62
- systemPromptFiles: undefined,
62
+ appendSystemPrompt: undefined,
63
63
  schemaEnv: undefined,
64
64
  };
65
65
  }
@@ -7,77 +7,28 @@
7
7
  //
8
8
  // builtin agent(包内 agents/*.md)走 pi.agents manifest(与 npm 包内发现规则一致)。
9
9
 
10
- import * as fs from "node:fs";
10
+
11
11
  import * as path from "node:path";
12
- import { fileURLToPath } from "node:url";
13
12
 
14
13
  import { getLogger } from "@zhushanwen/pi-extension-logger";
15
14
 
16
15
  import {
17
- type DiscoveredResource,
18
- discoverResourcesSync,
19
- type ScanConfig,
16
+ getCachedFile,
20
17
  } from "../shared/resource-discovery.ts";
18
+ import { normalizeRef, AGENT_REF_EXT } from "../shared/agent-ref.ts";
19
+ import { parseResourceMeta } from "../shared/meta-parser.ts";
20
+ import { lintAgentMeta } from "../orchestration/script-lint.ts";
21
+ import type { AgentMeta } from "../shared/resource-meta.ts";
21
22
  import type { AgentConfig } from "./model-resolver.ts";
22
23
 
23
24
  const logger = getLogger("subagents");
24
25
 
25
- /** 内置 agent(代码硬编码,如 default worker)。 */
26
- export interface BuiltinAgentRegistry {
27
- get(name: string): AgentConfig | undefined;
28
- list(): string[];
29
- }
30
-
31
- /**
32
- * 包内自带 agents(与 src/ 同级的 agents/ 目录)。
33
- *
34
- * 走 pi.agents manifest(package.json 的 pi.agents 字段),与 npm 包内发现规则一致。
35
- * manifest 缺失时 fallback 扫约定目录 agents/。
36
- *
37
- * [HISTORICAL] 此前 discoverAll 从未被调用,agentRegistry 永远为空——包内
38
- * agents/*.md(worker/reviewer/explorer 等)pi install 后开箱不可用。修复:构造时扫描
39
- * 包内 agents/ 作为 builtin(优先级最低,被用户同名文件覆盖)。
40
- */
41
- export function createPackageBuiltinRegistry(): BuiltinAgentRegistry {
42
- const packageRoot = path.resolve(
43
- path.dirname(fileURLToPath(import.meta.url)),
44
- "..",
45
- "..",
46
- );
47
- const cache = new Map<string, AgentConfig>();
48
- try {
49
- const config = discoverPackageAgentsSync(packageRoot);
50
- for (const resource of config) {
51
- if (!resource.available) continue;
52
- try {
53
- const raw = fs.readFileSync(resource.path, "utf-8");
54
- const agentConfig = parseAgentFrontmatter(resource.path, raw);
55
- if (agentConfig) cache.set(agentConfig.name, agentConfig);
56
- } catch (err) {
57
- // 单个 builtin agent 文件损坏不影响其他——降级跳过该文件。
58
- void err;
59
- logger.warn(`[subagents] skip malformed builtin agent: ${resource.path}`, {
60
- detail: err instanceof Error ? err.message : String(err),
61
- });
62
- }
63
- }
64
- } catch (err) {
65
- // agents/ 目录不存在(打包遗漏)→ 空 builtin,不崩。
66
- void err;
67
- logger.warn("[subagents] builtin agents/ directory unreadable, falling back to empty set", {
68
- detail: err instanceof Error ? err.message : String(err),
69
- });
70
- }
71
- return {
72
- get: (name) => cache.get(name),
73
- list: () => [...cache.keys()],
74
- };
75
- }
76
-
77
- /** mtime 缓存条目(跨 discoverAll 保留,靠 mtime 判失效)。 */
26
+ /** mtime 缓存条目(跨 loadByPath 保留,靠 mtime 判失效)。 */
78
27
  interface FileCacheEntry {
79
28
  mtimeMs: number;
80
29
  config: AgentConfig;
30
+ /** AgentMeta(W4 lint 用——cache-miss 时 lint 一次,warn 随 mtime 变化刷)。 */
31
+ meta: AgentMeta | null;
81
32
  }
82
33
 
83
34
  // ============================================================
@@ -92,11 +43,22 @@ const FM_DELIM = "---";
92
43
  * 兼容简单 YAML(key: value 单行格式)。body 作为 systemPrompt。
93
44
  */
94
45
  export function parseAgentFrontmatter(filePath: string, content: string): AgentConfig {
46
+ return parseAgentWithMeta(filePath, content).config;
47
+ }
48
+
49
+ /**
50
+ * 解析 agent .md → { config, meta } 二元组(m5 T2:W4 lint 需要 AgentMeta——
51
+ * AgentConfig 投影时 examples 被丢弃,meta 供 lintAgentMeta 使用)。
52
+ */
53
+ export function parseAgentWithMeta(
54
+ filePath: string,
55
+ content: string,
56
+ ): { config: AgentConfig; meta: AgentMeta | null } {
95
57
  const name = path.basename(filePath, ".md");
96
58
 
97
59
  // 无 frontmatter → 整个内容作为 systemPrompt
98
60
  if (!content.startsWith(FM_DELIM)) {
99
- return { name, systemPrompt: content.trim() };
61
+ return { config: { name, systemPrompt: content.trim() }, meta: null };
100
62
  }
101
63
 
102
64
  const closeIdx = content.indexOf(FM_DELIM, FM_DELIM.length);
@@ -104,28 +66,53 @@ export function parseAgentFrontmatter(filePath: string, content: string): AgentC
104
66
  // 未闭合 frontmatter:提取 name,其余作为 systemPrompt
105
67
  const yamlBlock = content.slice(FM_DELIM.length);
106
68
  return {
107
- name: extractYamlField(yamlBlock, "name") ?? name,
108
- systemPrompt: content.trim(),
69
+ config: {
70
+ name: extractYamlField(yamlBlock, "name") ?? name,
71
+ systemPrompt: content.trim(),
72
+ },
73
+ meta: null,
109
74
  };
110
75
  }
111
76
 
112
77
  const yamlBlock = content.slice(FM_DELIM.length, closeIdx);
113
78
  const body = content.slice(closeIdx + FM_DELIM.length).trim();
114
79
 
115
- const toolsRaw = extractYamlField(yamlBlock, "tools");
116
- const tools = toolsRaw
117
- ? toolsRaw.split(",").map((s) => s.trim()).filter(Boolean)
80
+ // m2:结构化路由字段(name/model/tools)经 IF1 parseResourceMeta(统一 parser),
81
+ // 消灭本地 frontmatter parser 与 subagent-list-injector 的重复。thinkingLevel/defaultBackground
82
+ // 是执行配置(非 AgentMeta 路由字段),仍用 extractYamlField 取。
83
+ const meta = parseResourceMeta(content, "agent");
84
+ const agentMeta = meta?.kind === "agent" ? meta : null;
85
+ // MF-3 regression fix:agentMeta=null(IF1 要求 name/description 必填,缺则 parseResourceMeta
86
+ // 返 null)时,model/tools 不能静默丢失——fallback 用 extractYamlField 取(保留重构前行为)。
87
+ // 注入路由可见性由 discovery/injector 按 available 标志决定,与本处(direct-path loadByPath)无关。
88
+ const modelFallback = extractYamlField(yamlBlock, "model");
89
+ const toolsFallbackRaw = extractYamlField(yamlBlock, "tools");
90
+ const toolsFallback = toolsFallbackRaw
91
+ ? toolsFallbackRaw.split(",").map((s) => s.trim()).filter(Boolean)
118
92
  : undefined;
119
-
93
+ if (!agentMeta && /^model:|^tools:/m.test(yamlBlock)) {
94
+ // m2 exec-review MINOR-2 + MF-3:IF1 未通过(缺 name/description)→ model/tools 经 legacy
95
+ // fallback 取(direct-path loadByPath 不丢配置,与重构前一致);但结构化路由注入不可见,
96
+ // 建议 agent 作者补 description 以满足 IF1。
97
+ logger.warn(
98
+ `[agent-registry] ${filePath}: agent frontmatter 缺 name/description(IF1 必填),` +
99
+ "model/tools 经 legacy fallback 生效(直接路径不丢配置),但结构化路由不可见——请补充 description",
100
+ );
101
+ }
120
102
  const defaultBackgroundRaw = extractYamlField(yamlBlock, "defaultBackground");
121
103
 
122
104
  return {
123
- name: extractYamlField(yamlBlock, "name") ?? name,
124
- systemPrompt: body,
125
- model: extractYamlField(yamlBlock, "model") ?? undefined,
126
- thinkingLevel: extractYamlField(yamlBlock, "thinkingLevel") ?? undefined,
127
- tools: tools && tools.length > 0 ? tools : undefined,
128
- defaultBackground: defaultBackgroundRaw === "true" ? true : undefined,
105
+ config: {
106
+ name: agentMeta?.name ?? name,
107
+ systemPrompt: body,
108
+ model: agentMeta?.model ?? modelFallback ?? undefined,
109
+ thinkingLevel: extractYamlField(yamlBlock, "thinkingLevel") ?? undefined,
110
+ tools: agentMeta?.tools && agentMeta.tools.length > 0
111
+ ? agentMeta.tools
112
+ : (toolsFallback && toolsFallback.length > 0 ? toolsFallback : undefined),
113
+ defaultBackground: defaultBackgroundRaw === "true" ? true : undefined,
114
+ },
115
+ meta: agentMeta,
129
116
  };
130
117
  }
131
118
 
@@ -145,116 +132,52 @@ function extractYamlField(yaml: string, key: string): string | undefined {
145
132
  // AgentRegistry
146
133
  // ============================================================
147
134
 
148
- /**
149
- * 发现配置:用于统一资源发现的扫描参数。
150
- */
151
- export interface AgentDiscoveryConfig {
152
- /** 项目根目录(findWorkspaceRoot 推导结果) */
153
- workspaceRoot: string;
154
- /** agent 配置目录(getAgentDir() 结果) */
155
- agentDir: string;
156
- }
157
-
158
- /**
159
- * agent 注册表。通过统一资源发现(shared/resource-discovery)扫描所有源。
160
- * hot-reload:每次 discoverAll 重扫,mtime 未变的文件跳过 read+parse。
161
- *
162
- * 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev →
163
- * project .pi → project .agents。builtin(包内)优先级最低。
164
- * 详见 ADR-031。
165
- */
166
135
  export class AgentRegistry {
167
- private readonly cache = new Map<string, AgentConfig>();
168
- /** 文件级 mtime 缓存(key=绝对路径,跨 discoverAll 保留)。 */
136
+ /** 文件级 mtime 缓存(key=绝对路径,跨 loadByPath 保留)。 */
169
137
  private readonly fileCache = new Map<string, FileCacheEntry>();
170
- /** 本轮扫描到的路径集(清理已删除文件的缓存)。 */
171
- private currentScanPaths = new Set<string>();
172
-
173
- constructor(private readonly discoveryConfig: AgentDiscoveryConfig) {}
174
138
 
175
- /** 扫描所有源 + 合并 builtin(hot-reload,每次重扫)。 */
176
- discoverAll(builtin: BuiltinAgentRegistry): void {
177
- this.cache.clear();
178
- this.currentScanPaths = new Set();
179
-
180
- const scanConfig: ScanConfig = {
181
- kind: "agents",
182
- workspaceRoot: this.discoveryConfig.workspaceRoot,
183
- agentDir: this.discoveryConfig.agentDir,
184
- };
185
- const resources = discoverResourcesSync(scanConfig);
186
-
187
- for (const resource of resources) {
188
- if (!resource.available) continue;
189
- this.currentScanPaths.add(resource.path);
190
- try {
191
- const config = this.loadWithMtimeCache(resource.path);
192
- if (config) this.cache.set(config.name, config);
193
- } catch (_err) {
194
- // 有意吞掉:文件不可读/解析失败 → 跳过(不阻断其他 agent 发现)
195
- void _err;
196
- }
197
- }
198
-
199
- // builtin 优先级最低(先写入,被文件 agent 覆盖)
200
- for (const agentName of builtin.list()) {
201
- if (!this.cache.has(agentName)) {
202
- const config = builtin.get(agentName);
203
- if (config) this.cache.set(agentName, config);
139
+ /**
140
+ * 按绝对路径加载 agent(agentRef 统一解析入口——S2 路径统一)。
141
+ *
142
+ * - ~/ 前缀展开;相对路径/非 .md 引用返回 undefined(引用唯一形态 = 绝对路径)
143
+ * - 文件不可读/不存在 → 驱逐缓存 + 返回 undefined(调用方给错误指引)
144
+ * - mtime 未变复用 config 缓存;cache-miss 时 W4 lint 一次
145
+ */
146
+ loadByPath(ref: string, require?: boolean): AgentConfig | undefined {
147
+ const filePath = normalizeRef(ref, AGENT_REF_EXT);
148
+ if (filePath === null) {
149
+ if (require) {
150
+ throw new Error(
151
+ `Invalid agent ref: ${ref}. Agent refs must be absolute paths to .md files (use <location> from <available_subagents>).`,
152
+ );
204
153
  }
154
+ return undefined;
205
155
  }
206
156
 
207
- // 清理本轮未扫描到的文件缓存条目(文件被删除/移走)
208
- for (const cachedPath of this.fileCache.keys()) {
209
- if (!this.currentScanPaths.has(cachedPath)) {
210
- this.fileCache.delete(cachedPath);
157
+ const file = getCachedFile(filePath);
158
+ if (file === null) {
159
+ // ENOENT/不可读 → 驱逐(mtime 缓存下文件删除不自愈的修复)
160
+ this.fileCache.delete(filePath);
161
+ if (require) {
162
+ throw new Error(
163
+ `Agent file not found or unreadable: ${filePath}. Use an absolute path from <available_subagents> <location>.`,
164
+ );
211
165
  }
166
+ return undefined;
212
167
  }
213
- }
214
-
215
- /** 按 name 查找。require=false 时找不到返回 undefined;true 时抛错。 */
216
- get(name: string, require?: boolean): AgentConfig | undefined {
217
- const config = this.cache.get(name);
218
- if (!config && require) {
219
- throw new Error(
220
- `Agent "${name}" not found. Discovered: ${[...this.cache.keys()].join(", ") || "(none)"}`,
221
- );
222
- }
223
- return config;
224
- }
225
-
226
- /** 列出所有已发现 agent 名(诊断/wizard 用)。 */
227
- list(): string[] {
228
- return [...this.cache.keys()];
229
- }
230
-
231
- // ── 内部 ──────────────────────────────────────────────────
232
168
 
233
- /** 带 mtime 缓存的单文件加载。mtime 未变复用缓存,否则 read+parse。 */
234
- private loadWithMtimeCache(filePath: string): AgentConfig | undefined {
235
- const stat = fs.statSync(filePath);
236
- const mtimeMs = stat.mtimeMs;
237
169
  const cached = this.fileCache.get(filePath);
238
- if (cached && cached.mtimeMs === mtimeMs) {
170
+ if (cached && cached.mtimeMs === file.mtimeMs) {
239
171
  return cached.config;
240
172
  }
241
- const content = fs.readFileSync(filePath, "utf-8");
242
- const config = parseAgentFrontmatter(filePath, content);
243
- this.fileCache.set(filePath, { mtimeMs, config });
173
+
174
+ const { config, meta } = parseAgentWithMeta(filePath, file.content);
175
+ // W4 lint:cache-miss(parse 时一次)——warn 只随 mtime 变化刷
176
+ const lintFindings = meta ? lintAgentMeta(meta) : [];
177
+ for (const finding of lintFindings) {
178
+ logger.warn(`[agent-registry] ${filePath}: ${finding.message}`);
179
+ }
180
+ this.fileCache.set(filePath, { mtimeMs: file.mtimeMs, config, meta });
244
181
  return config;
245
182
  }
246
183
  }
247
-
248
- // ============================================================
249
- // 包内 agent 发现(builtin 用,走 pi.agents manifest)
250
- // ============================================================
251
-
252
- import { processPackageSync } from "../shared/resource-discovery.ts";
253
-
254
- /**
255
- * 发现包内 agent 文件(走 pi.agents manifest 或约定目录 agents/)。
256
- * builtin 专用——不参与优先级合并,优先级最低。
257
- */
258
- function discoverPackageAgentsSync(packageRoot: string): DiscoveredResource[] {
259
- return processPackageSync(packageRoot, "agents");
260
- }
@@ -34,7 +34,7 @@ export const SLUG_MAX_LENGTH = 35;
34
34
  * ctxModel → ctxModel(完整 ModelInfo 对象透传,让 resolveModel 走第 3 层透明传递)
35
35
  * skillPath → skillPath
36
36
  * thinkingLevel → thinkingLevel(M1: 否则下游 subagent-service 读到 undefined)
37
- * systemPromptFiles → appendSystemPrompt(M2: resolveAgentOpts 写盘的临时文件需被 session-runner 消费)
37
+ * appendSystemPrompt → appendSystemPrompt(内容数组,同名同义透传)
38
38
  *
39
39
  * 忽略字段(委托后由 executeAndAwait 内部机制替代):
40
40
  * timeoutMs —— mergeTimeoutSignal 单独处理
@@ -61,7 +61,7 @@ export function mapToExecuteOptions(
61
61
  ctxModel,
62
62
  skillPath: opts.skillPath,
63
63
  thinkingLevel: opts.thinkingLevel,
64
- appendSystemPrompt: opts.systemPromptFiles,
64
+ appendSystemPrompt: opts.appendSystemPrompt,
65
65
  };
66
66
  }
67
67
 
@@ -6,8 +6,7 @@
6
6
  // 上游:SubagentService.execute 内部调 resolveModel。
7
7
  // session_start 时经 initModel 注入 modelRegistry。
8
8
 
9
- import { findWorkspaceRoot } from "../shared/resource-discovery.ts";
10
- import { AgentRegistry, createPackageBuiltinRegistry } from "./agent-registry.ts";
9
+ import { AgentRegistry } from "./agent-registry.ts";
11
10
  import {
12
11
  loadGlobalConfig,
13
12
  } from "./config.ts";
@@ -69,25 +68,15 @@ export class ModelConfigService {
69
68
  private globalConfig: SubagentsGlobalConfig;
70
69
  private readonly agentRegistry: AgentRegistry;
71
70
  private readonly agentRegistryDir: string;
72
- private readonly workspaceRoot: string;
73
71
  private modelRegistry: ModelRegistryLike | null = null;
74
72
  private _sessionId: string | undefined;
75
73
  /** 主 agent 当前 model 缓存(session_start 注入,model_select 刷新)。 */
76
74
  private _ctxModel: ModelInfo | undefined;
77
75
 
78
- /** 包内 builtin agent(agents/*.md,优先级最低,被用户覆盖)。 */
79
- private readonly builtinRegistry = createPackageBuiltinRegistry();
80
-
81
76
  constructor(init: ModelConfigServiceInit) {
82
77
  this.agentRegistryDir = init.agentDir;
83
- this.workspaceRoot = findWorkspaceRoot(init.cwd);
84
78
  this.globalConfig = loadGlobalConfig(init.agentDir);
85
- // 统一资源发现(ADR-031):扫描源由 workspaceRoot + agentDir 推导
86
- this.agentRegistry = new AgentRegistry({
87
- workspaceRoot: this.workspaceRoot,
88
- agentDir: this.agentRegistryDir,
89
- });
90
- this.agentRegistry.discoverAll(this.builtinRegistry);
79
+ this.agentRegistry = new AgentRegistry();
91
80
  }
92
81
 
93
82
  // ── 生命周期(index.ts 调)──────────────────────────────
@@ -99,9 +88,8 @@ export class ModelConfigService {
99
88
  * 3. setSessionId
100
89
  */
101
90
  initModel(init: ModelServiceSessionInit): void {
102
- // 1. 重载配置 + 重扫 agent(hot-reload:用户可能新增/修改 agent .md)
91
+ // 1. 重载配置(agent 按需 loadByPath,无预热扫描)
103
92
  this.globalConfig = loadGlobalConfig(this.agentRegistryDir);
104
- this.agentRegistry.discoverAll(this.builtinRegistry);
105
93
 
106
94
  // 2. modelRegistry(fail-fast)
107
95
  if (init.modelRegistry === null) {
@@ -127,24 +115,26 @@ export class ModelConfigService {
127
115
  /**
128
116
  * 解析 agent 的模型(三层:override → agentConfig → 主 agent model)。
129
117
  *
130
- * @param agentName agent 名(查 agentConfig.model override)
131
- * @param override 调用方显式 override(最高优先级)
132
- * @param ctxModel 主 agent 当前模型(兜底,直接透传)
118
+ * @param agentRef agent 引用(.md 绝对路径;查 agentConfigmodel override)
119
+ * @param override 调用方显式 override(最高优先级)
120
+ * @param ctxModel 主 agent 当前模型(兜底,直接透传)
133
121
  */
134
122
  resolveModel(
135
- agentName: string,
123
+ agentRef: string,
136
124
  override?: { model?: string; thinkingLevel?: string },
137
125
  ctxModel?: ModelInfo,
126
+ /** 已解析的 agent 配置(调用方已加载时复用,避免同一 agentRef 二次 loadByPath)。 */
127
+ agentConfig?: AgentConfig,
138
128
  ): ResolvedModel {
139
129
  this.assertReady();
140
- const agentConfig = this.agentRegistry.get(agentName);
130
+ const config = agentConfig ?? (agentRef ? this.agentRegistry.loadByPath(agentRef) : undefined);
141
131
  // ctxModel 优先用显式传入(execute 路径),其次用 session 缓存(renderCall 路径)
142
- return resolveModel(agentConfig, this.modelRegistry!, override, ctxModel ?? this._ctxModel);
132
+ return resolveModel(config, this.modelRegistry!, override, ctxModel ?? this._ctxModel);
143
133
  }
144
134
 
145
135
  /** 查询 agent 配置(SubagentService 内部判定 defaultBackground 用)。 */
146
- getAgentConfig(name?: string): AgentConfig | undefined {
147
- return name ? this.agentRegistry.get(name) : undefined;
136
+ getAgentConfig(agentRef?: string): AgentConfig | undefined {
137
+ return agentRef ? this.agentRegistry.loadByPath(agentRef) : undefined;
148
138
  }
149
139
 
150
140
  // ── 配置读取(subagent-service 调)────────────────────────
@@ -164,17 +154,6 @@ export class ModelConfigService {
164
154
  return this.agentRegistryDir;
165
155
  }
166
156
 
167
- /**
168
- * AgentRegistry 实例(workflow 域复用,F-4/D-003 统一)。
169
- *
170
- * workflow 域(resolveAgentOpts via LifecycleDeps.agentRegistry)需要与 subagents 域
171
- * 完全相同的 agent 名→config 解析,复用本实例避免二次扫描。registry 在 initModel
172
- * (每次 session_start)调 discoverAll hot-reload,两域共享同一份发现结果。
173
- */
174
- getAgentRegistry(): AgentRegistry {
175
- return this.agentRegistry;
176
- }
177
-
178
157
  /** modelRegistry(SubagentService 构造 factoryCtx 时读)。已注入保证非 null。 */
179
158
  getModelRegistry(): ModelRegistryLike {
180
159
  if (this.modelRegistry === null) {
@@ -60,8 +60,10 @@ export interface ResolvedModel {
60
60
  // 常量
61
61
  // ============================================================
62
62
 
63
- /** thinking level 支持顺序(低→高),用于 clamp 到 model 可用级别。 */
64
- const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
63
+ /** thinking level 支持顺序(低→高),用于 clamp 到 model 可用级别。
64
+ * SSOT(单一权威源):新增 thinking 级别只改此处,subagent-tool thinkingLevel 枚举
65
+ * 从本常量派生,避免两处硬编码不同步。 */
66
+ export const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
65
67
 
66
68
  /** 解析失败时错误信息列出的可用模型上限(防超长错误信息)。 */
67
69
  const MODEL_LIST_LIMIT = 20;
@@ -102,7 +104,7 @@ export function resolveModel(
102
104
  if (paramOverride?.model) {
103
105
  return lookupAndResolve(
104
106
  paramOverride.model,
105
- paramOverride.thinkingLevel,
107
+ paramOverride.thinkingLevel ?? agentConfig?.thinkingLevel,
106
108
  modelRegistry,
107
109
  "paramOverride",
108
110
  );
@@ -394,8 +394,9 @@ export class SubagentService {
394
394
  agent: string,
395
395
  override?: { model?: string; thinkingLevel?: string },
396
396
  ctxModel?: ModelInfo,
397
+ agentConfig?: AgentConfig,
397
398
  ): ResolvedModel {
398
- return this.modelService.resolveModel(agent, override, ctxModel);
399
+ return this.modelService.resolveModel(agent, override, ctxModel, agentConfig);
399
400
  }
400
401
 
401
402
  /**
@@ -599,7 +600,7 @@ export class SubagentService {
599
600
  // - **不可作为后续 agent 的 cwd**——目录已删,复用会 ENOENT。
600
601
  // - wave 内 worktree 复用(spec-w §2 "wave 内 8 action 共享 worktree")在 pi 当前架构下
601
602
  // 不可行:worktree 绑定单次 agent() record,每次 executeAndAwait 结束 finalizeRecord
602
- // 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd(见 recursive-split.js)。
603
+ // 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd
603
604
  wfResult.worktreePath = record.worktreeHandle?.path;
604
605
  return wfResult;
605
606
  }
@@ -626,15 +627,17 @@ export class SubagentService {
626
627
 
627
628
  /** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。 */
628
629
  private async resolveIdentity(opts: ExecuteOptions): Promise<ResolvedIdentity> {
629
- // 未显式指定 agent 时兜底为 DEFAULT_AGENT_NAME(与 TUI extractAgentName 共用同一常量,
630
- // 保证 block 标题显示的名与实际加载的 agent.md 一致)。见 types.ts 常量注释。
630
+ // agentRef 语义(S2):agent 参数 = .md 绝对路径;不传 = 不加载 agentConfig,
631
+ // 直接用 override agent model。DEFAULT_AGENT_NAME 仅作 record 显示名
632
+ // (TUI 层 extractAgentName 共用,保证显示一致)。
631
633
  const agent = opts.agent ?? DEFAULT_AGENT_NAME;
632
- const agentConfig = this.modelService.getAgentConfig(agent);
634
+ const agentConfig = opts.agent ? this.modelService.getAgentConfig(opts.agent) : undefined;
633
635
 
634
636
  const resolved = this.modelService.resolveModel(
635
- agent,
637
+ opts.agent ?? "",
636
638
  { model: opts.model, thinkingLevel: opts.thinkingLevel },
637
639
  opts.ctxModel,
640
+ agentConfig,
638
641
  );
639
642
 
640
643
  return { agent, agentConfig, resolved };
@@ -32,8 +32,9 @@ import type { ExecuteOptions } from "./types.ts";
32
32
  * - subagentService: 进程单例(getSubagentService()),委托目标
33
33
  * - ctxModel: 当前 session 主 agent 模型(D-008 model 填底,opts.model 空时用此)
34
34
  *
35
- * 不含 agentRegistry/sessionDir/activeTempFiles——resolveAgentOpts 在 orchestration
36
- * 完成(D-A3),结果已填进 AgentCallOpts.skillPath/schemaEnv,SAR 收已解析的 opts。
35
+ * resolveAgentOpts 在 orchestration 层完成(M2 修正后单参数,只处理 schema SO 指令 + skill),
36
+ * 结果填进 AgentCallOpts.skillPath/schemaEnv/appendSystemPrompt,SAR 收已解析的 opts。
37
+ * 不含 agent/session 目录/临时文件集合依赖(agent ref 交 resolveIdentity,无临时文件)。
37
38
  */
38
39
  export interface SubprocessAgentRunnerDeps {
39
40
  subagentService: SubagentService;