@nathan33/nao-skill 0.18.4 → 0.18.5

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.
@@ -40,6 +40,7 @@ description: pi-intercom 多会话协作协议(常驻引用)
40
40
  ## 会话内子代理(授权与回执归属)
41
41
 
42
42
  - **授权**:角色可在自身 turn 内用 pi-subagents 起**会话内子代理**(scout / researcher / oracle / reviewer / evidence-auditor 等)。默认**只读**;需要写入时须 `worktree: true` 隔离并指定**单一集成者**(父角色),同一 cwd 禁多写。子代理既**不属**舰队会话身份模型 —— 边界与 `status`/`close` 处置**见上文「子代理会话不属舰队会话身份模型」**,此处不重复。
43
+ - **角色口径同源(指针)**:常驻舰队会话 = `nao-fleet.sh` 以 `--append-system-prompt` 注入角色卡;会话内子代理 = 包内扩展(`.agents/extensions/nao-role-agents.mjs`)在 `session_start` 运行时注册 `nao-<role-id>`。两路**同卡同锚**(同一批 `.agents/prompts/*.md` + 同一环境锚点行);细节见该扩展与 `tests/t4/cases/17-role-agents.sh`,此处不复制。
43
44
  - **回执归属(硬性)**:子代理的 run 结果**只是证据,不等于回执**;`[编号] done(lite|full)` **只能由父角色**在自行核对后经 intercom `send` 发出(PM 只认父角色 done)。父角色不得把子代理输出直接转贴为回执。
44
45
  - **子代理禁令**:子代理**不得以舰队角色名义回执**、**不得发送 `done` 形态消息**、**不得直发 PM**;需决策/阻塞/授权走 `contact_supervisor`(进程内 native 通道,无需 pi-intercom;父侧对应用 `subagent_supervisor`)。
45
46
  - **证据格式**:子代理产物须含**命令 + exit code + 例数/文件数(或断言数)+ 路径**,供父角色写入回执「测试」栏;无证据的「完成」不算证据。注入文本见 `@.agents/templates/pi-subagents/intercom-bridge.md`(安装说明见同目录 `README.md`)。
@@ -0,0 +1,207 @@
1
+ // .agents/extensions/nao-role-agents.mjs
2
+ // T527 —— 角色→pi-subagents agent 的单一事实(运行时注册扩展 · E-lite)
3
+ //
4
+ // 契约点(外部公开面,升版漂移由 tests/t4/cases/17 探针 fail-loud):
5
+ // 事件名 pi-subagents:runtime-agent-register:v1(docs/extension-api.md §Runtime agent registration)
6
+ //
7
+ // BR1 零副本 :运行时读 .agents/prompts/*.md + .agents/roles.yaml;无生成物、无静态副本。
8
+ // BR2 同卡同锚 :systemPrompt = 卡正文(去 frontmatter)逐字节 + 环境锚点行(与
9
+ // `.agents/scripts/nao-fleet.sh` build_system_prompt 的 printf 段逐字一致;
10
+ // 唯一双写点,由 cases/17 cross-assert 兜住)。
11
+ // BR3 命名纪律 :nao-<role-id>(6 个)· 不设 alias · 不占 subagent- 前缀。
12
+ // BR4 上下文口径:systemPromptMode=replace + inheritProjectContext=true(否则卡内 AGENTS.md 契约静默丢失)。
13
+ // BR5 韧性 :注册/装配失败仅 warn、绝不 throw;session_start 先 dispose 再注册(幂等)。
14
+ // BR6 逃生阀 :NAO_ROLE_AGENTS=off ⇒ 不注册。
15
+ // BR9 分发面 :扩展在**每个 trusted 会话**加载(非仅舰队会话),前提同 pi.skills。
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ export const RUNTIME_AGENT_REGISTER_EVENT = "pi-subagents:runtime-agent-register:v1";
22
+ export const RUNTIME_AGENT_REGISTER_VERSION = 1;
23
+ export const ENV_ANCHOR_HEADING = "## 环境锚点";
24
+ export const ROLE_AGENT_PREFIX = "nao-";
25
+
26
+ // 包根 = <pkg>/.agents/extensions/<this> 向上两级
27
+ const EXT_DIR = dirname(fileURLToPath(import.meta.url));
28
+ const PKG_ROOT = resolve(EXT_DIR, "..", "..");
29
+
30
+ function warn(msg) {
31
+ try {
32
+ console.warn(`[nao-role-agents] ${msg}`);
33
+ } catch {
34
+ /* 日志失败不得影响会话 */
35
+ }
36
+ }
37
+
38
+ /** root 是否是 nao 机制包根(有 .agents/roles.yaml 才算,防把 shim-only 项目当包根) */
39
+ export function hasRoleMechanism(root) {
40
+ try {
41
+ return readFileSync(join(root, ".agents", "roles.yaml"), "utf8").length > 0;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /** 锚点用的 NAO_SKILLS:优先 env;env 指向的不是机制包根则回退扩展自身包根(下游 shim-only 场景) */
48
+ export function resolveSkillsRoot(env = process.env) {
49
+ const fromEnv = env && env.NAO_SKILLS;
50
+ if (fromEnv && hasRoleMechanism(fromEnv)) return resolve(fromEnv);
51
+ return PKG_ROOT;
52
+ }
53
+
54
+ /** 环境锚点行 —— 与 nao-fleet.sh:1068 的 printf 段逐字一致(cases/17 AC3 cross-assert) */
55
+ export function buildAnchorLine(skillsRoot) {
56
+ return `- NAO_SKILLS=${skillsRoot}(nao 机制**包根**;@.agents/... 引用一律以 \`$NAO_SKILLS/.agents/\` 为根解析。注意:项目内 \`.agents/\` 只有 shim,不存机制副本)`;
57
+ }
58
+
59
+ /** 拆分卡片:frontmatter 文本 + 正文(正文 = 首块 frontmatter 之后,逐字节保留原样) */
60
+ export function splitCard(raw) {
61
+ if (!raw.startsWith("---\n")) return { frontmatter: "", body: raw };
62
+ const end = raw.indexOf("\n---\n", 3);
63
+ if (end < 0) return { frontmatter: "", body: raw };
64
+ return { frontmatter: raw.slice(4, end + 1), body: raw.slice(end + 5) };
65
+ }
66
+
67
+ /**
68
+ * 卡正文(去 frontmatter + 首尾空白)—— pi-subagents registry 要求
69
+ * `systemPrompt.trim() === systemPrompt`(禁首尾空白,实测 error: "must be a non-empty string
70
+ * without leading or trailing whitespace"),故只归一化首尾空白,内部逐字节保留。
71
+ */
72
+ export function cardBody(raw) {
73
+ const { body } = splitCard(raw);
74
+ return body.replace(/^\n+/, "").replace(/\s+$/, "");
75
+ }
76
+
77
+ /** 取 frontmatter 单行字段(首个命中) */
78
+ export function frontmatterField(frontmatter, key) {
79
+ for (const line of frontmatter.split("\n")) {
80
+ const m = /^([A-Za-z_][A-Za-z0-9_]*):[ \t]*(.*)$/.exec(line);
81
+ if (m && m[1] === key) return m[2];
82
+ }
83
+ return "";
84
+ }
85
+
86
+ /** 严格子集解析 roles.yaml(角色 id 2 空格 / card 4 空格),保持声明顺序 */
87
+ export function parseRolesYaml(text) {
88
+ const roles = [];
89
+ let cur = null;
90
+ for (const line of text.split("\n")) {
91
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
92
+ let m = /^ ([a-z0-9][a-z0-9-]*):[ \t]*$/.exec(line);
93
+ if (m) {
94
+ cur = { role: m[1], card: "" };
95
+ roles.push(cur);
96
+ continue;
97
+ }
98
+ m = /^ card:[ \t]*(\S+)[ \t]*$/.exec(line);
99
+ if (m && cur) cur.card = m[1];
100
+ }
101
+ return roles.filter((r) => r.role && r.card);
102
+ }
103
+
104
+ /**
105
+ * 纯装配:读机制文件 → 6 条 agent 定义(不触碰 pi / 事件总线,便于探针逐字节比对)。
106
+ * @returns {{name:string,role:string,card:string,description:string,definition:object}[]}
107
+ */
108
+ export function buildRoleAgentDefinitions({ root, skillsRoot } = {}) {
109
+ const base = root || PKG_ROOT;
110
+ const anchorRoot = skillsRoot || base;
111
+ const roles = parseRolesYaml(readFileSync(join(base, ".agents", "roles.yaml"), "utf8"));
112
+ const anchorLine = buildAnchorLine(anchorRoot);
113
+ return roles.map(({ role, card }) => {
114
+ const raw = readFileSync(join(base, ".agents", "prompts", card), "utf8");
115
+ const { frontmatter } = splitCard(raw);
116
+ const description = frontmatterField(frontmatter, "description");
117
+ return {
118
+ name: `${ROLE_AGENT_PREFIX}${role}`,
119
+ role,
120
+ card,
121
+ description,
122
+ definition: {
123
+ description,
124
+ // BR2 同卡同锚:cardBody 逐字节 + 与 nao-fleet.sh 逐字一致的锚点段(`\n## 环境锚点\n<行>`)
125
+ systemPrompt: `${cardBody(raw)}\n${ENV_ANCHOR_HEADING}\n${anchorLine}`,
126
+ systemPromptMode: "replace",
127
+ inheritProjectContext: true,
128
+ },
129
+ };
130
+ });
131
+ }
132
+
133
+ /** 经事件总线注册(逐条 try/catch;无 owner 即停)。返回 {registrations, errors},绝不 throw。 */
134
+ export function registerRoleAgents(pi, definitions) {
135
+ const registrations = [];
136
+ const errors = [];
137
+ for (const def of definitions) {
138
+ const request = {
139
+ version: RUNTIME_AGENT_REGISTER_VERSION,
140
+ name: def.name,
141
+ definition: def.definition,
142
+ };
143
+ try {
144
+ pi.events.emit(RUNTIME_AGENT_REGISTER_EVENT, request);
145
+ const result = request.result;
146
+ if (result === undefined) {
147
+ errors.push(`${def.name}: pi-subagents 未安装/未就绪(事件无 owner 响应)`);
148
+ break; // 后续同名合同样无人接管,不刷屏
149
+ }
150
+ if (result.ok === false) throw result.error;
151
+ if (!result.ok || !result.registration || typeof result.registration.dispose !== "function") {
152
+ throw new Error("注册结果格式非法(缺 registration.dispose)");
153
+ }
154
+ registrations.push(result.registration);
155
+ } catch (error) {
156
+ errors.push(`${def.name}: ${error instanceof Error ? error.message : String(error)}`);
157
+ }
158
+ }
159
+ return { registrations, errors };
160
+ }
161
+
162
+ /** 释放注册(幂等:其 dispose 自身幂等;异常吞掉不 throw) */
163
+ export function disposeRoleAgents(registrations) {
164
+ for (const reg of registrations || []) {
165
+ try {
166
+ reg.dispose();
167
+ } catch {
168
+ /* 清理失败不得影响会话 */
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * pi 扩展工厂(薄 glue):
175
+ * session_start → 先 dispose 再注册(幂等;NAO_ROLE_AGENTS=off 跳过)
176
+ * session_shutdown → dispose
177
+ * 任何失败仅 warn,绝不 throw(最坏 = 无 agent,不伤会话)。
178
+ */
179
+ export default function naoRoleAgents(pi) {
180
+ let registrations = [];
181
+ const teardown = () => {
182
+ disposeRoleAgents(registrations);
183
+ registrations = [];
184
+ };
185
+
186
+ const onSessionStart = () => {
187
+ teardown(); // BR5 幂等:重复 session_start 净 6 条
188
+ if (String(process.env.NAO_ROLE_AGENTS || "").toLowerCase() === "off") return; // BR6
189
+ try {
190
+ const root = resolveSkillsRoot();
191
+ const definitions = buildRoleAgentDefinitions({ root, skillsRoot: root });
192
+ const outcome = registerRoleAgents(pi, definitions);
193
+ registrations = outcome.registrations;
194
+ for (const err of outcome.errors) warn(`注册失败(仅 warn): ${err}`);
195
+ } catch (error) {
196
+ warn(`装配失败(仅 warn): ${error instanceof Error ? error.message : String(error)}`);
197
+ }
198
+ };
199
+
200
+ try {
201
+ pi.on("session_start", onSessionStart);
202
+ pi.on("session_shutdown", teardown);
203
+ } catch (error) {
204
+ warn(`生命周期挂载失败(仅 warn): ${error instanceof Error ? error.message : String(error)}`);
205
+ }
206
+ return teardown;
207
+ }
@@ -1397,6 +1397,34 @@ cmd_check() {
1397
1397
  shopt -u nullglob
1398
1398
  (( orphan )) || echo ' ✓ 无孤儿卡'
1399
1399
 
1400
+ echo "== pi 扩展声明(T527:声明 + 文件存在,防「声明漏写 ⇒ 静默无 agent」)=="
1401
+ if [[ ! -f "$SKILLS_DIR/package.json" ]]; then
1402
+ # 非包根(legacy 仓内真脚本 / 夹具只有 .agents):无 manifest 可核,跳过不判 FAIL
1403
+ echo ' · package.json 缺失(非包根/legacy:跳过 pi.extensions 校验)'
1404
+ elif ! command -v node >/dev/null 2>&1; then
1405
+ printf ' ✗ node 缺失,无法校验 pi.extensions 声明\n'; rc=1
1406
+ else
1407
+ local ext_list="" ext_bad=0 e
1408
+ if ext_list="$(node -e '
1409
+ const p=require(process.argv[1]); const pi=p.pi||{};
1410
+ const ex=pi.extensions;
1411
+ if(!Array.isArray(ex)||ex.length===0) process.exit(3);
1412
+ process.stdout.write(ex.join("\n"));
1413
+ ' "$SKILLS_DIR/package.json" 2>/dev/null)"; then
1414
+ while IFS= read -r e; do
1415
+ [[ -n "$e" ]] || continue
1416
+ if [[ ! -e "$SKILLS_DIR/$e" ]]; then
1417
+ printf ' ✗ pi.extensions 声明条目不存在: %s\n' "$e"; ext_bad=1; rc=1
1418
+ fi
1419
+ done <<< "$ext_list"
1420
+ if (( ext_bad == 0 )); then
1421
+ printf ' ✓ pi.extensions 声明 %d 条且文件存在\n' "$(printf '%s\n' "$ext_list" | grep -c .)"
1422
+ fi
1423
+ else
1424
+ printf ' ✗ pi.extensions 未声明或非法(package.json)\n'; rc=1
1425
+ fi
1426
+ fi
1427
+
1400
1428
  echo "== 公共规范 =="
1401
1429
  for f in output-format.md intercom-protocol.md; do
1402
1430
  if [[ -f "$COMMON_DIR/$f" ]]; then printf ' ✓ %s\n' "$f"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathan33/nao-skill",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "nao 多角色 AI 开发舰队:角色卡 / 技能 / 协作协议 / 工具链,一键接入项目(pi / Claude / 任意 Agent)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,9 @@
27
27
  "skills": [
28
28
  "./.agents/skills/nao-fleet/SKILL.md",
29
29
  "./.agents/skills/frontend-design/SKILL.md"
30
+ ],
31
+ "extensions": [
32
+ "./.agents/extensions/nao-role-agents.mjs"
30
33
  ]
31
34
  },
32
35
  "author": "Nathan Lee",