@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -0,0 +1,279 @@
1
+ // src/shared/injection-render.ts
2
+ //
3
+ // 三段 XML 注入渲染(<available_subagents> / <available_workflows> /
4
+ // <available_provider_models>)——从 pi-sw 插件层下沉的平台无关纯函数
5
+ // (convergence D-3)。渲染骨架 escapeXml/renderXmlSection 见同目录
6
+ // xml-injection.ts。pi 侧原函数曾内嵌于三 injector(基线 5557e109b,C5 起改调
7
+ // 本模块,逐字节等价证据见
8
+ // docs/design/subagent-core-convergence.probe-a2-baseline.md)。
9
+ //
10
+ // 与 pi-sw 原实现的差异(本模块定约):
11
+ // - guide 文案必填注入:core 不内嵌任何平台文案,宿主(pi-sw / zsw)各自传入;
12
+ // - ModelEntry 并集口径:除 id/name 外全字段 optional(红线 5)——undefined
13
+ // 消费点显式守卫(input 缺席不抛、contextWindow 缺席不渲染该元素、
14
+ // provider 缺席/空串时 id 裸渲染),不是自然降级;
15
+ // - 分段条目预算:subagents/workflows 可传 maxEntries,超限按排序键码点序
16
+ // 截尾 + 追加宿主注入的兜底指引行(红线 7:内置条目无截断豁免,不做
17
+ // 「内置优先保留」两段式);models 段无预算参数,完整渲染永不截(设计钉死);
18
+ // - format 内部先排后截:pi 调用链数据已排时重排幂等,不破坏「pi 现调用
19
+ // 形态下输出逐字节等价」(CA2 快照验收前提)。
20
+ //
21
+ // 设计权威源:docs/design/subagent-core-convergence.md §3.2 D-3 / §3.3 红线 5、7。
22
+
23
+ import { escapeXml, renderXmlSection } from "./xml-injection.ts";
24
+
25
+ /** 从 agent .md frontmatter 提取的最小 agent 信息(随 D-3 从 pi-sw 下沉) */
26
+ export interface AgentEntry {
27
+ name: string;
28
+ description: string;
29
+ when?: string;
30
+ examples?: Array<{ match: string; action: string; positive: boolean }>;
31
+ /** agentRef:agent .md 文件的绝对路径(注入段 <location>,模型直接引用) */
32
+ path: string;
33
+ }
34
+
35
+ /** 解析后的 workflow 条目(name + 摘要后的 description + 脚本路径) */
36
+ export interface WorkflowEntry {
37
+ name: string;
38
+ description: string;
39
+ /** workflowRef:脚本 .js 文件的绝对路径(注入段 <location>,模型直接引用) */
40
+ path: string;
41
+ }
42
+
43
+ /** reasoning 对象形态(zsw 投影的档位结构);渲染面仅按 truthy 消费,字段值不进输出 */
44
+ export interface ModelReasoningInfo {
45
+ variants?: unknown[];
46
+ defaultVariant?: unknown;
47
+ }
48
+
49
+ /**
50
+ * 注入段的最小模型投影——pi 与 zsw 两侧数据形态的并集(D-3 + 本仓 provider 补充):
51
+ * id/name 为条目最小必填(缺则无渲染意义),其余字段 optional(红线 5)。
52
+ * - pi 投影:provider/reasoning:boolean/input[]/contextWindow 全给;
53
+ * - zsw 投影:reasoning:{variants} 档位对象、input 缺席;
54
+ * - label 与 reasoning.variants 进类型并集但渲染面暂不消费(宿主按需再扩)。
55
+ */
56
+ export interface ModelEntry {
57
+ id: string;
58
+ name: string;
59
+ provider?: string;
60
+ label?: string;
61
+ contextWindow?: number;
62
+ reasoning?: boolean | ModelReasoningInfo;
63
+ input?: string[];
64
+ }
65
+
66
+ /** 条目段(subagents/workflows)渲染选项:guide 宿主注入 + 可选条目预算 */
67
+ export interface ListFormatOptions {
68
+ /** 注入段引导文案——必填,core 不内嵌平台文案(D-3 guide 参数化) */
69
+ guide: string;
70
+ /**
71
+ * 条目预算上限:超限按排序键码点序截尾(保留靠前条目)。缺省不限制
72
+ * (pi 现行为全量)。models 段无此参数(永不截,设计钉死)。
73
+ */
74
+ maxEntries?: number;
75
+ /** 截断发生时在段末追加的兜底指引行(宿主注入,如查看全量的命令);未提供则不追加 */
76
+ truncationNotice?: string;
77
+ }
78
+
79
+ /** models 段渲染选项:仅 guide——无条目预算(完整渲染永不截,D-3 钉死) */
80
+ export interface ModelListFormatOptions {
81
+ guide: string;
82
+ }
83
+
84
+ /**
85
+ * 码点序排序(显式契约,禁 localeCompare——宿主 locale 差异会破坏跨环境
86
+ * 字节一致;注入段进每 turn system prompt,顺序必须与枚举序解耦)。
87
+ * 非变异:返回排序副本,入参数组不动(core 纯函数定位;截尾语义依赖此序)。
88
+ */
89
+ export function sortByCodepoint<T>(
90
+ items: readonly T[],
91
+ key: (item: T) => string,
92
+ ): T[] {
93
+ return [...items].sort((a, b) => {
94
+ const ka = key(a);
95
+ const kb = key(b);
96
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
97
+ });
98
+ }
99
+
100
+ /** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
101
+ const MAX_DESC_LEN = 160;
102
+
103
+ /** 断句阈值比例:句末标点位置须 >= maxLen 的 40% 才采用,否则硬截断保留更多信息 */
104
+ const DESC_BOUNDARY_MIN_RATIO = 0.4;
105
+
106
+ /**
107
+ * 将 workflow description 截断为 prompt 友好的摘要。
108
+ * 优先在 limit 内的最后一个句末标点处断句;无合适断点则硬截断 + 省略号。
109
+ */
110
+ export function summarizeDescription(
111
+ desc: string,
112
+ maxLen = MAX_DESC_LEN,
113
+ ): string {
114
+ const trimmed = desc.trim();
115
+ if (trimmed.length <= maxLen) return trimmed;
116
+ const slice = trimmed.slice(0, maxLen);
117
+ const boundary = Math.max(
118
+ slice.lastIndexOf("。"),
119
+ slice.lastIndexOf(";"),
120
+ slice.lastIndexOf(";"),
121
+ slice.lastIndexOf(". "),
122
+ );
123
+ // 断点过靠前(< 阈值比例)时不采用,改硬截断保留更多信息
124
+ if (boundary > maxLen * DESC_BOUNDARY_MIN_RATIO) return slice.slice(0, boundary + 1);
125
+ return `${slice}…`;
126
+ }
127
+
128
+ /**
129
+ * 排序后应用条目预算:超限截尾(保留码点序靠前条目)。返回保留条目与是否截断。
130
+ * 红线 7:内置条目无截断豁免——不做「内置优先保留」两段式。
131
+ */
132
+ function applyEntryBudget<T>(
133
+ sorted: T[],
134
+ maxEntries: number | undefined,
135
+ ): { kept: T[]; truncated: boolean } {
136
+ if (maxEntries === undefined || sorted.length <= maxEntries) {
137
+ return { kept: sorted, truncated: false };
138
+ }
139
+ return { kept: sorted.slice(0, maxEntries), truncated: true };
140
+ }
141
+
142
+ /**
143
+ * 将 agent 列表格式化为 XML 注入段。
144
+ *
145
+ * 内部先按 name 码点序排序再渲染截断(超预算截尾语义依赖码点序;pi 调用链
146
+ * 数据已排时重排幂等,不破坏与 pi 现输出的逐字节等价)。空列表返回空串
147
+ * (不注入);预算截断发生时在段末追加宿主注入的兜底指引行(缺省不追加)。
148
+ */
149
+ export function formatAgentList(
150
+ agents: AgentEntry[],
151
+ opts: ListFormatOptions,
152
+ ): string {
153
+ if (agents.length === 0) return "";
154
+
155
+ const sorted = sortByCodepoint(agents, (a) => a.name);
156
+ const { kept, truncated } = applyEntryBudget(sorted, opts.maxEntries);
157
+
158
+ const items = kept.map((agent) => {
159
+ let block = ` <agent><name>${escapeXml(agent.name)}</name><description>${escapeXml(agent.description)}</description>`;
160
+ // 路由样本(when + examples 正反原样渲染——negative 的 action 由作者写
161
+ // 「不调用(原因)」,渲染器不硬编码;全部内容 escapeXml 防 XML 注入段破坏)
162
+ if (agent.when) {
163
+ block += `<when>${escapeXml(agent.when)}</when>`;
164
+ }
165
+ if (agent.examples && agent.examples.length > 0) {
166
+ const exampleLines = agent.examples.map(
167
+ (e) => ` - "${escapeXml(e.match)}" → ${escapeXml(e.action)}`,
168
+ );
169
+ block += `\n <examples>\n${exampleLines.join("\n")}\n </examples>`;
170
+ }
171
+ block += `<location>${escapeXml(agent.path)}</location></agent>`;
172
+ return block;
173
+ });
174
+ if (truncated && opts.truncationNotice !== undefined) {
175
+ items.push(opts.truncationNotice);
176
+ }
177
+ return renderXmlSection({
178
+ tag: "available_subagents",
179
+ guide: opts.guide,
180
+ items,
181
+ });
182
+ }
183
+
184
+ /**
185
+ * 将 workflow 列表格式化为 XML 注入段。
186
+ *
187
+ * 与 formatAgentList 同约:先按 name 码点序排序再渲染截尾;空列表返回空串
188
+ * (不注入);截断时追加宿主注入的兜底指引行(缺省不追加)。
189
+ */
190
+ export function formatWorkflowList(
191
+ workflows: WorkflowEntry[],
192
+ opts: ListFormatOptions,
193
+ ): string {
194
+ if (workflows.length === 0) return "";
195
+
196
+ const sorted = sortByCodepoint(workflows, (w) => w.name);
197
+ const { kept, truncated } = applyEntryBudget(sorted, opts.maxEntries);
198
+
199
+ const items = kept.map((wf) =>
200
+ ` <workflow><name>${escapeXml(wf.name)}</name><description>${escapeXml(wf.description)}</description><location>${escapeXml(wf.path)}</location></workflow>`,
201
+ );
202
+ if (truncated && opts.truncationNotice !== undefined) {
203
+ items.push(opts.truncationNotice);
204
+ }
205
+ return renderXmlSection({
206
+ tag: "available_workflows",
207
+ guide: opts.guide,
208
+ items,
209
+ });
210
+ }
211
+
212
+ /** 码点序比较(显式契约,禁 localeCompare——同 sortByCodepoint 注释) */
213
+ function compareByCodepoint(a: string, b: string): number {
214
+ return a < b ? -1 : a > b ? 1 : 0;
215
+ }
216
+
217
+ /**
218
+ * models 段排序:provider 归一为非空串参与比较(缺席/空串 → "")——
219
+ * pi 投影(provider 全给且非空)下与 pi 现实现 (provider, id) 两段码点序一致;
220
+ * zsw 投影(provider 缺席)整体退化为 id 码点序;混合形态下无 provider 条目
221
+ * 排最前(空串码点最小),口径确定可复现。
222
+ */
223
+ function compareModelEntries(a: ModelEntry, b: ModelEntry): number {
224
+ const pa = a.provider ?? "";
225
+ const pb = b.provider ?? "";
226
+ return pa === pb
227
+ ? compareByCodepoint(a.id, b.id)
228
+ : compareByCodepoint(pa, pb);
229
+ }
230
+
231
+ /**
232
+ * 能力标记:reasoning truthy(布尔 true 或对象形态——zsw 的 {variants} 投影)
233
+ * → "reasoning";input 含 image → "vision"。空则省略 caps 段。
234
+ * 红线 5 守卫:input 缺席经 optional chaining 不抛(pi 版此处对 undefined 抛
235
+ * TypeError,本仓 L73 已核实)。
236
+ */
237
+ function formatCaps(entry: ModelEntry): string {
238
+ const caps: string[] = [];
239
+ if (entry.reasoning) caps.push("reasoning");
240
+ if (entry.input?.includes("image") ?? false) caps.push("vision");
241
+ return caps.join(",");
242
+ }
243
+
244
+ /**
245
+ * 将模型列表格式化为 XML 注入段。
246
+ *
247
+ * 输入按归一 (provider, id) 码点序排序(见 compareModelEntries)——同一数据集
248
+ * 输出字节稳定(KV-cache 契约)。空列表返回空串(不注入)。
249
+ * models 段无条目预算(完整渲染永不截,D-3 钉死)。
250
+ * 红线 5 守卫:contextWindow 缺席不渲染该元素(不输出 "undefined" 垃圾);
251
+ * provider 缺席/空串时 id 裸渲染。
252
+ */
253
+ export function formatModelList(
254
+ models: ModelEntry[],
255
+ opts: ModelListFormatOptions,
256
+ ): string {
257
+ if (models.length === 0) return "";
258
+
259
+ const sorted = [...models].sort(compareModelEntries);
260
+
261
+ const items = sorted.map((m) => {
262
+ const caps = formatCaps(m);
263
+ const idText = m.provider ? `${m.provider}/${m.id}` : m.id;
264
+ return (
265
+ ` <model><id>${escapeXml(idText)}</id>`
266
+ + `<name>${escapeXml(m.name)}</name>`
267
+ + (caps ? `<caps>${caps}</caps>` : "")
268
+ + (m.contextWindow !== undefined
269
+ ? `<contextWindow>${m.contextWindow}</contextWindow>`
270
+ : "")
271
+ + `</model>`
272
+ );
273
+ });
274
+ return renderXmlSection({
275
+ tag: "available_provider_models",
276
+ guide: opts.guide,
277
+ items,
278
+ });
279
+ }
@@ -88,7 +88,11 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
88
88
 
89
89
  if (kind === "workflow") {
90
90
  // minor-2:agent 专属字段不可出现在 workflow(串类 reject)
91
- if (o.examples !== undefined || o.tools !== undefined || o.model !== undefined || o.engine !== undefined) {
91
+ if (
92
+ o.examples !== undefined || o.tools !== undefined || o.model !== undefined
93
+ || o.engine !== undefined || o.maxTurns !== undefined
94
+ || o.disallowedTools !== undefined || o.skills !== undefined
95
+ ) {
92
96
  return null;
93
97
  }
94
98
  // phases 必填数组,元素为 string | {title:string, detail?:string}
@@ -159,6 +163,18 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
159
163
  }
160
164
  const model = isString(o.model) ? o.model : undefined;
161
165
  const engine = isString(o.engine) ? o.engine : undefined;
166
+ // D3 可选执行字段(maxTurns/disallowedTools/skills):与 tools 同风格投影
167
+ // (空数组 [] 同 tools 保留键——列表字段族空列表语义一致)。严格层对非法类型
168
+ // reject(minor-2..5 既有精神:不静默丢弃非法字段)——宽容降级是
169
+ // parseAgentProfile(agent-registry)的职责,不在本严格层。
170
+ if (o.maxTurns !== undefined && (typeof o.maxTurns !== "number" || !Number.isFinite(o.maxTurns))) {
171
+ return null;
172
+ }
173
+ const maxTurns = typeof o.maxTurns === "number" ? o.maxTurns : undefined;
174
+ const disallowedTools = parseStringListField(o.disallowedTools);
175
+ if (disallowedTools === null) return null;
176
+ const skills = parseStringListField(o.skills);
177
+ if (skills === null) return null;
162
178
 
163
179
  const meta: AgentMeta = {
164
180
  kind: "agent",
@@ -168,12 +184,36 @@ function typecheckMeta(raw: unknown, kind: ResourceKind): ResourceMeta | null {
168
184
  ...(tools !== undefined ? { tools } : {}),
169
185
  ...(model !== undefined ? { model } : {}),
170
186
  ...(engine !== undefined ? { engine } : {}),
187
+ ...(maxTurns !== undefined ? { maxTurns } : {}),
188
+ ...(disallowedTools !== undefined ? { disallowedTools } : {}),
189
+ ...(skills !== undefined ? { skills } : {}),
171
190
  ...(when !== undefined ? { when } : {}),
172
191
  ...(notFor !== undefined ? { notFor } : {}),
173
192
  };
174
193
  return meta;
175
194
  }
176
195
 
196
+ /**
197
+ * 字符串列表字段解析(tools 先例的泛化):数组形态逐元素校验 string,空数组保留
198
+ * 为 [](键进 meta——与 tools 数组形态 `tools: []` 语义一致,列表字段族同语义);
199
+ * 逗号分隔字符串按 `tools: read, bash` 约定拆分 trim,全空串(`skills: ,`)视同
200
+ * 缺席;字段缺席 → undefined(不进 meta);非法形态(数字/对象/数组含非字符串)
201
+ * → null(调用方 reject——minor-2..5 既有精神:非法字段显式拒绝,不静默丢弃)。
202
+ * 三态返回是为区分「缺席」与「非法」。
203
+ */
204
+ function parseStringListField(raw: unknown): string[] | null | undefined {
205
+ if (raw === undefined) return undefined;
206
+ if (Array.isArray(raw)) {
207
+ if (!raw.every(isString)) return null;
208
+ return raw as string[];
209
+ }
210
+ if (isString(raw)) {
211
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
212
+ return parts.length > 0 ? parts : undefined;
213
+ }
214
+ return null;
215
+ }
216
+
177
217
  // ── IF1: parseResourceMeta(discovery,fail-safe null)─────────────────
178
218
 
179
219
  /**
@@ -10,10 +10,10 @@
10
10
  // 4. manifest 路径存在性校验:声明的路径不存在 → 该包发现失败,不 fallback
11
11
  // 5. 废弃 discovery.json:扫描路径完全由代码内推导,无外部依赖
12
12
  //
13
- // 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev → project .pi → project .pi/.tmp(仅workflow) → project .agents
13
+ // 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev → ext-paths(XYZ_EXTENSION_PATHS/dev-link) → project .pi → project .pi/.tmp(仅workflow) → project-host(宿主注入槽) → project .agents
14
14
 
15
15
  import * as fsSync from "node:fs";
16
- import { access, readdir, readFile, stat } from "node:fs/promises";
16
+ import { access, readdir, readFile, realpath, stat } from "node:fs/promises";
17
17
  import { homedir } from "node:os";
18
18
  import { delimiter, join, resolve } from "node:path";
19
19
 
@@ -56,7 +56,19 @@ export interface DiscoveredResource {
56
56
  }
57
57
 
58
58
  /** 资源来源层级 */
59
- export type ResourceSource = "user-pi" | "user-agents" | "npm" | "npm-dev" | "user-extension-paths" | "project-pi" | "project-pi-tmp" | "project-agents";
59
+ export type ResourceSource =
60
+ | "user-pi"
61
+ | "user-agents"
62
+ | "npm"
63
+ | "npm-dev"
64
+ | "user-extension-paths"
65
+ | "project-pi"
66
+ | "project-pi-tmp"
67
+ // project-host(W2②):宿主注入的项目级根(如 zsw 的 <ws>/.zcode/agents)。
68
+ // 序位刻意压在 project-agents 之下——zsw 现语义项目 .agents > .zcode,
69
+ // project-host 承接 zcode 项目根,project-agents 仍是项目级最高逃生门。
70
+ | "project-host"
71
+ | "project-agents";
60
72
 
61
73
  /** 扫描配置 */
62
74
  export interface ScanConfig {
@@ -65,8 +77,14 @@ export interface ScanConfig {
65
77
  /** 项目根目录(findWorkspaceRoot 推导结果) */
66
78
  workspaceRoot: string;
67
79
  /** 宿主注入的发现根(DiscoveryRoot.dir 已含 kind 末级目录与安装布局,
68
- * source 为宿主语义标签——pi 壳 = user-pi/npm/npm-dev 三根)。buildScanTargets
69
- * 按标签查表填充对应槽位,宿主未提供某标签根时该槽位条目整体缺席。 */
80
+ * source 为宿主语义标签——pi 壳 = user-pi/npm/npm-dev 三根,zsw 壳可另注入
81
+ * project-host 等)。buildScanTargets 按标签填充对应槽位,宿主未提供某标签
82
+ * 根时该槽位条目整体缺席。
83
+ * 同标签多根语义(W2④):同标签多条目依注入序全部保留、同序位依次扫描——
84
+ * 宿主(zsw)把「目录 symlink 展开目标 + 本体根」按注入序注入同标签,core
85
+ * 合并 last-writer-wins 下靠后者胜,本体根必须注入在展开目标之后(本体胜,
86
+ * 红线 2)。user-agents/project-agents 硬编码槽与同标签注入合并时硬编码根
87
+ * 自动后置(同为「本体在后」语义)。 */
70
88
  hostRoots: DiscoveryRoot[];
71
89
  /** 是否包含 tmp 源(仅 workflow 用 .pi/workflows/.tmp/) */
72
90
  includeTmp?: boolean;
@@ -82,11 +100,12 @@ const MACHINE_SOURCES: ReadonlySet<ResourceSource> = new Set<ResourceSource>([
82
100
  "user-extension-paths",
83
101
  "project-pi",
84
102
  "project-pi-tmp",
103
+ "project-host",
85
104
  "project-agents",
86
105
  ]);
87
106
 
88
107
  /** 判断 source 是否属于机器源(安装拓扑常态,同名重复降 debug)。
89
- * 导出仅为测试穷举断言用(封闭 8 值枚举 × 分级边界)。 */
108
+ * 导出仅为测试穷举断言用(封闭 9 值枚举 × 分级边界)。 */
90
109
  export function isMachineSource(source: ResourceSource): boolean {
91
110
  return MACHINE_SOURCES.has(source);
92
111
  }
@@ -518,38 +537,44 @@ function readExtensionPaths(): string[] {
518
537
  * 构建所有扫描源(按优先级低→高排列)。
519
538
  *
520
539
  * agent 和 workflow 共享相同的前缀体系。宿主注入根(config.hostRoots)按 source
521
- * 标签查表填充三个槽位条目——标签字面是 core 编排的槽位键(同时是 ResourceSource
540
+ * 标签填充槽位条目——标签字面是 core 编排的槽位键(同时是 ResourceSource
522
541
  * 枚举成员,报告输出与历史值逐字一致);宿主未提供某标签根时该条目跳过(该源在
523
542
  * 遮蔽序中自然缺席),其余条目(core 自建:homedir/env/workspaceRoot 推导)留原位
524
543
  * 原序,遮蔽优先级语义与注入化前逐字一致。
544
+ *
545
+ * 同标签多根(W2④,Map→列表):同标签多条目依注入序全部保留、同序位依次扫描
546
+ * (旧实现 Map(source→dir) 同标签靠后者整体覆盖,多根语义缺失)。硬编码槽
547
+ * (user-agents/project-agents)与 hostRoots 同标签注入合并——硬编码根(本体根)
548
+ * 排在注入条目之后:core 合并 last-writer-wins 靠后者胜,本体在后 = 本体胜(红线 2
549
+ * 「注入/合并序语义」)。pi 单条目形态(每标签恰好一条或无注入)下 targets 序与
550
+ * 旧实现逐项一致(回归红线,见 __tests__ pi 单条目形态快照用例)。
525
551
  */
526
552
  function buildScanTargets(config: ScanConfig): ScanTarget[] {
527
553
  const { kind, workspaceRoot, hostRoots, includeTmp } = config;
528
554
  const home = homedir();
529
555
 
530
- // 同标签多条目时取列表靠后者(hostRoots 按优先级低→高,靠后 = 宿主侧更高优先级)
531
- const hostDirBySource = new Map<string, string>(
532
- hostRoots.map((root): [string, string] => [root.source, root.dir]),
533
- );
556
+ // 同标签多条目依注入序全部保留(W2④ 列表语义):宿主把「展开目标 + 本体根」
557
+ // 按注入序注入同标签,core 同序位依次扫描;本体靠后 last-writer-wins 本体胜。
558
+ const hostDirsBySource = new Map<string, string[]>();
559
+ for (const root of hostRoots) {
560
+ const list = hostDirsBySource.get(root.source);
561
+ if (list) list.push(root.dir);
562
+ else hostDirsBySource.set(root.source, [root.dir]);
563
+ }
564
+ const hostTargets = (source: ResourceSource): ScanTarget[] =>
565
+ (hostDirsBySource.get(source) ?? []).map((dir) => ({ dir, source, enabled: true }));
534
566
 
535
567
  const targets: ScanTarget[] = [];
536
568
  // 1. user .pi/agent/{kind}/(宿主注入,pi 壳 source "user-pi")
537
- const userPiDir = hostDirBySource.get("user-pi");
538
- if (userPiDir !== undefined) {
539
- targets.push({ dir: userPiDir, source: "user-pi", enabled: true });
540
- }
541
- // 2. user .agents/{kind}/(core 自建:homedir 推导,非宿主依赖)
569
+ targets.push(...hostTargets("user-pi"));
570
+ // 2. user .agents/{kind}/(硬编码根 = homedir 推导 + 宿主可选注入合并,硬编码
571
+ // 本体根后置 注入的展开目标在前、本体在后,last-writer-wins 本体胜)
572
+ targets.push(...hostTargets("user-agents"));
542
573
  targets.push({ dir: join(home, ".agents", kind), source: "user-agents", enabled: true });
543
574
  // 3. npm global: <agentDir>/npm/node_modules/*/<pkg>/(宿主注入,pi 壳 source "npm")
544
- const npmDir = hostDirBySource.get("npm");
545
- if (npmDir !== undefined) {
546
- targets.push({ dir: npmDir, source: "npm", enabled: true });
547
- }
575
+ targets.push(...hostTargets("npm"));
548
576
  // 4. npm dev symlink: <agentDir>/extensions/*/<pkg>/(宿主注入,pi 壳 source "npm-dev")
549
- const npmDevDir = hostDirBySource.get("npm-dev");
550
- if (npmDevDir !== undefined) {
551
- targets.push({ dir: npmDevDir, source: "npm-dev", enabled: true });
552
- }
577
+ targets.push(...hostTargets("npm-dev"));
553
578
  // user extension paths (XYZ_EXTENSION_PATHS, dev-link): each path is a package dir,
554
579
  // 走 processPackage 读 pi.{kind} manifest 或扫 {kind}/ 目录。优先级高于 npm/npm-dev
555
580
  // (dev-link 是开发版 override),低于 project(项目正式资源优先)。
@@ -568,7 +593,13 @@ function buildScanTargets(config: ScanConfig): ScanTarget[] {
568
593
  });
569
594
  }
570
595
 
571
- // 7. project .agents/{kind}/
596
+ // 6.5 project host 根(宿主注入,W2②):承接宿主自有项目级布局(如 zsw 的
597
+ // <ws>/.zcode/agents)。序位在 project-agents 之下(.agents 是项目级最高
598
+ // 逃生门);未注入该标签时槽位缺席(与 user-pi/npm/npm-dev 同语义)。
599
+ targets.push(...hostTargets("project-host"));
600
+
601
+ // 7. project .agents/{kind}/(硬编码根 + 宿主可选注入合并,本体根后置同槽 2)
602
+ targets.push(...hostTargets("project-agents"));
572
603
  targets.push({
573
604
  dir: join(workspaceRoot, ".agents", kind),
574
605
  source: "project-agents",
@@ -586,6 +617,12 @@ function buildScanTargets(config: ScanConfig): ScanTarget[] {
586
617
  * 按优先级低→高扫描所有源,同名资源靠后覆盖靠前(last-writer-wins)。
587
618
  * npm/dev 包内:有 manifest 只走 manifest(路径不存在则失败),无 manifest 扫约定目录。
588
619
  *
620
+ * realpath 归一去重(W2①,仅 async 链):多个不同名 symlink 指向同一物理文件时
621
+ * (多链同文件),stem 去重防不住——清单按物理文件归一只留一条(首遇者,位置
622
+ * 固定语义与 stem 合并一致);同 stem 不同物理文件的遮蔽语义不受影响(仍按
623
+ * last-writer-wins 覆盖)。realpath 解析失败(扫描后竞态删除/ELOOP 深链)回退
624
+ * 原 path,属预期失败不抛。
625
+ *
589
626
  * Throws on unrecoverable scan errors——未捕获异常向上抛(Promise.all 首个 reject
590
627
  * 即整体拒绝,与串行版 discoverResourcesSync 的传播语义一致,见实现内 [perf] 注释)。
591
628
  * 预期失败不抛:目录不存在/不可读返回空列表,manifest 声明路径缺失以 available=false 返回。
@@ -624,13 +661,35 @@ export async function discoverResources(config: ScanConfig): Promise<DiscoveredR
624
661
  }),
625
662
  );
626
663
 
664
+ // [W2①] realpath 归一键(仅 async 链):与 allBySource 同构的二维数组(源序×
665
+ // 源内资源序)。源级并行解析(每资源一次 realpath,资源量级为个位/十位数);
666
+ // 失败回退原 path——扫描与 realpath 之间竞态删除/ELOOP 属预期失败,不抛
667
+ // (对齐 scanDirectory 的 stat().catch 吞错面)。
668
+ const realpathKeys: string[][] = await Promise.all(
669
+ allBySource.map(async ({ resources }) =>
670
+ Promise.all(resources.map((r) => realpath(r.path).catch(() => r.path))),
671
+ ),
672
+ );
673
+
627
674
  // 按优先级合并:targets 数组顺序即优先级(低→高),高优先级后写覆盖
628
675
  // 用文件名 stem 作为去重 key(与旧逻辑一致:同名资源高优先级覆盖)
629
676
  const merged = new Map<string, DiscoveredResource>();
630
-
631
- for (const { resources } of allBySource) {
632
- for (const r of resources) {
677
+ // realpath → 已入清单 stem key:多链同文件(不同名 symlink 指向同一物理文件)
678
+ // 时后到链 skip——清单按物理文件只留一条,不计入遮蔽(同一文件无遮蔽语义)。
679
+ const realpathOwner = new Map<string, string>();
680
+
681
+ for (let si = 0; si < allBySource.length; si++) {
682
+ const { resources } = allBySource[si]!;
683
+ for (let ri = 0; ri < resources.length; ri++) {
684
+ const r = resources[ri]!;
633
685
  const key = stem(r.path);
686
+ const rp = realpathKeys[si]![ri]!;
687
+ // 同物理文件已以另一 stem 入清单 → 多链重复,跳过(不影响 stem 遮蔽语义:
688
+ // 同 stem 不同物理文件仍走下方 last-writer-wins)
689
+ const owner = realpathOwner.get(rp);
690
+ if (owner !== undefined && owner !== key) {
691
+ continue;
692
+ }
634
693
  const existing = merged.get(key);
635
694
  // available=false 的占位不覆盖已有的 available=true
636
695
  if (!r.available && existing) {
@@ -661,6 +720,7 @@ export async function discoverResources(config: ScanConfig): Promise<DiscoveredR
661
720
  }
662
721
  }
663
722
  merged.set(key, r);
723
+ realpathOwner.set(rp, key);
664
724
  }
665
725
  }
666
726
 
@@ -668,10 +728,12 @@ export async function discoverResources(config: ScanConfig): Promise<DiscoveredR
668
728
  }
669
729
 
670
730
  /**
671
- * 同步版:扫描单个目录下的资源文件路径(供 agent-registry 的 mtime 缓存模式使用)。
731
+ * 同步版:扫描单个目录下的资源文件路径。
672
732
  *
673
- * agent .md 发现需要 mtime 缓存(hot-reload),不能走 async 全量扫描。
674
- * 此函数提供目录级同步扫描,npm/dev 包内发现仍需 async(agent-registry builtin 兜底)。
733
+ * 消费关系(W2⑤ 漂移修正):当前无非测试调用方——agent-registry import
734
+ * getCachedFile(mtime 缓存),不消费 sync 扫描。保留作 async scanDirectory
735
+ * 对称 API / 测试用;生产 agent/workflow 发现统一走 async discoverResources
736
+ * (红线 4:修对生产面)。npm/dev 包内发现仍需 async(scanNpmDir)。
675
737
  */
676
738
  export function scanDirectorySync(dirPath: string, kind: ResourceKind): string[] {
677
739
  try {
@@ -697,7 +759,8 @@ export function scanDirectorySync(dirPath: string, kind: ResourceKind): string[]
697
759
 
698
760
  /**
699
761
  * 同步版:读取 package.json 的 pi.{kind} manifest。
700
- * agent-registry 同步路径使用。与 async 版共享 manifestCache(双读者同一 Map)。
762
+ * 当前无非测试调用方(W2⑤ 漂移修正,保留作对称 API/测试用)。与 async
763
+ * 共享 manifestCache(双读者同一 Map)。
701
764
  */
702
765
  export function readPackageManifestSync(pkgDir: string, kind: ResourceKind): string[] | undefined {
703
766
  const pkgJsonPath = resolve(pkgDir, "package.json");
@@ -725,7 +788,7 @@ export function readPackageManifestSync(pkgDir: string, kind: ResourceKind): str
725
788
 
726
789
  /**
727
790
  * 同步版:处理单个 npm/dev 包。
728
- * agent-registry 同步路径使用。
791
+ * 当前无非测试调用方(W2⑤ 漂移修正,保留作对称 API/测试用)。
729
792
  */
730
793
  export function processPackageSync(pkgDir: string, kind: ResourceKind): DiscoveredResource[] {
731
794
  const manifestPaths = readPackageManifestSync(pkgDir, kind);
@@ -767,7 +830,7 @@ export function processPackageSync(pkgDir: string, kind: ResourceKind): Discover
767
830
 
768
831
  /**
769
832
  * 同步版:扫描 npm node_modules 目录。
770
- * agent-registry 同步路径使用。
833
+ * 当前无非测试调用方(W2⑤ 漂移修正,保留作对称 API/测试用)。
771
834
  */
772
835
  export function scanNpmDirSync(nodeModulesDir: string, kind: ResourceKind): DiscoveredResource[] {
773
836
  let entries: string[];
@@ -800,10 +863,14 @@ export function scanNpmDirSync(nodeModulesDir: string, kind: ResourceKind): Disc
800
863
  }
801
864
 
802
865
  /**
803
- * 同步版:发现所有资源(agent-registry 专用,支持 mtime 缓存的 hot-reload)。
866
+ * 同步版:发现所有资源。
867
+ *
868
+ * 当前无非测试调用方(W2⑤ 漂移修正:agent-registry 只 import getCachedFile,
869
+ * 不消费本族;保留作 async discoverResources 的对照实现/测试用——两版输出等价
870
+ * 由既有快照用例锁定)。
804
871
  *
805
- * 与 discoverResources 对应的同步实现,扫描相同的源。
806
- * 返回所有源的资源(未去重,调用方按需处理优先级)。
872
+ * 与 discoverResources 对应的同步实现,扫描相同的源,同 stem last-writer-wins
873
+ * 合并(不含 async 链的 realpath 去重——sync 链无非测试调用方,勿扩面,红线 4)。
807
874
  */
808
875
  export function discoverResourcesSync(config: ScanConfig): DiscoveredResource[] {
809
876
  const targets = buildScanTargets(config);
@@ -59,6 +59,20 @@ export interface AgentMeta extends ResourceMetaBase {
59
59
  * 与 model 字段同风格——路由字段(不进 system prompt),执行侧(P4 路由层)消费。
60
60
  */
61
61
  engine?: string;
62
+ /**
63
+ * 执行预算:turn 上限(sink 设计 D3 新增可选执行字段之一)。
64
+ * 消费优先级:显式参数 > 本字段 > 缺省——pi 工具参数 maxTurns 语义不变(本字段
65
+ * 对 pi 是清单元数据),zsw 决策链 timeoutMs > profile.maxTurns > 缺省。
66
+ */
67
+ maxTurns?: number;
68
+ /** tool denylist(D3 可选执行字段):spawn 侧剔除,与 tools allowlist 正交。 */
69
+ disallowedTools?: string[];
70
+ /**
71
+ * agent 声明依赖的 skill 名清单(D3 可选执行字段)。注意与本文件头注
72
+ * 「skills 归 pi core」的 skills(资源种类)不同物:此处是 agent frontmatter 的
73
+ * 执行配置字段,不是 ResourceKind。
74
+ */
75
+ skills?: string[];
62
76
  }
63
77
 
64
78
  /** 判别联合(kind 判别)。 */
@@ -9,12 +9,12 @@
9
9
  * XML 结构——全部字段过一遍转义防注入段破碎)。
10
10
  */
11
11
  export function escapeXml(str: string): string {
12
- return str
13
- .replace(/&/g, "&amp;")
14
- .replace(/</g, "&lt;")
15
- .replace(/>/g, "&gt;")
16
- .replace(/"/g, "&quot;")
17
- .replace(/'/g, "&apos;");
12
+ return str
13
+ .replace(/&/g, "&amp;")
14
+ .replace(/</g, "&lt;")
15
+ .replace(/>/g, "&gt;")
16
+ .replace(/"/g, "&quot;")
17
+ .replace(/'/g, "&apos;");
18
18
  }
19
19
 
20
20
  /**
@@ -29,7 +29,7 @@ export function renderXmlSection(section: {
29
29
  guide: string;
30
30
  items: string[];
31
31
  }): string {
32
- if (section.items.length === 0) return "";
33
- const lines = [`\n\n<${section.tag}>`, section.guide, ...section.items, `</${section.tag}>`];
34
- return lines.join("\n");
32
+ if (section.items.length === 0) return "";
33
+ const lines = [`\n\n<${section.tag}>`, section.guide, ...section.items, `</${section.tag}>`];
34
+ return lines.join("\n");
35
35
  }