@clawos-dev/clawd 0.2.279 → 0.2.281

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/dist/cli.cjs CHANGED
@@ -48164,14 +48164,6 @@ var DEFAULT_PERSONAS = [
48164
48164
  network: true
48165
48165
  }
48166
48166
  },
48167
- {
48168
- personaId: "persona-builder",
48169
- label: "\u4EBA\u683C\u8BBE\u8BA1\u5E08",
48170
- model: "opus",
48171
- iconKey: "reading",
48172
- public: false,
48173
- sandboxProfile: DEFAULT_BYPASS_PROFILE
48174
- },
48175
48167
  {
48176
48168
  // HTML PPT 制作师:把想法/文字/旧 PPT 转成单文件 HTML 演示稿
48177
48169
  // bundle 含 frontend-slides skill(MIT,Zara Zhang),daemon ship 进 .claude/skills/
@@ -48398,6 +48390,61 @@ function migrateCodexSandbox(args) {
48398
48390
  }
48399
48391
  if (n > 0) args.logger.info("persona.codex-sandbox.done", { migrated: n });
48400
48392
  }
48393
+ var PERSONA_BUILDER_ID = "persona-builder";
48394
+ var PERSONA_BUILDER_TOMBSTONE_MARKER = "<!-- clawd:persona-builder-retired -->";
48395
+ var PERSONA_BUILDER_TOMBSTONE = `${PERSONA_BUILDER_TOMBSTONE_MARKER}
48396
+ \u300C\u4EBA\u683C\u8BBE\u8BA1\u5E08\u300D\u5DF2\u4E0B\u7EBF\u3002
48397
+
48398
+ \u5EFA\u65B0 persona \u7684\u80FD\u529B\u5DF2\u5E76\u5165 **clawd \u7BA1\u5BB6**\uFF08persona-clawd-butler\uFF09\u7684 \`persona-authoring\` skill\u3002
48399
+ \u843D\u76D8\u6539\u8D70 daemon \u7684 \`persona:create\`\uFF0C\u6C99\u7BB1\u914D\u7F6E\u4E0E\u754C\u9762\u4E0A\u5EFA\u51FA\u6765\u7684\u5B8C\u5168\u4E00\u81F4\uFF0C\u4E0D\u518D\u624B\u5199\u6587\u4EF6\u3002
48400
+
48401
+ \u8BF7\u5207\u5230 clawd \u7BA1\u5BB6\uFF0C\u76F4\u63A5\u8BF4\u300C\u6211\u60F3\u8981\u4E00\u4E2A xxx \u7684 persona\u300D\u3002
48402
+
48403
+ \u8FD9\u4E2A persona \u4E0B\u7684\u5386\u53F2\u4F1A\u8BDD\u4ECD\u53EF\u7FFB\u9605\uFF1B\u4F46\u4E0D\u8981\u518D\u7528\u5B83\u5EFA\u65B0 persona \u2014\u2014 \u624B\u5199\u843D\u76D8\u4EA7\u51FA\u7684\u6C99\u7BB1\u914D\u7F6E
48404
+ \u4E0E daemon \u6A21\u677F\u5DF2\u7ECF\u6F02\u79FB\u3002
48405
+ `;
48406
+ function containsAnyFile(dir) {
48407
+ let entries;
48408
+ try {
48409
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
48410
+ } catch {
48411
+ return false;
48412
+ }
48413
+ for (const e of entries) {
48414
+ if (e.isDirectory()) {
48415
+ if (containsAnyFile(path14.join(dir, e.name))) return true;
48416
+ } else {
48417
+ return true;
48418
+ }
48419
+ }
48420
+ return false;
48421
+ }
48422
+ function retirePersonaBuilder(args) {
48423
+ const dir = args.store.personaDirPath(PERSONA_BUILDER_ID);
48424
+ if (!fs12.existsSync(dir)) return;
48425
+ try {
48426
+ const used = containsAnyFile(path14.join(dir, ".clawd", "sessions")) || containsAnyFile(path14.join(dir, ".claude", "skills")) || containsAnyFile(path14.join(dir, "projects"));
48427
+ if (!used) {
48428
+ args.store.remove(PERSONA_BUILDER_ID);
48429
+ args.logger.info("persona.retire-builder.removed", { personaId: PERSONA_BUILDER_ID });
48430
+ return;
48431
+ }
48432
+ let wrote = false;
48433
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
48434
+ const p2 = path14.join(dir, name);
48435
+ const cur = fs12.existsSync(p2) ? fs12.readFileSync(p2, "utf8") : null;
48436
+ if (cur?.startsWith(PERSONA_BUILDER_TOMBSTONE_MARKER)) continue;
48437
+ fs12.writeFileSync(p2, PERSONA_BUILDER_TOMBSTONE);
48438
+ wrote = true;
48439
+ }
48440
+ if (wrote) args.logger.info("persona.retire-builder.tombstoned", { personaId: PERSONA_BUILDER_ID });
48441
+ } catch (err) {
48442
+ args.logger.warn("persona.retire-builder.failed", {
48443
+ personaId: PERSONA_BUILDER_ID,
48444
+ error: err instanceof Error ? err.message : String(err)
48445
+ });
48446
+ }
48447
+ }
48401
48448
  function findDeployKitRoot(logger) {
48402
48449
  const candidates = [];
48403
48450
  try {
@@ -54574,14 +54621,17 @@ function createHttpRouter(deps) {
54574
54621
  return true;
54575
54622
  }
54576
54623
  const dispatchArgs = typeof body === "object" && body != null ? body : {};
54624
+ let broadcastAfterResponse;
54577
54625
  try {
54578
54626
  const result = await deps.authedRpc.dispatch(method, dispatchArgs, auth.context);
54579
54627
  sendJson(res, 200, { ok: true, result: result.response }, AUTHED_RPC_CORS_HEADERS);
54628
+ broadcastAfterResponse = result.broadcast;
54580
54629
  } catch (err) {
54581
54630
  const e = err;
54582
54631
  const code = e?.code ?? "INTERNAL";
54583
54632
  sendJson(res, authedRpcErrorStatus(code), { ok: false, error: code, message: e?.message ?? String(err) }, AUTHED_RPC_CORS_HEADERS);
54584
54633
  }
54634
+ if (broadcastAfterResponse?.length) deps.authedRpc.fanOutBroadcast?.(broadcastAfterResponse);
54585
54635
  return true;
54586
54636
  }
54587
54637
  if (url.pathname.startsWith("/preview/")) {
@@ -58074,7 +58124,7 @@ function computeMethodAccess(args) {
58074
58124
  }
58075
58125
 
58076
58126
  // src/version.ts
58077
- var version = "0.2.279".length > 0 ? "0.2.279" : "dev";
58127
+ var version = "0.2.281".length > 0 ? "0.2.281" : "dev";
58078
58128
 
58079
58129
  // src/cli-probe/probe.ts
58080
58130
  var fs54 = __toESM(require("fs"), 1);
@@ -61343,6 +61393,7 @@ async function startDaemon(config) {
61343
61393
  refreshSandboxSettings({ store: personaStore, logger });
61344
61394
  migrateAgentsMirror({ store: personaStore, logger });
61345
61395
  migrateCodexSandbox({ store: personaStore, logger });
61396
+ retirePersonaBuilder({ store: personaStore, logger });
61346
61397
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
61347
61398
  const dispatchStore = createDispatchStore({
61348
61399
  filePath: import_node_path63.default.join(config.dataDir, "dispatch.json"),
@@ -62076,6 +62127,23 @@ async function startDaemon(config) {
62076
62127
  const authResolver = new AuthContextResolver({
62077
62128
  ownerToken: resolvedAuthToken
62078
62129
  });
62130
+ const fanOutBroadcast = (broadcast) => {
62131
+ for (const { frame: bf, target } of broadcast ?? []) {
62132
+ if (target === "all") {
62133
+ transport?.broadcastAll(bf);
62134
+ continue;
62135
+ }
62136
+ const sid = bf.sessionId;
62137
+ if (target === "first-subscriber" && sid) {
62138
+ const handle = transport?.firstSubscriber(sid);
62139
+ if (handle) handle.send(bf);
62140
+ continue;
62141
+ }
62142
+ if (sid) {
62143
+ transport?.broadcastToSession(sid, bf);
62144
+ }
62145
+ }
62146
+ };
62079
62147
  const authedRpc = {
62080
62148
  authenticate: (token) => authenticate(token, buildConnectAuthDeps()),
62081
62149
  dispatch: (method, body, ctx) => {
@@ -62091,7 +62159,9 @@ async function startDaemon(config) {
62091
62159
  handlers,
62092
62160
  feishuActive: () => feishuActive
62093
62161
  });
62094
- }
62162
+ },
62163
+ // mutating RPC 经 HTTP 调用时 handler broadcast 帧的外推(http-router 在 response 送出后调)
62164
+ fanOutBroadcast
62095
62165
  };
62096
62166
  const viewerAssetLoader = tryLoadViewerAssets(logger);
62097
62167
  const shareUiAssetLoader = tryLoadShareUi(logger);
@@ -62243,21 +62313,7 @@ async function startDaemon(config) {
62243
62313
  if (requestId && result.response) {
62244
62314
  client.send({ ...result.response, requestId });
62245
62315
  }
62246
- for (const { frame: bf, target } of result.broadcast ?? []) {
62247
- if (target === "all") {
62248
- transport?.broadcastAll(bf);
62249
- continue;
62250
- }
62251
- const sid = bf.sessionId;
62252
- if (target === "first-subscriber" && sid) {
62253
- const handle = transport?.firstSubscriber(sid);
62254
- if (handle) handle.send(bf);
62255
- continue;
62256
- }
62257
- if (sid) {
62258
- transport?.broadcastToSession(sid, bf);
62259
- }
62260
- }
62316
+ fanOutBroadcast(result.broadcast);
62261
62317
  });
62262
62318
  manager.sweepEphemeralOnStartup();
62263
62319
  await wss.start();
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: persona-authoring
3
+ description: 老板要在 clawd 里加一个新 persona 时触发——「我想要一个 xxx 的助手」「给我建个专门干 yyy 的 persona」「帮我加个人格」,也接 scan-sessions-suggest-personas 勾选后转交过来的候选。负责把一句话需求变成落盘的新 persona。只管**建新的**;改已有 persona 的人格 / 模型 / 沙箱是 clawd-config-editor,不是本 skill。
4
+ ---
5
+
6
+ # 建一个新 persona
7
+
8
+ 老板一句话进来,你补齐字段、写好人格、调一次 RPC 落盘。**不要多轮访谈**——老板要的是一个
9
+ 能用的 persona,不是一场需求评审。
10
+
11
+ ## 1. 听需求 → 抽字段
12
+
13
+ | 字段 | 怎么定 |
14
+ |---|---|
15
+ | `slug` | persona 的身份。从需求语义推一个**英文短名**("专门写商业文案的助手" → `copywriter`)。约束:`^[a-z0-9]+(-[a-z0-9]+)*$`、≤32 字符。daemon 自己拼 `persona-` 前缀,**你不要带前缀** |
16
+ | `label` | UI 显示名。默认中文短名("文案助手""调试侦探"),跟现有 persona 风格对齐;老板要英文 / 长名照办 |
17
+ | `personality` | CLAUDE.md 全文,见 §2 |
18
+ | `model` | 默认 `opus`;老板明确说"用 haiku / sonnet"再换 |
19
+ | `iconKey` | 不确定就省略(UI 兜默认)。9 个合法 key:`research`(研究)/ `code`(开发)/ `loop`(Loop 任务)/ `qa`(问答)/ `reading`(阅读)/ `debug`(调试)/ `idea`(创意)/ `doc`(文档)/ `assist`(助手) |
20
+ | `public` | 默认 `false`(私有);老板说"想分享出去"才 `true` |
21
+
22
+ **互动原则**:
23
+
24
+ - 一句话足够推断 → **不问**,直接把生成的定位和人格给老板看,他改不满意的地方
25
+ - 一句话太糙 → 挑**最影响人格定位**的 1-2 个点问一轮就够;典型:它的工作边界 / 它该问还是该先做 /
26
+ 它要不要能读写老板本机文件。不要 3-4 轮访谈
27
+ - 老板说"你看着办" → 你看着办,别再追问
28
+
29
+ ## 2. 写 CLAUDE.md
30
+
31
+ 这是真正的"灵魂"。**风格灵活**,每个 persona 应该长得不一样,按老板需求和你对人设的理解写。
32
+ 但保底要有这四块:
33
+
34
+ 1. **第一句:定位**("你是老板的 xxx 助手"),让任何打开它的 agent(Claude Code / Codex)
35
+ 一秒进入角色
36
+ 2. **何时找它**:trigger 场景,老板会在什么情况下切到这个 persona
37
+ 3. **工作方式 / 边界**:该做什么、不做什么;该问还是该先做;有没有红线
38
+ 4. **行为规范**:跟老板对话的风格(简洁 vs 详细、是否质疑老板判断、要不要解释思路)
39
+
40
+ 可以加但不强制:概念地图、目录结构参考、初次见面检查清单。
41
+
42
+ **硬性要求**:
43
+
44
+ - 让产出的 persona 知道"它是 clawd 的一个 persona"——避免新 persona 把自己说成独立产品
45
+ - 红线写**具体行为**("不擅自归类" / "不堆根目录"),不写空话("要谨慎" / "要负责")
46
+ - 老板的全局 CLAUDE.md 已经规定"每次回答必须以'老板'开头",新 persona 自动继承,
47
+ persona 级 CLAUDE.md 里不用重复
48
+ - 从 `scan-sessions-suggest-personas` 转交过来的候选,把画像卡里的**行为特征**沉淀进去
49
+ (常用什么工具、碰什么文件、边界在哪),别只写一句空泛的定位
50
+
51
+ ## 3. 落盘:一次 `persona:create`
52
+
53
+ ```
54
+ call({ method: "persona:create", args: {
55
+ slug: "copywriter",
56
+ label: "文案助手",
57
+ personality: "<CLAUDE.md 全文>",
58
+ model: "opus",
59
+ iconKey: "doc"
60
+ }}) # clawd-rpc MCP tool
61
+ ```
62
+
63
+ 三条务必记住:
64
+
65
+ - **不要自己写文件。** `persona.json`、`CLAUDE.md`、`AGENTS.md` 镜像、沙箱配置全由 daemon 落,
66
+ 跟老板在界面上点出来的 persona 完全一致。手写落盘一定会漂
67
+ - **slug 撞名 daemon 直接报错**(`personaId already exists: persona-<slug>`),不会自动加后缀。
68
+ 撞了先告诉老板,换一个 slug 再调,别自作主张改名
69
+ - **不用重启 daemon**。RPC 落盘的同时已经登记进内存,刷新界面就能看到
70
+
71
+ ## 4. 报告
72
+
73
+ 一句话报 persona id + 显示名,然后**简短问一句**:"这个 persona 要装哪些 skill 吗?"
74
+
75
+ - 老板回"不用" / "暂时不需要" → 跳过
76
+ - 老板说要 → 让老板给 skill 名(`<author>/<skill-name>` 形式),你帮他跑 `npx skills add <name> -y`
77
+
78
+ **不要复述 CLAUDE.md 全文。** 老板要看自己会去看。
79
+
80
+ ## 红线
81
+
82
+ - **不举例 skill** —— 老板自己清楚他要什么,你举例会污染他的判断
83
+ - **不替老板写人设** —— 老板说"我要个调试助手",你别脑补"它应该会写测试 / 会画时序图 / …"。
84
+ 只把老板原话里的东西沉淀下来,不确定的留白等老板补
85
+ - **不动其他 persona 目录** —— 只建新的。老板要改已有 persona 切 `clawd-config-editor`
86
+ - **冲突先问后做** —— slug 撞名、老板需求前后矛盾,先说清楚再动手
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: scan-sessions-suggest-personas
3
+ description: 给还没配好 persona 的老板做一次性冷启动盘点——扫本机 Claude Code / Codex 的历史 session 反推老板常干哪几类活,产出 persona 候选交老板勾选。当老板说「我刚上手 clawd,帮我配几个 persona」「我该建什么 persona」「根据我的使用记录推荐 persona」,或新会话 Setup 阶段发现还没盘点过时使用。只出候选不落盘(落盘交 persona-authoring);日常查会话内容 / 搜话题 / 汇总某时段 session 是 clawd-session-lens,把历史会话导入 clawd 是 clawd-session-import,都不是本 skill。
4
+ ---
5
+
6
+ # 从历史 session 反推 persona
7
+
8
+ 老板已经在 Claude Code / Codex 里干了很多活,这些记录就是最真实的需求样本。
9
+ 比让他凭空想「我需要什么 persona」准得多——**人说不清自己的习惯,记录说得清**。
10
+
11
+ ## 和 clawd-session-import 的区别
12
+
13
+ 两件事,各干各的,别混:
14
+
15
+ | | 干什么 | 产物 |
16
+ |---|---|---|
17
+ | `clawd-session-import` | 把历史会话**搬进** clawd,能接着聊 | clawd session |
18
+ | 本 skill | 从历史会话**反推人格**,建议建什么 persona | persona 候选建议 |
19
+
20
+ 扫描解析逻辑相似,但目标不同。不要为了复用把两件事捏一起。
21
+
22
+ ## 流程
23
+
24
+ ### 1. 跑提取脚本
25
+
26
+ ```bash
27
+ node scripts/extract-signals.mjs --days 7 --out /tmp/persona-scan
28
+ ```
29
+
30
+ **零依赖,只要有 node 就能跑**(不需要 python、不装任何包)。无 LLM、无网络、只读。
31
+ 约 100MB session 记录 0.4 秒扫完。
32
+
33
+ 常用参数:
34
+
35
+ | 参数 | 默认 | 说明 |
36
+ |---|---|---|
37
+ | `--days` | 7 | 回溯天数。信号太少就放宽到 14 / 30 |
38
+ | `--min-queries` | 5 | 低于此 query 数视为零散试用,不出卡 |
39
+ | `--min-days` | 2 | 低于此活跃天数视为一次性任务,不出卡 |
40
+ | `--no-skip-known` | 关 | 默认**跳过**已有 persona 覆盖的目录;加上它才全量报 |
41
+
42
+ 对 node 版本没有硬要求:`node:sqlite`(读 codex 的 threads 表拿 git 分支等元信息)
43
+ 只在 node ≥22.5 上有,拿不到就自动降级到扫 rollout 文件,**query 提取结果完全相同**。
44
+ 输出是确定性的——同样的输入跑两次,逐字节一致。
45
+
46
+ 产出两个文件:
47
+
48
+ - **`profiles.json`** —— 每个工作目录一张画像卡,**这是你要读的**,通常 <20KB
49
+ - `signals.jsonl` —— 逐条原始 query,留档备查,一般不用读
50
+
51
+ ### 2. 读画像卡,判断这是什么活
52
+
53
+ 每张卡长这样:
54
+
55
+ ```json
56
+ { "proj": "...", "cwd": "/Users/x/work/papers", "git_branch": "",
57
+ "queries": 34, "unique_queries": 29, "sessions": 8, "active_days": 5,
58
+ "top_tools": [["WebFetch",41],["Read",38],["Write",12]],
59
+ "top_exts": [[".pdf",22],[".md",14]],
60
+ "session_titles": ["提炼 Transformer 论文方法部分", "对比三篇实验设置", "..."],
61
+ "samples": ["...", "..."] }
62
+ ```
63
+
64
+ **读的顺序:`session_titles` → `top_tools` → `samples`。**
65
+
66
+ `session_titles` 是 Claude Code 自己给每个会话生成的标题(约九成会话有),
67
+ **每条就是一句已经抽象好的任务摘要**——这正是你要输出的那种「意义」,
68
+ 不用再从原话里提炼。十来条标题排在一起,干什么活一目了然。
69
+
70
+ 工具指纹是行为事实,比说的话更硬,用来交叉验证标题的判断:
71
+
72
+ | 指纹 | 大概率是 |
73
+ |---|---|
74
+ | `Bash` + `Edit` 高频,`.ts/.py/.go` | 写代码 |
75
+ | `WebSearch` / `WebFetch` 压倒性 | 做调研 |
76
+ | `Read` + `Write`,`.md/.docx/.pdf` | 读写文档 |
77
+ | `mcp__lark-*` 密集 | 办公协同 |
78
+ | `Bash` 独大但没有 Edit | 运维 / 机器管理 |
79
+
80
+ 判断时守两条:
81
+
82
+ - **看重复出现的意图,不看单次任务。** 一次性的「帮我查个报错」不是 persona,
83
+ 连续五天都在「读文献 → 提炼 → 转中文」才是
84
+ - **警惕语义噪音。** 脚本滤得掉带标签的系统回填,滤不掉 agent 自己造的探针指令
85
+ (比如 `用 Bash 执行 cat .../marker.txt`)——这类长得像人话,靠你眼力剔除。
86
+ `session_titles` 基本不受这类污染,又一个优先看它的理由
87
+
88
+ ### 3. 输出候选,交给老板勾选
89
+
90
+ **永远不自动创建。** 人格是老板的东西,你只出建议。
91
+
92
+ **不要贴老板的原话。** 抽象出这类活的**意义**再表述——原话散、带上下文、还牵扯隐私,
93
+ 抽象后的一句定位反而更能让老板一眼判断准不准。
94
+
95
+ 每个候选给四行:
96
+
97
+ ```
98
+ 候选 1 ── 论文研读助手 (slug: paper-reader)
99
+ 在干的活 连续 5 天、8 个会话,读英文文献 → 提炼方法 → 转中文结构化摘要
100
+ 行为特征 WebFetch/Read 为主,几乎不写代码,碰 .pdf/.bib
101
+ 建议定位 帮老板快速消化外文论文,只做消化和提炼,不代笔写作
102
+ 可能重叠 与现有 persona-researcher 有部分重合,researcher 偏通用网络调研
103
+ ```
104
+
105
+ 给 3-5 个就够,多了老板挑不动。同一类活散在多个目录的要**合并成一个候选**,
106
+ 不要一个目录一张卡。
107
+
108
+ ### 4. 老板勾中的,转交 `persona-authoring`
109
+
110
+ **你不落盘。** 老板勾完,把选中候选的这几项交给 `persona-authoring` skill:
111
+
112
+ - **slug / label** —— 候选卡上已经给了
113
+ - **建议定位** —— 候选卡的那一行
114
+ - **行为特征** —— 常用什么工具、碰什么文件、边界在哪。这条必须带上:新 persona 的
115
+ CLAUDE.md 要把它沉淀进去,别只留一句空泛的定位
116
+
117
+ 落盘、id 拼装、沙箱配置由 `persona-authoring` 调一次 `persona:create` 完成,你不碰文件。
118
+
119
+ ## 边界
120
+
121
+ - **只读,不出网。** 脚本不修改任何 session 文件,不上传任何内容
122
+ - **不自动建 persona。** 只出候选,老板勾选后才落盘
123
+ - **跳过已有 persona 覆盖的目录**(默认行为)。老板已经有专人干的活不用重复推荐;
124
+ 要全量看时才加 `--no-skip-known`
125
+ - **信号不足就直说。** 如果扫完只剩零散试用,老实告诉老板「最近一周的活已经被现有
126
+ persona 覆盖完了,没发现新的」,不要硬凑候选
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 扫描本机 Claude Code / Codex 的 session 记录,提取「用户真实 query」+ 工作画像。
4
+ *
5
+ * 零依赖、只读、不出网、无 LLM。只要有 node 就能跑(不需要 python)。
6
+ *
7
+ * 用法:
8
+ * node extract-signals.mjs --days 7 --out /tmp/persona-scan
9
+ * node extract-signals.mjs --days 30 --no-skip-known
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import os from 'node:os';
14
+ import readline from 'node:readline';
15
+
16
+ // node:sqlite 是实验特性,加载时 node 会打 ExperimentalWarning。
17
+ // 得先摘掉 node 自带的 listener,否则它照打不误。
18
+ process.removeAllListeners('warning');
19
+ process.on('warning', (w) => {
20
+ if (w.name === 'ExperimentalWarning' && /SQLite/i.test(w.message)) return;
21
+ console.warn(`${w.name}: ${w.message}`);
22
+ });
23
+
24
+ const HOME = os.homedir();
25
+ const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
26
+ const CODEX_DIR = path.join(HOME, '.codex');
27
+ const PERSONAS_DIR = path.join(HOME, '.clawd', 'personas');
28
+
29
+ // ---------- 系统回填识别:这些不是人在表达需求 ----------
30
+ const DROP_PREFIX = [
31
+ '<system-reminder', '<command-name', '<command-message', '<command-args',
32
+ '<local-command-stdout', '<local-command-stderr', '<user-prompt-submit-hook',
33
+ '<task-notification', '<turn', '<environment', '<user_instructions',
34
+ '[Dispatched from', '[Request interrupted', '[attachment:image:data:',
35
+ 'Caveat: The messages below', '# AGENTS.md instructions', '# CLAUDE.md',
36
+ '# 连接上下文', '你之前委派出去的任务有了结果',
37
+ 'Another Claude session sent a message', '<teammate-message',
38
+ ];
39
+ const DROP_SUBSTR = ['<clawd-shift-fire', '<teammate-message', '<task-notification'];
40
+
41
+ const B64 = /data:image\/[a-z]+;base64,[A-Za-z0-9+/=]+/g;
42
+
43
+ const clean = (t) => t.replace(B64, '[图片]').replace(/[ \t]+/g, ' ').trim();
44
+
45
+ /** 结构上能判定的噪音在这里滤干净。语义噪音(agent 自造的探针指令等)正则救不了,
46
+ * 交给下游聚类稀释——占比极低,不值得为它引入一层判断。 */
47
+ function isRealQuery(t) {
48
+ if (!t || t.length < 6) return false;
49
+ if (DROP_PREFIX.some((p) => t.startsWith(p))) return false;
50
+ const head = t.slice(0, 200);
51
+ if (DROP_SUBSTR.some((s) => head.includes(s))) return false;
52
+ if ((t[0] === '{' || t[0] === '[') && t.length > 300) return false; // 整块粘贴的 JSON/日志
53
+ return true;
54
+ }
55
+
56
+ function inWindow(ts, cutMs) {
57
+ const t = Date.parse(ts);
58
+ return Number.isFinite(t) && t >= cutMs;
59
+ }
60
+
61
+ /** 从 message.content 取纯文本;含 tool_result 块的整条判为工具回填。 */
62
+ function textOf(content) {
63
+ if (typeof content === 'string') return content;
64
+ if (!Array.isArray(content)) return '';
65
+ const parts = [];
66
+ for (const b of content) {
67
+ if (!b || typeof b !== 'object') continue;
68
+ if (b.type === 'tool_result') return '';
69
+ if (b.type === 'text') parts.push(b.text || '');
70
+ }
71
+ return parts.join(' ');
72
+ }
73
+
74
+ // ---------- 小工具 ----------
75
+ const bump = (m, k, n = 1) => m.set(k, (m.get(k) || 0) + n);
76
+ // 计数相同时按名字排——否则并列项谁进前 N 随遍历顺序漂移,同样输入得不到同样输出
77
+ const topN = (m, n) =>
78
+ [...m.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, n);
79
+
80
+ /** 递归列出目录下所有 .jsonl(不用 fs.glob,那是新版 node 才有的实验 API)。 */
81
+ function walkJsonl(dir, out = [], depth = 0) {
82
+ if (depth > 8) return out;
83
+ let ents;
84
+ try {
85
+ ents = fs.readdirSync(dir, { withFileTypes: true });
86
+ } catch {
87
+ return out;
88
+ }
89
+ // 排序:保证遍历顺序稳定,否则截断类字段(session_titles 等)会随机漂移
90
+ ents.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
91
+ for (const e of ents) {
92
+ const p = path.join(dir, e.name);
93
+ if (e.isDirectory()) walkJsonl(p, out, depth + 1);
94
+ else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(p);
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** 逐行流式读——单个 session 文件可能几十 MB,不能整个读进内存。 */
100
+ async function eachLine(file, fn) {
101
+ let rl;
102
+ try {
103
+ rl = readline.createInterface({
104
+ input: fs.createReadStream(file, { encoding: 'utf8' }),
105
+ crlfDelay: Infinity,
106
+ });
107
+ } catch {
108
+ return;
109
+ }
110
+ try {
111
+ for await (const line of rl) {
112
+ if (!line) continue;
113
+ let rec;
114
+ try {
115
+ rec = JSON.parse(line);
116
+ } catch {
117
+ continue;
118
+ }
119
+ fn(rec);
120
+ }
121
+ } catch {
122
+ /* 文件读坏了就跳过这个文件 */
123
+ }
124
+ }
125
+
126
+ // ---------- 已有 persona:这些目录下的活已经有人干了,默认跳过 ----------
127
+ function knownPersonaDirs() {
128
+ const out = new Map();
129
+ let ents;
130
+ try {
131
+ ents = fs.readdirSync(PERSONAS_DIR, { withFileTypes: true });
132
+ } catch {
133
+ return out;
134
+ }
135
+ for (const e of ents) {
136
+ if (!e.isDirectory()) continue;
137
+ const d = path.join(PERSONAS_DIR, e.name);
138
+ if (fs.existsSync(path.join(d, '.clawd', 'persona.json'))) {
139
+ try {
140
+ out.set(fs.realpathSync(d), e.name);
141
+ } catch {
142
+ out.set(d, e.name);
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function coveredBy(cwd, known) {
150
+ if (!cwd) return null;
151
+ let rp = cwd;
152
+ try {
153
+ rp = fs.realpathSync(cwd);
154
+ } catch { /* 目录可能已删,用原字符串比 */ }
155
+ for (const [d, name] of known) {
156
+ if (rp === d || rp.startsWith(d + path.sep)) return name;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ const blank = () => ({
162
+ sessions: new Set(), days: new Set(), cwd: '', branch: '',
163
+ tools: new Map(), exts: new Map(), titles: [],
164
+ });
165
+ const profOf = (prof, k) => {
166
+ if (!prof.has(k)) prof.set(k, blank());
167
+ return prof.get(k);
168
+ };
169
+
170
+ // ---------- Claude Code ----------
171
+ async function scanClaude(cutMs, sig, prof) {
172
+ let dirs;
173
+ try {
174
+ dirs = fs.readdirSync(CLAUDE_PROJECTS, { withFileTypes: true });
175
+ } catch {
176
+ return;
177
+ }
178
+ dirs.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
179
+ for (const de of dirs) {
180
+ if (!de.isDirectory()) continue;
181
+ const proj = de.name;
182
+ const pdir = path.join(CLAUDE_PROJECTS, proj);
183
+ let files;
184
+ try {
185
+ files = fs.readdirSync(pdir).filter((f) => f.endsWith('.jsonl')).sort();
186
+ } catch {
187
+ continue;
188
+ }
189
+ for (const fname of files) {
190
+ const f = path.join(pdir, fname);
191
+ try {
192
+ if (fs.statSync(f).mtimeMs < cutMs) continue; // 整个文件都在窗口外,不必逐行解析
193
+ } catch {
194
+ continue;
195
+ }
196
+ const sid = fname.slice(0, -6);
197
+ const p = profOf(prof, proj);
198
+
199
+ await eachLine(f, (d) => {
200
+ const typ = d.type;
201
+ const cwd = d.cwd || '';
202
+
203
+ // Claude Code 自己生成的会话标题:一句已抽象好的任务摘要,
204
+ // 比任何启发式取样都准,是画像的首选信号(约 9 成会话有)
205
+ if (typ === 'ai-title' && d.aiTitle) {
206
+ p.titles.push(d.aiTitle);
207
+ return;
208
+ }
209
+
210
+ const ts = d.timestamp || '';
211
+ if (!inWindow(ts, cutMs)) return;
212
+
213
+ if (typ === 'user' && !d.isMeta && !d.isSidechain) {
214
+ const t = clean(textOf(d.message?.content));
215
+ if (!isRealQuery(t)) return;
216
+ sig.push({ src: 'claude', proj, cwd, session: sid, date: ts.slice(0, 10), text: t.slice(0, 600) });
217
+ p.sessions.add(sid);
218
+ p.days.add(ts.slice(0, 10));
219
+ if (cwd) p.cwd = cwd;
220
+ if (d.gitBranch) p.branch = d.gitBranch;
221
+ } else if (typ === 'assistant') {
222
+ for (const b of d.message?.content || []) {
223
+ if (!b || b.type !== 'tool_use') continue;
224
+ bump(p.tools, b.name || '?');
225
+ const fp = b.input?.file_path;
226
+ if (typeof fp === 'string' && fp) {
227
+ const ext = path.extname(fp).toLowerCase();
228
+ if (ext) bump(p.exts, ext);
229
+ }
230
+ }
231
+ }
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ // ---------- Codex ----------
238
+ /** threads 表是 codex 的权威索引,比解析 rollout 准,还带 git/model 信息。
239
+ * node:sqlite 要 node>=22.5 且是实验特性——拿不到就返回空,由 rollout 扫描兜底。 */
240
+ async function codexMetaFromDb() {
241
+ const out = new Map();
242
+ let dbs;
243
+ try {
244
+ dbs = fs.readdirSync(CODEX_DIR)
245
+ .filter((f) => /^state_.*\.sqlite$/.test(f))
246
+ .sort()
247
+ .map((f) => path.join(CODEX_DIR, f));
248
+ } catch {
249
+ return out;
250
+ }
251
+ if (!dbs.length) return out;
252
+
253
+ let DatabaseSync;
254
+ try {
255
+ ({ DatabaseSync } = await import('node:sqlite'));
256
+ } catch {
257
+ return out; // 老版本 node 没有 node:sqlite,走兜底
258
+ }
259
+ try {
260
+ const db = new DatabaseSync(dbs[dbs.length - 1], { readOnly: true });
261
+ const cols = new Set(db.prepare('PRAGMA table_info(threads)').all().map((r) => r.name));
262
+ if (!cols.has('rollout_path')) return out;
263
+ const pick = (n) => (cols.has(n) ? n : `'' AS ${n}`);
264
+ const rows = db
265
+ .prepare(`SELECT rollout_path, cwd, ${pick('git_branch')}, ${pick('model')} FROM threads`)
266
+ .all();
267
+ for (const r of rows) {
268
+ if (!r.rollout_path) continue;
269
+ let key = r.rollout_path;
270
+ try {
271
+ key = fs.realpathSync(key);
272
+ } catch { /* 文件可能已删 */ }
273
+ out.set(key, { cwd: r.cwd || '', branch: r.git_branch || '', model: r.model || '' });
274
+ }
275
+ db.close();
276
+ } catch {
277
+ return out;
278
+ }
279
+ return out;
280
+ }
281
+
282
+ async function scanCodex(cutMs, sig, prof) {
283
+ const db = await codexMetaFromDb();
284
+ const files = walkJsonl(path.join(CODEX_DIR, 'sessions'));
285
+ for (const f of files) {
286
+ try {
287
+ if (fs.statSync(f).mtimeMs < cutMs) continue;
288
+ } catch {
289
+ continue;
290
+ }
291
+ let meta = db.get(f);
292
+ if (!meta) {
293
+ try {
294
+ meta = db.get(fs.realpathSync(f));
295
+ } catch { /* 取不到就靠 session_meta 兜底 */ }
296
+ }
297
+ let cwd = meta?.cwd || '';
298
+ const branch = meta?.branch || '';
299
+ const sid = path.basename(f).slice(0, -6);
300
+ let key = cwd ? `codex:${path.basename(cwd)}` : null;
301
+
302
+ await eachLine(f, (d) => {
303
+ const p = d.payload || {};
304
+ if (d.type === 'session_meta') {
305
+ cwd = cwd || p.cwd || '';
306
+ key = `codex:${path.basename(cwd) || '?'}`;
307
+ return;
308
+ }
309
+ const ts = d.timestamp || '';
310
+ if (d.type !== 'response_item' || !inWindow(ts, cutMs)) return;
311
+ if (key === null) key = `codex:${path.basename(cwd) || '?'}`;
312
+ const pr = profOf(prof, key);
313
+
314
+ if (p.role === 'user') {
315
+ const t = clean(
316
+ (p.content || [])
317
+ .filter((b) => b && b.type === 'input_text')
318
+ .map((b) => b.text || '')
319
+ .join(' '),
320
+ );
321
+ if (!isRealQuery(t)) return;
322
+ sig.push({ src: 'codex', proj: key, cwd, session: sid, date: ts.slice(0, 10), text: t.slice(0, 600) });
323
+ pr.sessions.add(sid);
324
+ pr.days.add(ts.slice(0, 10));
325
+ pr.cwd = cwd;
326
+ if (branch) pr.branch = branch;
327
+ } else if (p.type === 'function_call') {
328
+ bump(pr.tools, p.name || '?');
329
+ }
330
+ });
331
+ }
332
+ }
333
+
334
+ // ---------- 主流程 ----------
335
+ function parseArgs(argv) {
336
+ const a = { days: 7, out: '.', samples: 12, minQueries: 5, minDays: 2, skipKnown: true };
337
+ for (let i = 0; i < argv.length; i++) {
338
+ const k = argv[i];
339
+ const next = () => argv[++i];
340
+ if (k === '--days') a.days = parseInt(next(), 10);
341
+ else if (k === '--out') a.out = next();
342
+ else if (k === '--samples') a.samples = parseInt(next(), 10);
343
+ else if (k === '--min-queries') a.minQueries = parseInt(next(), 10);
344
+ else if (k === '--min-days') a.minDays = parseInt(next(), 10);
345
+ else if (k === '--no-skip-known') a.skipKnown = false;
346
+ else if (k === '-h' || k === '--help') a.help = true;
347
+ }
348
+ return a;
349
+ }
350
+
351
+ const HELP = `扫描本机 Claude Code / Codex session,提取真实 query + 工作画像。
352
+
353
+ --days N 回溯天数 (默认 7)
354
+ --out DIR 输出目录 (默认 .)
355
+ --samples N 每个目录保留的 query 样本数 (默认 12)
356
+ --min-queries N 低于此 query 数不出卡 (默认 5)
357
+ --min-days N 低于此活跃天数不出卡 (默认 2)
358
+ --no-skip-known 不跳过已有 persona 覆盖的目录
359
+ `;
360
+
361
+ async function main() {
362
+ const a = parseArgs(process.argv.slice(2));
363
+ if (a.help) {
364
+ console.log(HELP);
365
+ return;
366
+ }
367
+ fs.mkdirSync(a.out, { recursive: true });
368
+ const cutMs = Date.now() - a.days * 86400000;
369
+
370
+ const sig = [];
371
+ const prof = new Map();
372
+ await scanClaude(cutMs, sig, prof);
373
+ await scanCodex(cutMs, sig, prof);
374
+ sig.sort((x, y) => (x.date < y.date ? -1 : x.date > y.date ? 1 : 0));
375
+
376
+ fs.writeFileSync(
377
+ path.join(a.out, 'signals.jsonl'),
378
+ sig.map((s) => JSON.stringify(s)).join('\n') + (sig.length ? '\n' : ''),
379
+ );
380
+
381
+ const byProj = new Map();
382
+ for (const s of sig) {
383
+ if (!byProj.has(s.proj)) byProj.set(s.proj, []);
384
+ byProj.get(s.proj).push(s);
385
+ }
386
+
387
+ const known = knownPersonaDirs();
388
+ const cards = [];
389
+ const skipped = [];
390
+ const thin = [];
391
+
392
+ for (const [k, v] of prof) {
393
+ const qs = byProj.get(k);
394
+ if (!qs || !qs.length) continue;
395
+
396
+ const owner = coveredBy(v.cwd, known);
397
+ if (owner && a.skipKnown) {
398
+ skipped.push([k, owner, qs.length]);
399
+ continue;
400
+ }
401
+ // session resume/fork 会重复落盘,按正文头部去重
402
+ const seen = new Set();
403
+ const uniq = [];
404
+ for (const q of qs) {
405
+ const key = q.text.slice(0, 80);
406
+ if (seen.has(key)) continue;
407
+ seen.add(key);
408
+ uniq.push(q);
409
+ }
410
+ // 信号量门槛:偶尔试一次的目录不构成一类「常干的活」
411
+ if (uniq.length < a.minQueries || v.days.size < a.minDays) {
412
+ thin.push([k, uniq.length, v.days.size]);
413
+ continue;
414
+ }
415
+ // 长文本多是粘贴的日志/报错,短的多是「继续」「对」这类确认,
416
+ // 真正表达需求的在中间段——按与 90 字的距离排序
417
+ const top = [...uniq]
418
+ .sort(
419
+ (x, y) =>
420
+ Math.abs(x.text.length - 90) - Math.abs(y.text.length - 90) ||
421
+ (x.text < y.text ? -1 : x.text > y.text ? 1 : 0),
422
+ )
423
+ .slice(0, a.samples);
424
+
425
+ cards.push({
426
+ proj: k, cwd: v.cwd, src: qs[0].src, git_branch: v.branch,
427
+ queries: qs.length, unique_queries: uniq.length,
428
+ sessions: v.sessions.size, active_days: v.days.size,
429
+ top_tools: topN(v.tools, 8),
430
+ top_exts: topN(v.exts, 6),
431
+ session_titles: [...new Set(v.titles)].slice(0, 20),
432
+ samples: top.map((q) => q.text.slice(0, 220)),
433
+ });
434
+ }
435
+ cards.sort((x, y) => y.queries - x.queries);
436
+
437
+ const pp = path.join(a.out, 'profiles.json');
438
+ fs.writeFileSync(pp, JSON.stringify(cards, null, 1));
439
+ const kb = fs.statSync(pp).size / 1024;
440
+
441
+ console.log(`真实 query : ${sig.length} 条`);
442
+ console.log(`画像卡 : ${cards.length} 张 (${kb.toFixed(0)} KB) → ${pp}`);
443
+ if (skipped.length) {
444
+ console.log(`已跳过 : ${skipped.length} 个目录(已有 persona 覆盖)`);
445
+ for (const [, owner, n] of skipped.sort((x, y) => y[2] - x[2])) {
446
+ console.log(` ${owner} (${n} 条)`);
447
+ }
448
+ }
449
+ if (thin.length) {
450
+ console.log(`信号太弱 : ${thin.length} 个目录(< ${a.minQueries} query 或 < ${a.minDays} 天)`);
451
+ }
452
+ if (!cards.length) console.log('\n没有发现已有 persona 之外、且信号足够的活。');
453
+ }
454
+
455
+ main().catch((e) => {
456
+ console.error('扫描失败:', e.message);
457
+ process.exit(1);
458
+ });
@@ -1,4 +1,5 @@
1
- 你是「clawd 管家」,老板的 chief-of-staff —— 知本机 clawd 一切,能主动,代拿小主意。
1
+ 你是「clawd 管家」,老板的 chief-of-staff —— 知本机 clawd 一切,能主动,代拿小主意,
2
+ 老板要添个新 persona 也是找你。
2
3
 
3
4
  ## 你怎么工作
4
5
 
@@ -16,7 +17,7 @@ call({ method: "persona:list" }) # clawd-rpc MCP tool
16
17
 
17
18
  **不猜**:拿不准就查(Read / Bash / WebFetch),不要凭印象编 clawd 功能。
18
19
 
19
- ## 6 个 skill
20
+ ## 8 个 skill
20
21
 
21
22
  按老板意图触发对应 skill;skill 自己 fetch 线上文档回答:
22
23
 
@@ -26,11 +27,24 @@ call({ method: "persona:list" }) # clawd-rpc MCP tool
26
27
  | 本机状态 / persona / extension / tunnel / contact / 排障 | `clawd-introspection` |
27
28
  | session 汇总 / 搜话题 / 拉全文 | `clawd-session-lens` |
28
29
  | dispatch / DM / 排定时 / 管联系人 | `clawd-orchestration` |
29
- | persona.json / CLAUDE.md / sandbox / profile / contacts | `clawd-config-editor` |
30
+ | **改**已有 persona CLAUDE.md / persona.json / sandbox,以及 profile / contacts | `clawd-config-editor` |
31
+ | **建**新 persona(老板说「我想要个 xxx 的助手」) | `persona-authoring` |
32
+ | 冷启动盘点:扫历史 session 反推该建哪些 persona | `scan-sessions-suggest-personas` |
30
33
  | 把 Claude Code / Codex 的历史会话关联进 clawd(冷启动) | `clawd-session-import` |
31
34
 
35
+ **建 vs 改别选错**:`persona-authoring` 只管从零建一个新 persona(落盘走 `persona:create`);
36
+ 已经存在的 persona 要改人格 / 换模型 / 调沙箱,一律 `clawd-config-editor`。
37
+
32
38
  ## Setup(新会话第一轮 check 一次)
33
39
 
34
40
  **称呼**:Read `~/.clawd/profile.json`。文件不存在 / `displayName` 为空 → 问老板「我该怎么称呼您?」,拿到名字后 Write / Edit 保留其它字段。告诉老板「记好了,需要重启 clawd daemon 让所有 persona 看到这个名字」。sandbox 拒写就说明是 listener 模式被调用——请老板在 owner 模式做。
35
41
 
42
+ **persona 冷启动盘点**:同一份 `~/.clawd/profile.json`,看有没有 `personaScanDoneAt` 字段。
43
+
44
+ - 有 → 跳过,一个字都别提
45
+ - 没有 → 用 `scan-sessions-suggest-personas` 扫一次历史 session,出 3-5 个候选交老板勾选;
46
+ 勾中的转 `persona-authoring` 落盘。老板回「先不用」也正常收工,不追问
47
+ - **无论哪种结局**,Edit `profile.json` 写入 `personaScanDoneAt: <当前毫秒时间戳>`(保留其它
48
+ 字段)—— 这是一次性盘点,此后不再主动问
49
+
36
50
  设置完进入正常工作。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.279",
3
+ "version": "0.2.281",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,163 +0,0 @@
1
- 你是老板的「人格设计师」—— 当老板想在 clawd 里加新 persona 时,找你干。
2
-
3
- # 你做什么
4
-
5
- 老板说"我想要一个 xxx 的 persona"或者"给我加一个能干 yyy 的助手",你负责把它变成 `~/.clawd/personas/persona-<id>/` 下面的一套完整文件 + 引导老板装 skill + 提醒重启。
6
-
7
- 你**不做**的事:
8
-
9
- - 不替老板做心理建设式访谈(不要 3-4 轮逼问"您真的想清楚了吗")
10
- - 不动其他 persona 的目录(只创建你这次要造的那个)
11
- - 不替老板重启 daemon(你写完文件就报告,重启交给老板)
12
- - 不调任何 daemon HTTP / RPC(**写文件就是创建**——见下面机制说明)
13
-
14
- # 关键机制:persona 是怎么生效的
15
-
16
- clawd daemon 启动 / 刷新时扫 `~/.clawd/personas/`,凡是含 `.clawd/persona.json` 的子目录就是一个 persona。所以**创建 = 写文件**,没有"注册"步骤。daemon 已经在跑的话,新 persona 通常**需要重启 daemon 或 UI 刷新**才能看到。
17
-
18
- 每个 persona 需要的最小文件集:
19
-
20
- ```
21
- ~/.clawd/personas/<persona-id>/
22
- .clawd/
23
- persona.json # 元信息(必需)
24
- sandbox-settings.json # 沙箱配置(必需,用 daemon 默认模板原样,claude 会话用)
25
- codex-sandbox.json # codex 沙箱配置(可不写,daemon 启动 migrate 会补默认断网版)
26
- CLAUDE.md # 人设(必需)
27
- AGENTS.md # 人设镜像(必需,内容与 CLAUDE.md 完全一致——claude 读
28
- # CLAUDE.md、codex 读 AGENTS.md,写完 CLAUDE.md 原样复制一份)
29
- .claude/skills/ # 可选,老板要装的 skill 会在这里(两种 agent 都认这个目录)
30
- ```
31
-
32
- # 工作流(轻互动模式,默认)
33
-
34
- ## 1. 听需求 → 抽关键字段
35
-
36
- 老板一句话进来后,你需要补齐这几个字段才能落盘——能从一句话推断就直接推断,**只问明显缺的那 1-2 个最关键的**,不要列清单逼问:
37
-
38
- | 字段 | 用途 | 推断规则 |
39
- |---|---|---|
40
- | **personaId** | 目录名 + daemon 索引 | 从老板的描述里提一个英文短 slug(如"专门写商业文案的助手" → `copywriter`),最终 id 是 `persona-<slug>`。**先检查 `~/.clawd/personas/<id>/` 是否已存在**,存在的话末尾补 4 位短随机(`a-z0-9`,参考 `persona-notes-6x66` 的风格)|
41
- | **label** | UI 显示名 | 默认中文短名("文案助手""调试侦探"等),跟现有 persona 风格对齐;老板要英文 / 长名照办 |
42
- | **model** | 默认模型 | 默认 `opus`,老板明确说"用 haiku/sonnet"再换 |
43
- | **iconKey** | 图标 | 不确定就省略(optional 字段,UI 兜默认)。9 个合法 key:`research`(研究)/ `code`(开发)/ `loop`(Loop 任务)/ `qa`(问答)/ `reading`(阅读)/ `debug`(调试)/ `idea`(创意)/ `doc`(文档)/ `assist`(助手)。其他值 UI 会回退到默认图标 |
44
- | **核心人设** | CLAUDE.md 内容 | 老板说清了直接写;模糊时挑 1-2 个最影响产出的点问(典型:"它的工作边界 / 它该问还是该先做 / 它要不要可以读写您本机文件") |
45
-
46
- **互动原则**:
47
-
48
- - 一句话足够推断 → 不问,直接生成给老板看,他改不满意的地方
49
- - 一句话太糙 → 挑**最影响人格定位**的 1-2 个点问一轮就够;不要 3-4 轮访谈
50
- - 老板说"你看着办" → 你看着办,别再追问
51
-
52
- ## 2. 生成 personaId 和落盘
53
-
54
- ```bash
55
- # 检查冲突
56
- ID="persona-<slug>"
57
- if [ -e ~/.clawd/personas/$ID ]; then
58
- ID="persona-<slug>-$(openssl rand -hex 2 | head -c4)"
59
- fi
60
-
61
- # 探测能不能写(owner 模式能,listener 模式不能;不能写就停下告诉老板)
62
- mkdir -p ~/.clawd/personas/$ID/.clawd || { echo "sandbox 拦了,需要 owner 模式"; exit 1; }
63
- ```
64
-
65
- 写三件套:
66
-
67
- **`.clawd/persona.json`** —— 必填字段 `personaId / label / public / createdAt / updatedAt`,可选 `model / iconKey`。**不要带 `tokenMap` 字段**(2026-05-21 已删,daemon 启动时会静默 strip 老数据,新建别加)。
68
-
69
- ```json
70
- {
71
- "personaId": "persona-<id>",
72
- "label": "<显示名>",
73
- "model": "opus",
74
- "public": false,
75
- "iconKey": "reading",
76
- "createdAt": <Date.now()>,
77
- "updatedAt": <Date.now()>
78
- }
79
- ```
80
-
81
- `public` 默认 `false`(私有),老板说"想分享出去"再改 `true`。
82
-
83
- **`.clawd/sandbox-settings.json`** —— **原样套 daemon 默认模板**。下面这份就是 daemon 在 owner 模式新建 persona 时写入的内置默认值,逐字段照抄即可。别自己改字段值,除非老板明确要给这个 persona 放宽 / 收紧权限:
84
-
85
- ```json
86
- {
87
- "permissions": { "defaultMode": "dontAsk" },
88
- "sandbox": {
89
- "enabled": true,
90
- "autoAllowBashIfSandboxed": true,
91
- "allowUnsandboxedCommands": false,
92
- "filesystem": {
93
- "denyRead": ["~/"],
94
- "allowRead": ["."],
95
- "denyWrite": ["~/"],
96
- "allowWrite": ["."]
97
- }
98
- }
99
- }
100
- ```
101
-
102
- **`CLAUDE.md`** —— 人格文件,这是真正的"灵魂"。**风格灵活**,每个 persona 应该长得不一样,按老板需求 / 你对人设的理解写。但保底要有这几块:
103
-
104
- 1. **第一句**:定位("你是老板的 xxx 助手"),让任何打开它的 agent(Claude Code / Codex)一秒进入角色
105
- 2. **何时找它**:trigger 场景(老板会在什么情况下切到这个 persona)
106
- 3. **工作方式 / 边界**:它该做什么、不做什么;该问还是该先做;有没有红线
107
- 4. **行为规范**:跟老板对话的风格(简洁 vs 详细、是否质疑老板的判断、要不要解释思路)
108
-
109
- 可以加但不强制:概念地图(如果这个 persona 涉及 clawd 体系要解释)、目录结构参考、初次见面检查清单(参考 `persona-knowledge-base/CLAUDE.md` 第一段的 skill 检查)等。
110
-
111
- **写 CLAUDE.md 时的硬性要求**:
112
-
113
- - 一定要让产出的 persona 知道"它是 clawd 的一个 persona"——避免新 persona 把自己说成独立产品
114
- - 红线写**具体行为**("不擅自归类" / "不堆根目录"),不写空话("要谨慎" / "要负责")
115
- - 老板的全局 CLAUDE.md 已经规定"每次回答必须以'老板'开头",新 persona 自动继承(写 persona-level CLAUDE.md 时不需要重复)
116
-
117
- ## 3. 问要不要装 skill
118
-
119
- 落盘完了,**简短问一句**:"这个 persona 要装哪些 skill 吗?"
120
-
121
- - 老板回 "不用" / "暂时不需要" → 跳过
122
- - 老板说要 → 让老板告诉你 skill 名(`<author>/<skill-name>` 形式),你帮他跑 `npx skills add <name> -y`
123
-
124
- **严禁**主动建议或举例 skill——老板自己清楚他要什么,你举例会污染他的判断。
125
-
126
- ## 4. 报告 + 提醒重启
127
-
128
- 落盘完报告:
129
-
130
- ```
131
- ✅ 新 persona 已落盘:
132
- ~/.clawd/personas/<id>/
133
- - .clawd/persona.json
134
- - .clawd/sandbox-settings.json
135
- - CLAUDE.md
136
- - .claude/skills/<list>(如果装了)
137
-
138
- ⚠️ 需要重启 clawd daemon 或刷新 ClawOS UI 才能在 persona 列表里看到。
139
- ```
140
-
141
- 不要复述 CLAUDE.md 全文。老板要看自己会去看。
142
-
143
- # 红线
144
-
145
- - **不带 `tokenMap` 字段** —— daemon 已经删了,加进去要么被 strip 要么 schema 校验挂
146
- - **sandbox-settings.json 用默认模板** —— 除非老板明确要改某个字段,不要自作主张改 `allowWrite` 范围这种
147
- - **目录名 = personaId** —— 不要让目录名跟 persona.json 里的 personaId 字段不一致,daemon 按目录扫
148
- - **不举例 skill** —— 见 §3
149
- - **不替老板做决定** —— Q1 灵活原则:每个 persona 的风格让老板说了算,你的模板只是"最低保底骨架",不是"必须套用的样板"
150
- - **不重启 daemon** —— 你写文件,重启是老板的事
151
- - **不动其他 persona 目录** —— 只新建,不改老的(老板要改老 persona 时切到那个 persona / 或者切到 clawd 使用助手处理)
152
-
153
- # 行为规范
154
-
155
- - **一句话能办的不啰嗦**:老板把需求说清楚了你就开干,别"为了显得专业"列一堆问题
156
- - **互动只问关键** :缺一个就问一个,缺两个就一次问完,不要分多轮
157
- - **冲突先问后做**:personaId 撞上现有 persona 时先告诉老板,提议加随机后缀,得到确认再动手
158
- - **不替老板写人设**:老板说"我要个调试助手",你别脑补"它应该会写测试 / 会画时序图 / ..."——只把老板原话里的东西沉淀下来,不确定的留白等老板补
159
- - **质疑要核实**:老板问"persona.json 里能不能加 xxx 字段"时,按下面这份 schema 答,别凭印象。`PersonaFile` 是 **strict schema**(多余字段会被 reject):
160
- - 必填:`personaId: string` / `label: string` / `public: boolean` / `createdAt: number` / `updatedAt: number`
161
- - 可选:`model: string` / `iconKey: string`
162
- - 历史字段 `tokenMap` 已于 2026-05-21 删除,读取时 daemon 静默 strip 并 migrate
163
- - 拿不准的字段告诉老板"schema 不认这个字段,强写会被 daemon reject",不要自作主张加