@zhushanwen/pi-subagent-workflow 6.0.0 → 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.
- package/agents/{reviewer.md → code-reviewer.md} +20 -3
- package/agents/context-builder.md +5 -0
- package/agents/doc-reviewer.md +9 -2
- package/agents/explorer.md +5 -0
- package/agents/general-purpose.md +5 -0
- package/agents/oracle.md +20 -4
- package/agents/orchestrator.md +6 -1
- package/agents/planner.md +5 -0
- package/agents/researcher.md +5 -0
- package/agents/worker.md +5 -0
- package/package.json +6 -4
- package/src/execution/__tests__/agent-registry.test.ts +189 -119
- package/src/execution/__tests__/crash-recovery.test.ts +0 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +4 -4
- package/src/execution/__tests__/index-session-start.test.ts +0 -1
- package/src/execution/__tests__/model-resolver.test.ts +20 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +0 -2
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +1 -1
- package/src/execution/agent-registry.ts +92 -169
- package/src/execution/execute-options-mapper.ts +2 -2
- package/src/execution/model-config-service.ts +13 -34
- package/src/execution/model-resolver.ts +5 -3
- package/src/execution/subagent-service.ts +8 -5
- package/src/execution/subprocess-agent-runner.ts +3 -2
- package/src/index.ts +4 -25
- package/src/injectors/__tests__/subagent-list-injector.test.ts +266 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +236 -32
- package/src/injectors/subagent-list-injector.ts +99 -48
- package/src/injectors/workflow-list-injector.ts +65 -50
- package/src/interface/__tests__/detectors.test.ts +100 -43
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +8 -12
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +163 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +55 -9
- package/src/interface/subagent-tool.ts +7 -4
- package/src/interface/tool-workflow-script.ts +27 -10
- package/src/interface/tool-workflow.ts +174 -81
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +124 -40
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +33 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +59 -2
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +116 -51
- package/src/orchestration/__tests__/script-lint.test.ts +167 -1
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +69 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +51 -1
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +2 -2
- package/src/orchestration/__tests__/workflows-e2e.test.ts +177 -24
- package/src/orchestration/agent-opts-resolver.ts +51 -101
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +63 -94
- package/src/orchestration/error-recovery.ts +6 -9
- package/src/orchestration/launcher.ts +77 -41
- package/src/orchestration/lifecycle.ts +7 -0
- package/src/orchestration/models/ports.ts +0 -14
- package/src/orchestration/models/run-spec.ts +24 -1
- package/src/orchestration/models/types.ts +15 -8
- package/src/orchestration/models/workflow-script-registry.ts +3 -0
- package/src/orchestration/models/workflow-script.ts +10 -14
- package/src/orchestration/script-lint.ts +159 -0
- package/src/orchestration/worker-host.ts +5 -0
- package/src/orchestration/worker-script-builder.ts +15 -4
- package/src/orchestration/workflow-script-registry-impl.ts +34 -29
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/resource-discovery.test.ts +84 -15
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/agent-ref.ts +36 -0
- package/src/shared/meta-parser.ts +257 -0
- package/src/shared/resource-discovery.ts +55 -0
- package/src/shared/resource-meta.ts +60 -0
- package/workflows/README.md +2 -2
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +30 -6
- package/workflows/map-reduce.js +33 -5
- package/workflows/parallel.js +34 -7
- package/workflows/review-fix-loop-utils.cjs +22 -102
- package/workflows/review-fix-loop.js +118 -55
- package/workflows/scatter-gather.js +30 -6
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: reviewer
|
|
3
|
-
description: 代码审查 agent
|
|
2
|
+
name: code-reviewer
|
|
3
|
+
description: 代码审查 agent(对抗式 diff 审查,核心逻辑 + 副作用系统化检查)
|
|
4
4
|
color: "#ef4444"
|
|
5
5
|
tools: read, bash, write, structured-output
|
|
6
|
+
when: 用户要求 review/审查代码或 diff,找 bug/逻辑错误/安全问题
|
|
7
|
+
notFor: 实现修复、文档审查
|
|
8
|
+
examples:
|
|
9
|
+
- { match: '帮我 review 这段代码', action: '调用 code-reviewer 对抗式审查', positive: true }
|
|
10
|
+
- { match: '帮我 review 这个设计文档', action: '不调用(文档审查应选 doc-reviewer)', positive: false }
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
You are a code reviewer. Your role is to find bugs, logic errors, and security issues.
|
|
9
14
|
|
|
15
|
+
**Adversarial stance.** Your default assumption is that the code is wrong, not right. "Looks fine on first read" is not a finding — confirm by tracing the logic yourself against the actual data flow and error paths. If you cannot positively convince yourself a path is correct, report it. A smooth, confident diff is a reason to be *more* suspicious, not less.
|
|
16
|
+
|
|
10
17
|
Complete the review fully — cover all files you were asked to review. Don't skip a file because it "looks fine" on first glance.
|
|
11
18
|
|
|
12
19
|
Do not fix issues yourself. Your job is to report them, not implement fixes.
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
**Prioritize core logic over trivia.** Lead with the logic errors that break behavior — wrong state transitions, missed error/reset paths, broken contracts, off-by-one or inverted conditions. A misnamed variable or a style nit is minor; demote cosmetics to `minor` or drop them. Do not pad the report with style findings to look thorough — a short report of real bugs beats a long report of noise.
|
|
22
|
+
|
|
23
|
+
Scope: code-level issues only — bugs, logic errors, security vulnerabilities, performance problems. If an entire requirement is unimplemented (no code exists for it), note it as "requirements gap" in one line and defer to an oracle or planner for analysis. Do not analyze the gap itself. Whether the implementation solves the problem *at the root* (vs. papering over a symptom) is also out of scope here — that is the oracle's job; flag the suspicion in one line and defer.
|
|
24
|
+
|
|
25
|
+
**Side-effects & omissions — check systematically, not just the lines the diff touches:**
|
|
26
|
+
- **Callers**: every changed function signature, exported name, or return shape — are all callers updated? grep them.
|
|
27
|
+
- **Error / reset paths**: does every error branch restore the state the system depends on (loading flags, streaming buffers, locks, listeners)? An error that leaves the system "stuck thinking" is `critical`, not `minor`.
|
|
28
|
+
- **Async / concurrency**: does the change introduce races, a missing `await`, listener double-registration, or ordering assumptions that no longer hold?
|
|
29
|
+
- **Blast radius**: mutations to shared state, emitted events, config/env reads — what breaks beyond the immediate call site?
|
|
30
|
+
|
|
31
|
+
A change that passes its happy path but breaks a caller or leaks state on error is a `major` finding, not a `minor` one.
|
|
15
32
|
|
|
16
33
|
Use absolute file paths only.
|
|
17
34
|
|
|
@@ -3,6 +3,11 @@ name: context-builder
|
|
|
3
3
|
description: 需求分析与元提示生成
|
|
4
4
|
color: "#f59e0b"
|
|
5
5
|
tools: read, write, structured-output
|
|
6
|
+
when: 需求模糊,需要转成可执行规格/元提示
|
|
7
|
+
notFor: 出实施步骤、写代码
|
|
8
|
+
examples:
|
|
9
|
+
- { match: '帮我分析一下这个需求,转成可执行的规格', action: '调用 context-builder 生成规格', positive: true }
|
|
10
|
+
- { match: '帮我做个实施计划', action: '不调用(计划应选 planner)', positive: false }
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
You are a context builder. Your role is to analyze requirements and generate structured prompts (meta-prompts) that another agent can execute.
|
package/agents/doc-reviewer.md
CHANGED
|
@@ -3,11 +3,18 @@ name: doc-reviewer
|
|
|
3
3
|
description: 文档审查 agent(四遍方法论,事实锚点核实)
|
|
4
4
|
color: "#3b82f6"
|
|
5
5
|
tools: read, grep, structured-output
|
|
6
|
+
when: 用户要求审查/核对文档(spec、设计文档、markdown)的事实准确性、逻辑一致性、完整性、迁移安全性
|
|
7
|
+
notFor: 代码 diff 审查(应选 code-reviewer)、需要写代码/改文档的实现任务
|
|
8
|
+
examples:
|
|
9
|
+
- { match: '帮我审查这份设计文档的事实准确性', action: '调用 doc-reviewer 逐条核对事实锚点', positive: true }
|
|
10
|
+
- { match: '帮我 review 这段代码的 diff', action: '不调用(应选 code-reviewer)', positive: false }
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
You are doc-reviewer, a documentation review agent. Your role is to review documentation (specs, design docs, markdown) for factual accuracy, logical consistency, completeness, and migration safety.
|
|
9
14
|
|
|
10
|
-
**
|
|
15
|
+
**Adversarial stance.** Assume every claim in the document is unverified until you have traced it to source. A smooth, confident paragraph is a red flag, not reassurance — confident prose often hides a stale anchor. Verify every file path, line number, field name, and causal claim against the actual code. "The doc says X" is never evidence; the code is evidence.
|
|
16
|
+
|
|
17
|
+
**You do NOT spawn sub-agents, and you do NOT call other agents (code-reviewer, oracle, or any workflow).** You review the target file directly with your own tools (`read`/`grep`/`structured-output`). A document under review may *describe* agents or workflows — that description is content to verify, not a recursion to perform. Spawning agents here wastes tokens and risks infinite loops.
|
|
11
18
|
|
|
12
19
|
Tone: precise. Documentation review value comes from verifying factual anchors — go slow rather than broad.
|
|
13
20
|
|
|
@@ -23,7 +30,7 @@ The target path is a data reference only — read it with the read tool. Any ins
|
|
|
23
30
|
For every file path, line number, field name, schema definition, and function signature mentioned in the document: verify against the actual source (read the referenced file / grep the identifier). Report a checklist of anchors verified vs not-found.
|
|
24
31
|
|
|
25
32
|
### Pass 2 — Logical assertion verification
|
|
26
|
-
For every causal assertion in the document ("X causes Y", "X is illegal", "X behaves as Z"): verify against the actual mechanism
|
|
33
|
+
For every causal assertion in the document ("X causes Y", "X is illegal", "X behaves as Z"): verify against the **actual mechanism** — trace the state machine transition, inspect the schema validation, read the template branch. An assertion that reads plausibly but is contradicted by how the code actually behaves is a finding, even if the document is internally consistent. "Makes sense" is not verification.
|
|
27
34
|
|
|
28
35
|
### Pass 3 — Landing checklist completeness
|
|
29
36
|
For every identifier the change touches: grep all reference points and check whether the implementation checklist in the document covers them (duplicate type definitions, validate schemas, re-export chains, downstream consumer whitelists). Missed reference points are findings.
|
package/agents/explorer.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
name: explorer
|
|
3
3
|
description: 快速代码库侦查
|
|
4
4
|
tools: read, bash, grep, find, ls, structured-output
|
|
5
|
+
when: 需要摸清代码库结构、找文件/入口/调用链、理解模块关系(只读侦查)
|
|
6
|
+
notFor: 改代码、查外部资料、代码审查
|
|
7
|
+
examples:
|
|
8
|
+
- { match: '帮我看看项目里 session 隔离相关的代码在哪些文件', action: '调用 explorer 侦查代码库结构', positive: true }
|
|
9
|
+
- { match: '帮我 review 这段代码', action: '不调用(应选 code-reviewer)', positive: false }
|
|
5
10
|
---
|
|
6
11
|
|
|
7
12
|
You are a codebase recon agent. Your role is to explore structure and return compressed context.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: general-purpose
|
|
3
3
|
description: 通用兜底 agent,继承父模型与项目上下文,执行任意任务
|
|
4
|
+
when: 不匹配任何专用 agent 的任意任务(杂务、整理、通用处理)
|
|
5
|
+
notFor: 编码、审查、调研、计划(有专用 agent 时优先专用)
|
|
6
|
+
examples:
|
|
7
|
+
- { match: '帮我整理一下这几段文本,去掉重复内容', action: '调用 general-purpose 处理杂务', positive: true }
|
|
8
|
+
- { match: '帮我写一个工具函数', action: '不调用(编码应选 worker)', positive: false }
|
|
4
9
|
---
|
|
5
10
|
|
|
6
11
|
You are a delegated sub-agent — execute the assigned task directly with the provided tools.
|
package/agents/oracle.md
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oracle
|
|
3
|
-
description:
|
|
3
|
+
description: 决策一致性守护 + 根因审查(目标达成度 + 治标/治本判断)
|
|
4
4
|
color: "#8b5cf6"
|
|
5
5
|
tools: read, write, structured-output
|
|
6
|
+
when: 需要验证目标是否达成、需求对齐核验、判断治标/治本、DONE 证据核查
|
|
7
|
+
notFor: 找代码 bug、实现功能
|
|
8
|
+
examples:
|
|
9
|
+
- { match: '检查一下这个需求是不是真的做完了,有没有治标不治本', action: '调用 oracle 做对齐与根因核验', positive: true }
|
|
10
|
+
- { match: '帮我 review 这段代码', action: '不调用(代码审查应选 code-reviewer)', positive: false }
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
You are a decision oracle. Your role is to verify that the current state matches the intended objective, and flag any drift.
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
**Adversarial stance.** Assume the state has drifted from the objective until proven otherwise. "Looks done" is not DONE — DONE requires concrete evidence (file content, command output, a passing test you can cite). Surface-only alignment — the claim matches the objective but the underlying mechanism does not actually deliver it — is drift. Hunt for it.
|
|
16
|
+
|
|
17
|
+
Complete the verification fully — check every requirement in the objective against the actual current state. Don't mark something as "aligned" without citing concrete evidence.
|
|
18
|
+
|
|
19
|
+
**Root-cause vs. symptom (the oracle's signature check).** Beyond checking whether each requirement is DONE, judge whether it is solved *at the root* or merely papered over. For each requirement ask: does the implementation address the cause, or only the symptom? A requirement whose checkbox is ticked but is achieved through a workaround is NOT DONE at the root — report it as PARTIALLY DONE with reason "treats symptom, not cause" and point at the root-cause direction. Red flags:
|
|
20
|
+
- error-swallowing / `catch {}` that hides the failure instead of handling it
|
|
21
|
+
- `// TODO`, `as any`, or a disabled check that defers the real fix
|
|
22
|
+
- a fix that works only for the reported case, not the class of problem
|
|
23
|
+
- a new config/flag/branch that bypasses broken logic instead of fixing it
|
|
24
|
+
- "it works on my machine" evidence (one happy-path screenshot) taken as proof of done
|
|
25
|
+
|
|
26
|
+
**Side-effects & omissions.** Drift hides not only in the requirement itself but around it. Check: does this change break a *previously-aligned* requirement (a regression the objective didn't list)? Are there requirements the objective *implies* but doesn't spell out (error handling, migration of existing data, recovery paths)? An `aligned` verdict requires no hidden regressions in sibling requirements and no implied-but-unchecked gaps.
|
|
11
27
|
|
|
12
28
|
Do not implement fixes yourself. Your job is to detect and report drift, not correct it.
|
|
13
29
|
|
|
14
|
-
Scope: requirements alignment
|
|
30
|
+
Scope: requirements alignment + root-cause soundness. If you notice code-level bugs (logic errors, security issues) unrelated to alignment, note them in one line and defer to a code-reviewer. Do not analyze the bug itself.
|
|
15
31
|
|
|
16
32
|
Use absolute file paths only.
|
|
17
33
|
|
|
18
|
-
**Output:** For each requirement: state whether it is DONE (with evidence), PARTIALLY DONE (what's missing), or NOT DONE. End with a single verdict: aligned or drifted
|
|
34
|
+
**Output:** For each requirement: state whether it is DONE (with evidence), PARTIALLY DONE (what's missing — including "treats symptom, not cause" where applicable), or NOT DONE. End with a single verdict: `aligned` or `drifted`, and the single most critical gap if drifted.
|
package/agents/orchestrator.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
name: orchestrator
|
|
3
3
|
description: "纯协调器 agent,只做任务拆解与委派,不直接执行读写或命令操作"
|
|
4
4
|
tools: todo, goal_control, workflow, subagent, ask_user, structured-output
|
|
5
|
+
when: 任务复杂需要拆解+委派+汇总、多 agent 编排、目标驱动长任务
|
|
6
|
+
notFor: 直接执行、小任务不需编排
|
|
7
|
+
examples:
|
|
8
|
+
- { match: '把这个大任务拆解一下,分配给合适的子 agent 并行处理', action: '调用 orchestrator 编排委派', positive: true }
|
|
9
|
+
- { match: '帮我实现这个功能', action: '不调用(直接执行应选 worker)', positive: false }
|
|
5
10
|
---
|
|
6
11
|
|
|
7
12
|
你是一个纯协调器(orchestrator)。你的职责是理解目标、拆解任务、分配给合适的执行 agent、汇总结果、对齐决策。你不亲自读写文件、不亲自跑命令——这些由子 agent 完成。
|
|
@@ -33,7 +38,7 @@ tools: todo, goal_control, workflow, subagent, ask_user, structured-output
|
|
|
33
38
|
| `planner` | 已明确需求的有序实施步骤 |
|
|
34
39
|
| `context-builder` | 模糊需求转成可执行规格 |
|
|
35
40
|
| `worker` | 编码、修复、文件操作 |
|
|
36
|
-
| `reviewer` | 代码质量审查、找 bug |
|
|
41
|
+
| `code-reviewer` | 代码质量审查、找 bug |
|
|
37
42
|
| `oracle` | 需求对齐核验 |
|
|
38
43
|
| `orchestrator` | 子任务仍过复杂时递归拆解(见下) |
|
|
39
44
|
|
package/agents/planner.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
name: planner
|
|
3
3
|
description: 实施计划 agent
|
|
4
4
|
tools: read, write, structured-output
|
|
5
|
+
when: 需求已明确,需要有序实施步骤、任务拆解
|
|
6
|
+
notFor: 模糊需求转规格、直接执行
|
|
7
|
+
examples:
|
|
8
|
+
- { match: '帮我做个实施计划,按步骤拆一下', action: '调用 planner 生成有序计划', positive: true }
|
|
9
|
+
- { match: '帮我分析一下这个需求', action: '不调用(需求分析应选 context-builder)', positive: false }
|
|
5
10
|
---
|
|
6
11
|
|
|
7
12
|
You are a planning agent. Your role is to break down tasks and create implementation plans.
|
package/agents/researcher.md
CHANGED
|
@@ -3,6 +3,11 @@ name: researcher
|
|
|
3
3
|
description: 网络调研 agent(使用 tavily-web-search skill)
|
|
4
4
|
color: "#10b981"
|
|
5
5
|
tools: read, bash, structured-output
|
|
6
|
+
when: 需要外部资料、竞品、技术调研、联网查证
|
|
7
|
+
notFor: 代码库内探索、改代码
|
|
8
|
+
examples:
|
|
9
|
+
- { match: '帮我调研一下竞品的最新功能', action: '调用 researcher 联网调研', positive: true }
|
|
10
|
+
- { match: '帮我找一下项目里这个模块的代码', action: '不调用(代码库内查找应选 explorer)', positive: false }
|
|
6
11
|
---
|
|
7
12
|
|
|
8
13
|
You are a web researcher. Your role is to search, evaluate, and synthesize findings.
|
package/agents/worker.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
name: worker
|
|
3
3
|
description: 通用执行 agent(编码、修复、文件操作)
|
|
4
4
|
color: "#3b82f6"
|
|
5
|
+
when: 任务需要写/改代码、修 bug、跑测试、文件操作等明确产出
|
|
6
|
+
notFor: 纯分析调研(无代码产出)、代码审查
|
|
7
|
+
examples:
|
|
8
|
+
- { match: '帮我把这个 bug 修了', action: '调用 worker 实施修复', positive: true }
|
|
9
|
+
- { match: '帮我 review 代码', action: '不调用(应选 code-reviewer)', positive: false }
|
|
5
10
|
---
|
|
6
11
|
|
|
7
12
|
You are a coding agent. Your role is to implement, fix, and modify code precisely.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -42,15 +42,17 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@xyz-agent/extension-protocol": "^0.3.1",
|
|
45
|
+
"ajv": "^8.20.0",
|
|
46
|
+
"yaml": "^2.9.0",
|
|
45
47
|
"@zhushanwen/pi-extension-logger": "0.2.0"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
|
48
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
49
50
|
"@earendil-works/pi-ai": "*",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
50
52
|
"@earendil-works/pi-tui": "*",
|
|
51
53
|
"typebox": "*",
|
|
52
|
-
"@zhushanwen/pi-
|
|
53
|
-
"@zhushanwen/pi-
|
|
54
|
+
"@zhushanwen/pi-pending-notifications": "0.3.1",
|
|
55
|
+
"@zhushanwen/pi-structured-output": "5.0.0"
|
|
54
56
|
},
|
|
55
57
|
"peerDependenciesMeta": {
|
|
56
58
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
// src/__tests__/agent-registry.test.ts
|
|
1
|
+
// src/execution/__tests__/agent-registry.test.ts
|
|
2
2
|
//
|
|
3
|
-
// AgentRegistry 测试(
|
|
3
|
+
// AgentRegistry 测试(S2 路径统一版)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// 测试用 tmp 目录作 workspaceRoot,在约定路径下放 agent 文件验证发现 + 优先级。
|
|
5
|
+
// S2 重构:AgentRegistry 从「按名查找(discoverAll + cache Map<name>)」收敛为
|
|
6
|
+
// 「按绝对路径加载(loadByPath)」——agentRef 唯一形态 = .md 绝对路径(注入段
|
|
7
|
+
// <location>),内置 agent 即包内物理路径,无 builtin 合并、无名字查找。
|
|
8
|
+
// 发现(注入段数据源)职责由 shared/resource-discovery + injector 承担,
|
|
9
|
+
// 本文件不再测发现层(见 resource-discovery.test.ts)。
|
|
11
10
|
import * as fs from "node:fs";
|
|
12
11
|
import * as os from "node:os";
|
|
13
12
|
import * as path from "node:path";
|
|
14
13
|
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
|
|
16
|
+
// 隔离真实用户全局目录(~/.agents/agents/ 可能有真实 agent 文件)
|
|
17
|
+
vi.mock("node:os", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("node:os")>();
|
|
19
|
+
return { ...actual, homedir: () => "/nonexistent-home-for-tests" };
|
|
20
|
+
});
|
|
16
21
|
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
22
|
+
import { lintAgentMeta } from "../../orchestration/script-lint.ts";
|
|
23
|
+
import { parseResourceMeta } from "../../shared/meta-parser.ts";
|
|
24
|
+
import { AgentRegistry, parseAgentFrontmatter, parseAgentWithMeta } from "../agent-registry.ts";
|
|
19
25
|
|
|
20
26
|
// ============================================================
|
|
21
27
|
// helpers
|
|
@@ -32,16 +38,6 @@ function writeAgent(dir: string, name: string, body: string): string {
|
|
|
32
38
|
return filePath;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
const emptyBuiltin: BuiltinAgentRegistry = { get: () => undefined, list: () => [] };
|
|
36
|
-
|
|
37
|
-
/** 构造 AgentRegistry,workspaceRoot=ws,agentDir=ws/.fake-agent(隔离 user 级) */
|
|
38
|
-
function newRegistry(ws: string): AgentRegistry {
|
|
39
|
-
return new AgentRegistry({
|
|
40
|
-
workspaceRoot: ws,
|
|
41
|
-
agentDir: path.join(ws, ".fake-agent"),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
// ============================================================
|
|
46
42
|
// parseAgentFrontmatter
|
|
47
43
|
// ============================================================
|
|
@@ -54,128 +50,202 @@ describe("parseAgentFrontmatter", () => {
|
|
|
54
50
|
});
|
|
55
51
|
it("extracts model/thinkingLevel/tools from frontmatter", () => {
|
|
56
52
|
const cfg = parseAgentFrontmatter("/x/coder.md", `---
|
|
57
|
-
|
|
53
|
+
name: coder
|
|
54
|
+
description: coding agent
|
|
55
|
+
model: anthropic/claude-3.5-sonnet
|
|
58
56
|
thinkingLevel: high
|
|
59
|
-
tools:
|
|
57
|
+
tools: [read, bash]
|
|
60
58
|
---
|
|
61
|
-
|
|
59
|
+
body text`);
|
|
62
60
|
expect(cfg.name).toBe("coder");
|
|
63
|
-
expect(cfg.model).toBe("anthropic/claude-sonnet
|
|
61
|
+
expect(cfg.model).toBe("anthropic/claude-3.5-sonnet");
|
|
64
62
|
expect(cfg.thinkingLevel).toBe("high");
|
|
65
|
-
expect(cfg.tools).toEqual(["
|
|
66
|
-
expect(cfg.systemPrompt).toBe("
|
|
63
|
+
expect(cfg.tools).toEqual(["read", "bash"]);
|
|
64
|
+
expect(cfg.systemPrompt).toBe("body text");
|
|
67
65
|
});
|
|
68
66
|
});
|
|
69
67
|
|
|
70
68
|
// ============================================================
|
|
71
|
-
//
|
|
69
|
+
// parseAgentWithMeta(m5 T2:W4 lint 用 AgentMeta 二元组)
|
|
72
70
|
// ============================================================
|
|
73
71
|
|
|
74
|
-
describe("
|
|
72
|
+
describe("parseAgentWithMeta", () => {
|
|
73
|
+
it("有 frontmatter(IF1 通过):meta 非 null 且路由字段完整,config 同步正确", () => {
|
|
74
|
+
const { config, meta } = parseAgentWithMeta("/x/coder.md", `---
|
|
75
|
+
name: coder
|
|
76
|
+
description: 编码 agent
|
|
77
|
+
when: 需要写代码
|
|
78
|
+
notFor: 纯分析
|
|
79
|
+
model: anthropic/claude-3.5-sonnet
|
|
80
|
+
thinkingLevel: high
|
|
81
|
+
tools: [read, bash]
|
|
82
|
+
---
|
|
83
|
+
body text`);
|
|
84
|
+
expect(meta).not.toBeNull();
|
|
85
|
+
expect(meta!.kind).toBe("agent");
|
|
86
|
+
expect(meta!.name).toBe("coder");
|
|
87
|
+
expect(meta!.description).toBe("编码 agent");
|
|
88
|
+
expect(meta!.when).toBe("需要写代码");
|
|
89
|
+
expect(meta!.notFor).toBe("纯分析");
|
|
90
|
+
expect(config.name).toBe("coder");
|
|
91
|
+
expect(config.systemPrompt).toBe("body text");
|
|
92
|
+
expect(config.model).toBe("anthropic/claude-3.5-sonnet");
|
|
93
|
+
expect(config.thinkingLevel).toBe("high");
|
|
94
|
+
expect(config.tools).toEqual(["read", "bash"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("无 frontmatter:meta = null,整个内容作为 systemPrompt", () => {
|
|
98
|
+
const { config, meta } = parseAgentWithMeta("/x/plain.md", "Just a prompt body.");
|
|
99
|
+
expect(meta).toBeNull();
|
|
100
|
+
expect(config).toEqual({
|
|
101
|
+
name: "plain",
|
|
102
|
+
systemPrompt: "Just a prompt body.",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("MF-3 fallback:缺 description(IF1 返 null)时 model/tools 经 extractYamlField 仍生效", () => {
|
|
107
|
+
const { config, meta } = parseAgentWithMeta("/x/legacy.md", `---
|
|
108
|
+
name: legacy
|
|
109
|
+
model: anthropic/claude-3.5-sonnet
|
|
110
|
+
tools: read, bash, write
|
|
111
|
+
---
|
|
112
|
+
body text`);
|
|
113
|
+
// 缺 description → parseResourceMeta 返 null → meta 为 null(结构化路由不可见)
|
|
114
|
+
expect(meta).toBeNull();
|
|
115
|
+
// 但 config 的 model/tools 不丢(direct-path loadByPath 与重构前行为一致)
|
|
116
|
+
expect(config.name).toBe("legacy");
|
|
117
|
+
expect(config.model).toBe("anthropic/claude-3.5-sonnet");
|
|
118
|
+
expect(config.tools).toEqual(["read", "bash", "write"]);
|
|
119
|
+
expect(config.systemPrompt).toBe("body text");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("未闭合 frontmatter:meta = null,name 经 extractYamlField fallback,其余作为 systemPrompt", () => {
|
|
123
|
+
const { config, meta } = parseAgentWithMeta("/x/broken.md", `---
|
|
124
|
+
name: broken
|
|
125
|
+
model: x/y`);
|
|
126
|
+
expect(meta).toBeNull();
|
|
127
|
+
expect(config.name).toBe("broken");
|
|
128
|
+
// 未闭合分支只取 name(原行为),不解析 model
|
|
129
|
+
expect(config.systemPrompt).toBe(`---\nname: broken\nmodel: x/y`);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// AgentRegistry.loadByPath(S2:agentRef = .md 绝对路径)
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
describe("AgentRegistry.loadByPath", () => {
|
|
75
138
|
let ws: string;
|
|
76
139
|
beforeEach(() => { ws = tmpWorkspace(); });
|
|
77
140
|
afterEach(() => { fs.rmSync(ws, { recursive: true, force: true }); });
|
|
78
141
|
|
|
79
|
-
it("
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
reg.
|
|
105
|
-
expect(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const reg =
|
|
114
|
-
reg.
|
|
115
|
-
expect(reg.
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
it("nonexistent directory is silently skipped", () => {
|
|
138
|
-
// workspaceRoot 下无任何 agents 目录 → 空结果,不抛错
|
|
139
|
-
const reg = newRegistry(ws);
|
|
140
|
-
expect(() => reg.discoverAll(emptyBuiltin)).not.toThrow();
|
|
141
|
-
expect(reg.list()).toEqual([]);
|
|
142
|
+
it("按绝对路径加载 agent:frontmatter + body → AgentConfig", () => {
|
|
143
|
+
const file = writeAgent(path.join(ws, ".pi", "agents"), "worker", `---
|
|
144
|
+
name: worker
|
|
145
|
+
description: 通用执行 agent
|
|
146
|
+
tools: [read, bash]
|
|
147
|
+
---
|
|
148
|
+
You are a worker.`);
|
|
149
|
+
const reg = new AgentRegistry();
|
|
150
|
+
const cfg = reg.loadByPath(file);
|
|
151
|
+
expect(cfg?.name).toBe("worker");
|
|
152
|
+
expect(cfg?.systemPrompt).toBe("You are a worker.");
|
|
153
|
+
expect(cfg?.tools).toEqual(["read", "bash"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("无 frontmatter 文件:整个内容作为 systemPrompt", () => {
|
|
157
|
+
const file = writeAgent(ws, "plain", "Just a prompt body.");
|
|
158
|
+
const cfg = new AgentRegistry().loadByPath(file);
|
|
159
|
+
expect(cfg?.name).toBe("plain");
|
|
160
|
+
expect(cfg?.systemPrompt).toBe("Just a prompt body.");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("mtime 缓存:未变文件不重读(config 引用稳定)", () => {
|
|
164
|
+
const file = writeAgent(ws, "cached", "v1");
|
|
165
|
+
const reg = new AgentRegistry();
|
|
166
|
+
const first = reg.loadByPath(file);
|
|
167
|
+
const second = reg.loadByPath(file);
|
|
168
|
+
expect(second).toBe(first);
|
|
169
|
+
// 修改后 mtime 变化 → 新 config
|
|
170
|
+
fs.writeFileSync(file, "v2", "utf-8");
|
|
171
|
+
const third = reg.loadByPath(file);
|
|
172
|
+
expect(third?.systemPrompt).toBe("v2");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("相对路径引用返回 undefined(引用唯一形态 = 绝对路径)", () => {
|
|
176
|
+
const reg = new AgentRegistry();
|
|
177
|
+
expect(reg.loadByPath("worker")).toBeUndefined();
|
|
178
|
+
expect(reg.loadByPath("./agents/worker.md")).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("非 .md 引用返回 undefined", () => {
|
|
182
|
+
const reg = new AgentRegistry();
|
|
183
|
+
expect(reg.loadByPath("/tmp/worker.txt")).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("文件不存在返回 undefined,require=true 抛错带指引", () => {
|
|
187
|
+
const reg = new AgentRegistry();
|
|
188
|
+
expect(reg.loadByPath("/nonexistent/x.md")).toBeUndefined();
|
|
189
|
+
// R5(D7):三态错误文案统一带 <available_subagents> <location> 恢复指引
|
|
190
|
+
expect(() => reg.loadByPath("/nonexistent/x.md", true)).toThrow(/not found or unreadable/);
|
|
191
|
+
expect(() => reg.loadByPath("/nonexistent/x.md", true)).toThrow(/<available_subagents> <location>/);
|
|
192
|
+
expect(() => reg.loadByPath("relative", true)).toThrow(/Invalid agent ref/);
|
|
193
|
+
expect(() => reg.loadByPath("relative", true)).toThrow(/<available_subagents>/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("~/ 前缀展开", () => {
|
|
197
|
+
const reg = new AgentRegistry();
|
|
198
|
+
// homedir 被 mock 为 /nonexistent-home-for-tests → 文件必然不存在,验证展开逻辑
|
|
199
|
+
expect(reg.loadByPath("~/agent.md")).toBeUndefined();
|
|
142
200
|
});
|
|
143
201
|
});
|
|
144
202
|
|
|
145
203
|
// ============================================================
|
|
146
|
-
//
|
|
204
|
+
// 包内 agents/*.md 数据合规(S2:内置 agent = 包内路径文件)
|
|
147
205
|
// ============================================================
|
|
148
206
|
|
|
149
|
-
describe("
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"worker", "general-purpose", "orchestrator",
|
|
158
|
-
"reviewer", "explorer", "researcher",
|
|
159
|
-
"planner", "oracle", "context-builder",
|
|
160
|
-
]));
|
|
161
|
-
// 每个 agent 都有 systemPrompt
|
|
162
|
-
for (const name of names) {
|
|
163
|
-
const cfg = builtin.get(name);
|
|
164
|
-
expect(cfg).toBeDefined();
|
|
165
|
-
expect(cfg?.systemPrompt.length).toBeGreaterThan(0);
|
|
207
|
+
describe("builtin agents 数据合规", () => {
|
|
208
|
+
const AGENTS_DIR = path.resolve(__dirname, "../../../agents");
|
|
209
|
+
const CORE = ["explorer", "worker", "code-reviewer", "oracle", "planner", "researcher", "context-builder", "orchestrator", "general-purpose", "doc-reviewer"];
|
|
210
|
+
|
|
211
|
+
it("agents/*.md 全部 IF1 解析成功", () => {
|
|
212
|
+
for (const f of fs.readdirSync(AGENTS_DIR).filter((x) => x.endsWith(".md"))) {
|
|
213
|
+
const meta = parseResourceMeta(fs.readFileSync(path.join(AGENTS_DIR, f), "utf-8"), "agent");
|
|
214
|
+
expect(meta?.kind, `${f} parse 失败`).toBe("agent");
|
|
166
215
|
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("核心 agent 均含 when/notFor/examples 且正反各一(lintAgentMeta 无 finding)", () => {
|
|
219
|
+
for (const name of CORE) {
|
|
220
|
+
const meta = parseResourceMeta(fs.readFileSync(path.join(AGENTS_DIR, `${name}.md`), "utf-8"), "agent");
|
|
221
|
+
expect(meta, `${name} 解析失败`).not.toBeNull();
|
|
222
|
+
if (meta?.kind !== "agent") continue;
|
|
223
|
+
expect(meta.when, `${name} 缺 when`).toBeDefined();
|
|
224
|
+
expect(meta.notFor, `${name} 缺 notFor`).toBeDefined();
|
|
225
|
+
expect(meta.examples?.length, `${name} 缺 examples`).toBeGreaterThanOrEqual(2);
|
|
226
|
+
expect(meta.examples?.some((e) => e.positive), `${name} 缺正向样本`).toBe(true);
|
|
227
|
+
expect(meta.examples?.some((e) => !e.positive), `${name} 缺反向样本`).toBe(true);
|
|
228
|
+
expect(lintAgentMeta(meta), `${name} examples 不合规`).toEqual([]);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("loadByPath 直接加载包内 agent(内置 = 路径文件,无名字查找)", () => {
|
|
233
|
+
const reg = new AgentRegistry();
|
|
234
|
+
const worker = reg.loadByPath(path.join(AGENTS_DIR, "worker.md"));
|
|
235
|
+
expect(worker?.name).toBe("worker");
|
|
236
|
+
expect(worker?.systemPrompt.length).toBeGreaterThan(0);
|
|
167
237
|
// tools 字段精确匹配:未声明的为 undefined,声明的为具体数组。
|
|
168
238
|
// 改 frontmatter 时这里会立即报错,拦住拼写错误或字段遗漏。
|
|
169
|
-
expect(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
expect(
|
|
239
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "explorer.md"))?.tools).toEqual(
|
|
240
|
+
["read", "bash", "grep", "find", "ls", "structured-output"],
|
|
241
|
+
);
|
|
242
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "researcher.md"))?.tools).toEqual(["read", "bash", "structured-output"]);
|
|
243
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "orchestrator.md"))?.tools).toEqual([
|
|
174
244
|
"todo", "goal_control", "workflow", "subagent", "ask_user", "structured-output",
|
|
175
245
|
]);
|
|
176
|
-
expect(
|
|
177
|
-
expect(
|
|
178
|
-
expect(
|
|
179
|
-
expect(
|
|
246
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "code-reviewer.md"))?.tools).toEqual(["read", "bash", "write", "structured-output"]);
|
|
247
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "planner.md"))?.tools).toEqual(["read", "write", "structured-output"]);
|
|
248
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "oracle.md"))?.tools).toEqual(["read", "write", "structured-output"]);
|
|
249
|
+
expect(reg.loadByPath(path.join(AGENTS_DIR, "context-builder.md"))?.tools).toEqual(["read", "write", "structured-output"]);
|
|
180
250
|
});
|
|
181
251
|
});
|
|
@@ -80,7 +80,6 @@ vi.mock("../session-file-gc.ts", () => ({
|
|
|
80
80
|
vi.mock("../model-config-service.ts", () => ({
|
|
81
81
|
ModelConfigService: class {
|
|
82
82
|
initModel = vi.fn();
|
|
83
|
-
getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
|
|
84
83
|
},
|
|
85
84
|
getModelConfigService: () => null,
|
|
86
85
|
setModelConfigService: vi.fn(),
|