@nowcrew/daemon 0.6.15 → 0.6.17

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 (46) hide show
  1. package/dist/agent-ability/controller.js +6 -2
  2. package/dist/agent-ability/resolver.js +6 -0
  3. package/dist/agent-ability/runtime-context.js +7 -1
  4. package/dist/agent-ability/runtime.js +2 -3
  5. package/dist/atomic-private-write.js +54 -1
  6. package/dist/automatic-install-target.js +40 -11
  7. package/dist/console.js +9 -0
  8. package/dist/control-plane-url.js +2 -2
  9. package/dist/daemon-migration-controller.js +198 -0
  10. package/dist/daemon-migration-wiring.js +22 -0
  11. package/dist/daemon-update-eligibility.js +1 -1
  12. package/dist/directory-projection-identity.js +32 -0
  13. package/dist/directory-projection.js +922 -0
  14. package/dist/execution-protocol.js +78 -11
  15. package/dist/execution-runner.js +50 -2
  16. package/dist/i18n.js +1 -0
  17. package/dist/local-execution-prompt.js +57 -0
  18. package/dist/local-executor.js +99 -40
  19. package/dist/machine-info.js +45 -9
  20. package/dist/normalize.js +5 -0
  21. package/dist/profile-layout.js +41 -0
  22. package/dist/project-skills/controller.js +74 -14
  23. package/dist/project-skills/execution-adapter.js +11 -0
  24. package/dist/project-skills/initialized-reconciler.js +20 -0
  25. package/dist/project-skills/projection-set-switch.js +419 -0
  26. package/dist/project-skills/projection-state-domain.js +153 -0
  27. package/dist/project-skills/projection-state-store.js +841 -0
  28. package/dist/project-skills/projection-state-transaction.js +318 -0
  29. package/dist/project-skills/projection-state.js +3 -0
  30. package/dist/project-skills/reconciler.js +299 -68
  31. package/dist/project-skills/runtime-warning.js +6 -0
  32. package/dist/project-skills/scanner.js +30 -1
  33. package/dist/project-skills/types.js +9 -0
  34. package/dist/project-workspaces/resolver.js +179 -0
  35. package/dist/project-workspaces/types.js +1 -0
  36. package/dist/prompt.js +64 -5
  37. package/dist/runner.js +1 -0
  38. package/dist/runtimes/claude.js +235 -4
  39. package/dist/runtimes/codex-app-server-runner.js +92 -23
  40. package/dist/runtimes/codex-contract.js +123 -0
  41. package/dist/runtimes/codex.js +2 -0
  42. package/dist/serve.js +31 -17
  43. package/dist/session.js +3 -0
  44. package/dist/supervised-runtime.js +12 -4
  45. package/dist/workspace.js +14 -5
  46. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import { constants } from "node:fs";
2
+ import { access, lstat, realpath, stat } from "node:fs/promises";
3
+ import { posix, win32 } from "node:path";
4
+ const UNAVAILABLE_CODE = "project_context_unavailable";
5
+ const WINDOWS_EXTENDED_PREFIX = "\\\\?\\";
6
+ const WINDOWS_EXTENDED_UNC_PREFIX = "\\\\?\\UNC\\";
7
+ const WINDOWS_DEVICE_PREFIX = "\\\\.\\";
8
+ const WINDOWS_DRIVE_ABSOLUTE = /^[A-Za-z]:\\/u;
9
+ export class ProjectContextUnavailableError extends Error {
10
+ projectId;
11
+ code = UNAVAILABLE_CODE;
12
+ constructor(projectId) {
13
+ super(UNAVAILABLE_CODE);
14
+ this.projectId = projectId;
15
+ Object.defineProperty(this, "name", {
16
+ configurable: true,
17
+ value: "ProjectContextUnavailableError",
18
+ });
19
+ }
20
+ toJSON() {
21
+ return { projectId: this.projectId, code: this.code };
22
+ }
23
+ }
24
+ const compareProjectIds = (left, right) => {
25
+ if (left < right)
26
+ return -1;
27
+ if (left > right)
28
+ return 1;
29
+ return 0;
30
+ };
31
+ const warningFor = (projectId) => Object.freeze({
32
+ projectId,
33
+ code: UNAVAILABLE_CODE,
34
+ });
35
+ const nodeFilesystem = { lstat, stat, access, realpath };
36
+ /**
37
+ * Shape/access preflight only. In particular, fs.access cannot prove effective Windows ACL access;
38
+ * process launch remains authoritative and Windows capability enablement is owned outside this module.
39
+ */
40
+ const preflightDirectory = async (root, filesystem) => {
41
+ try {
42
+ const entry = await filesystem.lstat(root);
43
+ if (!entry.isDirectory()) {
44
+ if (!entry.isSymbolicLink())
45
+ return false;
46
+ if (!(await filesystem.stat(root)).isDirectory())
47
+ return false;
48
+ }
49
+ await filesystem.access(root, constants.R_OK | constants.X_OK);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ };
56
+ const registryByProjectId = (registrations) => new Map(registrations.map((registration) => [registration.projectId, registration]));
57
+ const hasUncServerAndShare = (root, prefixLength) => {
58
+ const [server, share] = root.slice(prefixLength).split("\\");
59
+ return server !== undefined && server.length > 0 && share !== undefined && share.length > 0;
60
+ };
61
+ const normalizeWindowsRoot = (root) => {
62
+ const normalized = win32.normalize(root);
63
+ const lower = normalized.toLowerCase();
64
+ const extendedPrefix = WINDOWS_EXTENDED_PREFIX.toLowerCase();
65
+ const extendedUncPrefix = WINDOWS_EXTENDED_UNC_PREFIX.toLowerCase();
66
+ if (normalized.startsWith(WINDOWS_DEVICE_PREFIX))
67
+ return null;
68
+ if (lower.startsWith(extendedUncPrefix)) {
69
+ return hasUncServerAndShare(normalized, WINDOWS_EXTENDED_UNC_PREFIX.length)
70
+ ? normalized
71
+ : null;
72
+ }
73
+ if (lower.startsWith(extendedPrefix)) {
74
+ return WINDOWS_DRIVE_ABSOLUTE.test(normalized.slice(WINDOWS_EXTENDED_PREFIX.length))
75
+ ? normalized
76
+ : null;
77
+ }
78
+ if (WINDOWS_DRIVE_ABSOLUTE.test(normalized))
79
+ return win32.resolve(normalized);
80
+ if (!normalized.startsWith("\\\\"))
81
+ return null;
82
+ return hasUncServerAndShare(normalized, 2) ? win32.resolve(normalized) : null;
83
+ };
84
+ const normalizeRegisteredRoot = (root, platform) => {
85
+ if (root.length === 0)
86
+ return null;
87
+ if (platform === "win32")
88
+ return normalizeWindowsRoot(root);
89
+ return posix.isAbsolute(root) ? posix.resolve(root) : null;
90
+ };
91
+ const rootIdentity = (root, platform) => {
92
+ if (platform !== "win32")
93
+ return root;
94
+ const lower = root.toLowerCase();
95
+ if (lower.startsWith(WINDOWS_EXTENDED_UNC_PREFIX.toLowerCase())) {
96
+ return win32.normalize(`\\\\${root.slice(WINDOWS_EXTENDED_UNC_PREFIX.length)}`).toLowerCase();
97
+ }
98
+ if (lower.startsWith(WINDOWS_EXTENDED_PREFIX.toLowerCase())) {
99
+ return win32.normalize(root.slice(WINDOWS_EXTENDED_PREFIX.length)).toLowerCase();
100
+ }
101
+ return root.toLowerCase();
102
+ };
103
+ export async function resolveProjectContext(snapshot, registry, options = {}) {
104
+ if (snapshot.projectIds.length === 0 && snapshot.primaryProjectId === undefined) {
105
+ return Object.freeze({
106
+ secondary: Object.freeze([]),
107
+ warnings: Object.freeze([]),
108
+ });
109
+ }
110
+ const platform = options.platform ?? process.platform;
111
+ const filesystem = options.filesystem ?? nodeFilesystem;
112
+ const sortedProjectIds = [...snapshot.projectIds].sort(compareProjectIds);
113
+ const primaryProjectId = snapshot.primaryProjectId;
114
+ let registrations;
115
+ try {
116
+ registrations = await registry.list();
117
+ }
118
+ catch {
119
+ registrations = Object.freeze([]);
120
+ }
121
+ const localProjects = registryByProjectId(registrations);
122
+ const seenRoots = new Set();
123
+ const resolveOne = async (projectId) => {
124
+ const registration = localProjects.get(projectId);
125
+ if (registration === undefined)
126
+ return { status: "unavailable" };
127
+ const root = normalizeRegisteredRoot(registration.root, platform);
128
+ if (root === null || !(await preflightDirectory(root, filesystem))) {
129
+ return { status: "unavailable" };
130
+ }
131
+ let physicalRoot;
132
+ try {
133
+ physicalRoot = await filesystem.realpath(root);
134
+ }
135
+ catch {
136
+ return { status: "unavailable" };
137
+ }
138
+ const normalizedPhysicalRoot = normalizeRegisteredRoot(physicalRoot, platform);
139
+ if (normalizedPhysicalRoot === null)
140
+ return { status: "unavailable" };
141
+ const identity = rootIdentity(normalizedPhysicalRoot, platform);
142
+ if (seenRoots.has(identity))
143
+ return { status: "duplicate" };
144
+ seenRoots.add(identity);
145
+ return {
146
+ status: "resolved",
147
+ project: Object.freeze({ projectId, root }),
148
+ };
149
+ };
150
+ let primary;
151
+ if (primaryProjectId !== undefined) {
152
+ if (!sortedProjectIds.includes(primaryProjectId)) {
153
+ throw new ProjectContextUnavailableError(primaryProjectId);
154
+ }
155
+ const resolvedPrimary = await resolveOne(primaryProjectId);
156
+ if (resolvedPrimary.status !== "resolved") {
157
+ throw new ProjectContextUnavailableError(primaryProjectId);
158
+ }
159
+ primary = resolvedPrimary.project;
160
+ }
161
+ const secondary = [];
162
+ const warnings = [];
163
+ for (const projectId of sortedProjectIds) {
164
+ if (projectId === primaryProjectId)
165
+ continue;
166
+ const resolved = await resolveOne(projectId);
167
+ if (resolved.status === "unavailable")
168
+ warnings.push(warningFor(projectId));
169
+ else if (resolved.status === "resolved")
170
+ secondary.push(resolved.project);
171
+ }
172
+ const base = {
173
+ secondary: Object.freeze(secondary),
174
+ warnings: Object.freeze(warnings),
175
+ };
176
+ return primary === undefined
177
+ ? Object.freeze(base)
178
+ : Object.freeze({ primary, ...base });
179
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/prompt.js CHANGED
@@ -20,6 +20,46 @@ const EXTERNAL_RESULT_READABILITY = `
20
20
  - 有足够重点时,只加粗三到五处真正决定性的数字、最终状态、风险、截止时间、负责人或行动项;不足三处时宁缺毋滥。加粗范围要短,不要整段加粗,不要把每个数字都加粗,不得强化未经验证的判断。
21
21
  - 需要颜色层级时,只在确有对应语义的短加粗重点前使用一个彩色圆点:🟢 绿色圆点表示已验证成功或健康,🟠 橙色圆点表示风险、临期或需要关注,🔴 红色圆点表示失败、阻塞或严重异常,🔵 蓝色圆点表示负责人、行动项或关键中性数据。每个短加粗重点前最多放一个;不要给普通段落、标题或每个要点都加标记。
22
22
  - 使用标题、列表、引用、彩色圆点和加粗形成在企微原生流式消息中稳定可见的层级;不得输出 \`<font>\` 或其它 HTML 标色标签。`;
23
+ const compareProjectIds = (left, right) => {
24
+ if (left < right)
25
+ return -1;
26
+ if (left > right)
27
+ return 1;
28
+ return 0;
29
+ };
30
+ const JSON_SINGLE_LINE_ESCAPE = /[\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu;
31
+ const escapeJsonSingleLineControls = (json) => json.replace(JSON_SINGLE_LINE_ESCAPE, (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`);
32
+ const canonicalJsonString = (value) => escapeJsonSingleLineControls(JSON.stringify(value));
33
+ const canonicalJsonRecord = (value) => escapeJsonSingleLineControls(JSON.stringify(value));
34
+ /**
35
+ * Resolved paths are daemon-local facts. JSON rows keep paths with spaces or Unicode as data,
36
+ * never as shell fragments; warning rows expose only logical IDs and stable codes, never roots.
37
+ */
38
+ export function buildRuntimeWorkspacePrompt(projectContext) {
39
+ if (projectContext?.primary === undefined)
40
+ return "";
41
+ const projectRoles = [
42
+ { role: "primary", ...projectContext.primary },
43
+ ...[...projectContext.secondary]
44
+ .sort((left, right) => compareProjectIds(left.projectId, right.projectId))
45
+ .map((project) => ({ role: "secondary", ...project })),
46
+ ].map(({ role, projectId, root }) => `- ${canonicalJsonRecord({ role, projectId, root })}`);
47
+ const projectWarnings = [...projectContext.warnings]
48
+ .sort((left, right) => compareProjectIds(left.projectId, right.projectId)
49
+ || compareProjectIds(left.code, right.code))
50
+ .map(({ projectId, code }) => `- ${canonicalJsonRecord({ projectId, code })}`);
51
+ const warningsBlock = projectWarnings.length === 0
52
+ ? ""
53
+ : `\n\n## Daemon-local project warnings\n${projectWarnings.join("\n")}`;
54
+ return `\n\n## Runtime workspace boundaries
55
+ Repository cwd: ${canonicalJsonString(projectContext.primary.root)}. Use it for source, builds, tests, and Git operations.
56
+ Task state directory: $CREW_TASK_DIR. Put downloads, diagnostics, drafts, reports, and temporary artifacts there.
57
+ Work log: $CREW_TASK_LOG. Do not treat the repository as NowWork task storage.
58
+ Runtime permission and sandbox rules still govern repository writes; project binding grants no additional write access.
59
+
60
+ ## Daemon-local project roles
61
+ ${projectRoles.join("\n")}${warningsBlock}`;
62
+ }
23
63
  export function capWorkLogForInject(workLog, cap = WORKLOG_INJECT_CAP) {
24
64
  if (workLog.length <= cap)
25
65
  return workLog;
@@ -89,12 +129,16 @@ export function buildSystemPrompt(ctx) {
89
129
  // scheduled 换成静默版表述(2026-07-13 复审 Minor 5)。
90
130
  const threadTaskRule = scheduled
91
131
  ? "\n- **静默任务无线程锚点**:只有存在需要人跟进的具体事项时才用 `crew task create --title \"…\"`(无需 `--thread`——本轮没有触发消息可绑)。"
92
- : `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。若这件事需要创建 task,只建一个,且必须把它绑到当前线程:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`;
132
+ : ctx.wakeMessageId
133
+ ? `\n- **线程 = 工作单元 / 一个请求一个 task(CRITICAL)**:一条对话(thread)对应一件事,最多绑**一个** task。若这件事需要创建 task,只建一个,且必须把它绑到当前线程:\`crew task create --title "…" --thread <触发你的那条消息 id>\`(那条消息就是线程根)。**一定要带 \`--thread\`**——不带会另起一条飘在顶层的新线程,task 就和你的讨论分家了(这正是要避免的)。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。当前线程已有 task 时再建会被服务端拒绝(报错会提示你)。`
134
+ : `\n- **无线程锚点 / 一个请求一个 task(CRITICAL)**:当前来信位于频道顶层,没有可复用的当前线程。若这件事需要创建 task,只建一个,使用 \`crew task create --channel ${ctx.channelId} --new-thread --title "…"\` 创建并绑定新线程;之后以命令返回的线程根作为这件事的唯一线程。**别把一个请求拆成多个 task**(如"拉代码"+"读文档"+"写记忆"是同一件事 → 一个 task,用 todo/进度推进,不要建第二个)。`;
93
135
  const progressRule = alwaysReport
94
136
  ? "\n- **报告交由 daemon 投递**:只返回一个完整最终报告,不要调用 `crew message send`;本轮没有线程锚点。"
95
137
  : scheduled
96
138
  ? "\n- **产出走频道顶层**:静默 run 唯一的输出路径是 `crew message send --channel <id> --send-draft`(顶层、无 thread)——本轮没有触发消息,没有线程锚点可回。"
97
- : "\n- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 `crew message send --thread <当前线程根>` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**";
139
+ : ctx.wakeMessageId
140
+ ? "\n- **进度/产出回本线程**:这件事的认领/进度/完成汇报都用 `crew message send --thread <当前线程根>` 回复在**这个线程里**(线程根 = 触发你的那条消息 id,即 \$CREW_WAKE_MESSAGE_ID)。**严禁把进度发到别的线程或频道顶层。**"
141
+ : `\n- **无线程锚点时的回复位置**:不创建 task 的直接答复用 \`crew message send --channel ${ctx.channelId}\` 回复频道顶层;创建 task 后,这件事的认领/进度/完成汇报都回复到命令返回的新线程根。不得虚构线程 id。`;
98
142
  const scheduleIntentRule = scheduled ? "" : `
99
143
 
100
144
  ## 定时任务创建与管理
@@ -115,12 +159,15 @@ export function buildSystemPrompt(ctx) {
115
159
  const actionRule = scheduled
116
160
  ? `- **判定规则**:直接执行本轮定时指令,执行前不需要 claim。${threadTaskRule}${progressRule}`
117
161
  : `- **判定规则**:定时任务创建与管理和 memory_prune 无需 task claim,只遵守各自唤醒指令;其它来信若需要你"回复之外的动作"(跑工具/改代码/做变更),先 claim;若只是回答问题或闲聊,无需 claim。${threadTaskRule}${progressRule}`;
162
+ const interactiveTaskCreateCommand = ctx.wakeMessageId
163
+ ? `6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。`
164
+ : `6. **\`crew task create --channel <id> --new-thread --title "<标题>"\`** —— 当前没有线程锚点时,新建任务并绑定到命令返回的新线程;后续回复使用返回的完整线程根消息 id。`;
118
165
  const taskAndScheduleCommands = alwaysReport
119
166
  ? `4. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
120
167
  : scheduled
121
168
  ? `5. **\`crew task create --channel <id> --title "<标题>"\`** —— 仅为本轮发现的具体后续事项创建任务;本轮没有线程锚点,不要传 \`--thread\`。`
122
169
  : `5. **\`crew task list --channel <id>\`** —— 看任务板。支持 \`--status <s>\` / \`--mine\`。
123
- 6. **\`crew task create --channel <id> --title "<标题>" --thread <当前线程根msgId>\`** —— 新建任务并**绑定到当前线程**。\`--thread\` 传你读到的那条**触发消息 id**(线程根),任务就和讨论同处一个线程。省略 \`--thread\` 会另起新线程,**几乎总是错的——务必带上**。
170
+ ${interactiveTaskCreateCommand}
124
171
  7. **\`crew task claim <taskId>\`** —— 除定时任务创建与管理和 memory_prune 外,执行需要动作的工作前先认领任务。
125
172
  8. **\`crew task update <taskId> --status <in_progress|in_review|done>\`** —— 推进任务状态。
126
173
  9. **\`crew task unclaim <taskId>\`** —— 释放认领,把任务让给别人。
@@ -129,6 +176,14 @@ export function buildSystemPrompt(ctx) {
129
176
  12. **\`crew schedule list --channel <id> --agent <handle> [--json]\`** —— 查询本频道指定 agent 的定时任务,\`--json\` 返回完整字段。
130
177
  13. **\`crew schedule update <jobId> ...\`** —— 修改定时任务标题、指令、时间或输出策略。
131
178
  14. **\`crew schedule pause|resume|cancel|run-now <jobId>\`** —— 控制定时任务。`;
179
+ const memoryTaskReadRule = scheduled
180
+ ? ""
181
+ : ctx.wakeMessageId
182
+ ? `- 普通交互执行将写共享记忆前,先运行 crew thread read --thread "$CREW_WAKE_MESSAGE_ID"。crew thread read 返回的顶层 task 是当前线程任务的权威事实;不得把 parent.task 或同频道其它 task 当成当前线程任务。顶层 task 为 null 时按当前线程没有 task 处理。`
183
+ : `- 普通交互执行将写共享记忆前,先运行 crew message read --channel ${ctx.channelId}。当前没有线程锚点时不得运行 crew thread read;需要普通 task 时先用 crew task create --channel ${ctx.channelId} --new-thread --title "…" 创建新线程,再对返回的线程根运行 crew thread read --thread <线程根> 并以其顶层 task 为权威事实。不得把同频道其它 task 当成当前任务。`;
184
+ const messageThreadUsage = ctx.wakeMessageId
185
+ ? "被唤醒时优先使用环境变量 `$CREW_WAKE_MESSAGE_ID` 或唤醒提示里的完整 id,不要手动截短。"
186
+ : "当前没有线程锚点:频道顶层回复只带 `--channel`;创建 task 后再使用命令返回的完整线程根消息 id。";
132
187
  const communicationSection = alwaysReport
133
188
  ? `## 控制面工具 —— crew CLI
134
189
  需要读取频道或操作任务时使用 crew CLI;最终报告不得通过消息命令发送。可使用:
@@ -142,7 +197,7 @@ ${taskAndScheduleCommands}`
142
197
  2. **\`crew message read --channel <id>\`** —— 读频道历史(读取即自动推进你的已读/新鲜度游标)。支持 \`--after <seq>\` / \`--limit <n>\`。
143
198
  3. **\`crew message check --channel <id>\`** —— 非阻塞查看未读数。工作中可在自然断点随时用。
144
199
  4. **\`crew message send --channel <id>\`** —— 发消息。${messageBodyInput}
145
- 也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。被唤醒时优先使用环境变量 \`$CREW_WAKE_MESSAGE_ID\` 或唤醒提示里的完整 id,不要手动截短。
200
+ 也可用 \`--content "<短正文>"\`。线程内回复:加 \`--thread <完整线程根消息 id>\`。${messageThreadUsage}
146
201
  ${taskAndScheduleCommands}`;
147
202
  const freshnessRule = alwaysReport
148
203
  ? ""
@@ -162,8 +217,11 @@ ${taskAndScheduleCommands}`;
162
217
  : `
163
218
  - **本轮是 NowWork 内部唤醒**:普通 \`crew message send\` 只写入 NowWork。内部唤醒不得使用 \`--reply-origin\`,该参数只回答直接触发本轮的企微原消息。
164
219
  - **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。${EXTERNAL_RESULT_READABILITY}`;
220
+ const newThreadRule = ctx.wakeMessageId
221
+ ? "- **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 `crew task create --new-thread --title \"…\"`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。"
222
+ : `- **当前没有线程锚点**:需要普通 task 时用 \`crew task create --channel ${ctx.channelId} --new-thread --title "…"\` 创建根线程;之后这件事的回复都发到返回的新线程里。`;
165
223
  const interactiveTaskRules = scheduled ? "" : `
166
- - **毫不相关的新任务才另起线程**:只有要处理的事**和当前线程毫不相关**(或用户明确要求新建)时,才用 \`crew task create --new-thread --title "…"\`——系统另起一个子线程(parent=当前线程)绑新 task;之后这件事的回复要发到**这个新子线程**里。能不拆就不拆。
224
+ ${newThreadRule}
167
225
  - 任务状态流:\`todo → in_progress → in_review → done\`。claim 后用 \`crew task update\` 推进:开工→in_progress、完成待验收→in_review、人类确认后→done。只有 assignee 能改自己任务的状态。
168
226
  - **交接(handoff)**:当你这一环干完、需要别的角色接手时(如开发完成 → 交给 QA 测试),用 \`crew task assign <taskId> --to <下家handle>\` 把任务交接出去,并在线程里给下家足够背景(分支名 / 改动摘要 / 测试建议)。交接后对方会被自动唤醒。**别让任务停在你手里无人跟进**。
169
227
  - **分诊(若你是总管)**:若你收到「【分诊请求】」唤醒,说明频道里有一个无人认领的任务需要你按团队职责分派。唤醒内容里已附上团队成员及其职责:判断谁最合适,用 \`crew task assign <taskId> --to <handle>\` 指派给他(若该你自己做就 \`crew task claim\`);确实没人合适时,在频道里 @发起人 说明并给建议,**不要让任务悬空**。`;
@@ -237,6 +295,7 @@ ${taskAndScheduleCommands}`;
237
295
  - **普通任务默认只写两处**:① 本任务的隔离 cwd(代码/草稿);② 本任务的工作日志 **\`$CREW_TASK_LOG\`**(每任务一份,别人不会碰)。默认不要修改共享 \`$CREW_HOME/MEMORY.md\` 或 notes/;只有当前用户明确要求更新或记住长期记忆,或明确表达具体且跨后续任务持续生效的规则、偏好或约束时,才按下方条件写入相关共享文件。在工作日志里记:当前阶段 / 下一步 / 卡在谁 / 关键结论(每条尽量 ≤3 行)。
238
296
  - **当前请求明确要求更新或记住长期记忆,明确表达需要跨后续任务持续生效的具体规则、偏好或约束,或当前消息是对本线程紧邻记忆方案的无歧义确认时**,可以按最小范围直接写共享记忆。例如“以后所有测试报告统一按五部分写”属于明确长期意图,即使没有使用“记忆”一词。先读取相关索引和 note,语义已存在时不重复追加,不要把 Agent 自己判断值得记住视为用户授权。
239
297
  - 本节的直接更新共享记忆和相关 task 流转仅适用于用户消息触发的普通交互执行。memory_prune 和 scheduled 执行只遵守各自唤醒指令,不得依据本节认领、创建、重开或推进 task。
298
+ ${memoryTaskReadRule}
240
299
  - 普通交互执行将写共享记忆时,使用当前线程唯一的普通 task:已有非终态 task 时先按归属规则确保当前 Agent 成功认领;当前线程没有 task 时先创建并认领一个普通 task;已有 task 为 done/closed 时不得由 Agent 重开,done 报告当前轻量方案无法安全复用终态 task,closed 保持人工终态。遇到终态或归属问题时报告冲突且不得创建第二个 task 或新的任务类型。完成写入后,只有任务本身的全部工作已完成时才推进终态以触发 prune。
241
300
  - 提出共享记忆修改方案并等待用户确认时,该 task 仍有剩余工作,保持 in_review 或其它非终态;收到无歧义确认后再执行写入,并仅在全部工作完成后推进终态。
242
301
  - 普通执行直接更新后,仍然保留任务结束后的专门收尾执行,不因已经写过而跳过 prune。
package/dist/runner.js CHANGED
@@ -46,6 +46,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
46
46
  systemPrompt: ({ workspace, resuming }) => buildSystemPrompt({
47
47
  handle: input.handle,
48
48
  channelId: input.channelId,
49
+ ...(input.wakeMessageId ? { wakeMessageId: input.wakeMessageId } : {}),
49
50
  agentId: credential.agentId,
50
51
  homeDir: workspace.dir,
51
52
  productName: config.productName,
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Claude Code runtime 适配:print + stream-json 模式,headless 驱动。
3
3
  */
4
+ import { realpath } from "node:fs/promises";
5
+ import { posix, win32 } from "node:path";
4
6
  // cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
5
7
  import spawn from "cross-spawn";
6
8
  // Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
@@ -9,6 +11,237 @@ export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
9
11
  // 未配置/非法档位时的默认思考强度:medium 开启原生 thinking(终端透传要展示思考过程),
10
12
  // 又不至于 high/max 的 token 开销;agent 配置白名单档位可覆盖。
11
13
  export const CLAUDE_DEFAULT_EFFORT = "medium";
14
+ export const CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_MIN_VERSION = "2.1.237";
15
+ export const CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV = "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD";
16
+ const CLAUDE_VERSION_PROBE_TIMEOUT_MS = 1_000;
17
+ const CLAUDE_VERSION_PROBE_KILL_GRACE_MS = 250;
18
+ const CLAUDE_VERSION_PROBE_MAX_BUFFER = 8 * 1024;
19
+ const CLAUDE_VERSION_PROBE_SUCCESS_TTL_MS = 30_000;
20
+ const CLAUDE_REALPATH_TIMEOUT_MS = 250;
21
+ const ignoreTerminalProbeError = () => undefined;
22
+ const defaultVersionProbeSpawner = (file, args, options) => spawn(file, [...args], options);
23
+ const versionProbeCache = new Map();
24
+ const probeClaudeVersionOnce = (bin, env, start, dependencies) => {
25
+ // A .cmd launch creates an unmanaged cmd.exe process tree. Without a Job Object or another
26
+ // verifiable owner, a timeout cannot safely distinguish that tree from PID reuse. Fail closed:
27
+ // directory access remains available, but nested CLAUDE.md auto-loading stays unverified.
28
+ if ((dependencies.platform ?? process.platform) === "win32")
29
+ return Promise.resolve(null);
30
+ let child;
31
+ try {
32
+ child = start(bin, ["--version"], {
33
+ env,
34
+ stdio: ["ignore", "pipe", "pipe"],
35
+ windowsHide: true,
36
+ shell: false,
37
+ });
38
+ }
39
+ catch {
40
+ return Promise.resolve(null);
41
+ }
42
+ return new Promise((resolve) => {
43
+ let settled = false;
44
+ let timedOut = false;
45
+ let closed = false;
46
+ let output = Buffer.alloc(0);
47
+ let resolveClose;
48
+ const close = new Promise((closeResolve) => { resolveClose = closeResolve; });
49
+ const waitForClose = async () => {
50
+ let deadline;
51
+ try {
52
+ await Promise.race([
53
+ close,
54
+ new Promise((waitResolve) => {
55
+ deadline = setTimeout(waitResolve, CLAUDE_VERSION_PROBE_KILL_GRACE_MS);
56
+ }),
57
+ ]);
58
+ }
59
+ finally {
60
+ if (deadline !== undefined)
61
+ clearTimeout(deadline);
62
+ }
63
+ };
64
+ const append = (chunk) => {
65
+ if (output.length >= CLAUDE_VERSION_PROBE_MAX_BUFFER)
66
+ return;
67
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
68
+ const remaining = CLAUDE_VERSION_PROBE_MAX_BUFFER - output.length;
69
+ output = Buffer.concat([output, bytes.subarray(0, remaining)]);
70
+ };
71
+ child.stdout?.on("data", append);
72
+ child.stderr?.on("data", append);
73
+ const observeClose = () => {
74
+ if (closed)
75
+ return;
76
+ closed = true;
77
+ resolveClose();
78
+ };
79
+ const cleanup = () => {
80
+ child.stdout?.off("data", append);
81
+ child.stderr?.off("data", append);
82
+ child.off("error", onError);
83
+ child.off("close", onClose);
84
+ // ChildProcess can report a late error after its final close deadline. Retain one static sink:
85
+ // it captures no local state, creates no timer, and remains safe until the child is collected.
86
+ child.off("error", ignoreTerminalProbeError);
87
+ child.on("error", ignoreTerminalProbeError);
88
+ child.stdout?.destroy();
89
+ child.stderr?.destroy();
90
+ };
91
+ const settle = (value) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ clearTimeout(timeoutTimer);
96
+ cleanup();
97
+ resolve(value);
98
+ };
99
+ const onError = () => {
100
+ if (!timedOut)
101
+ settle(null);
102
+ };
103
+ const onClose = (code) => {
104
+ observeClose();
105
+ if (timedOut)
106
+ return;
107
+ const value = output.toString("utf8").trim();
108
+ settle(code === 0 && value !== "" ? value : null);
109
+ };
110
+ const timeoutTimer = setTimeout(() => {
111
+ timedOut = true;
112
+ void (async () => {
113
+ try {
114
+ child.kill("SIGTERM");
115
+ }
116
+ catch { /* process already exited */ }
117
+ await waitForClose();
118
+ if (!closed) {
119
+ try {
120
+ child.kill("SIGKILL");
121
+ }
122
+ catch { /* process already exited */ }
123
+ await waitForClose();
124
+ }
125
+ settle(null);
126
+ })();
127
+ }, CLAUDE_VERSION_PROBE_TIMEOUT_MS);
128
+ child.on("error", onError);
129
+ child.on("close", onClose);
130
+ });
131
+ };
132
+ /** Bounded local-only probe; errors and output never cross the execution protocol. */
133
+ export function probeClaudeVersion(bin, env, start = defaultVersionProbeSpawner, dependencies = {}) {
134
+ if (start !== defaultVersionProbeSpawner) {
135
+ return probeClaudeVersionOnce(bin, env, start, dependencies);
136
+ }
137
+ const key = JSON.stringify([dependencies.platform ?? process.platform, bin, env.PATH ?? ""]);
138
+ const now = Date.now();
139
+ const cached = versionProbeCache.get(key);
140
+ if (cached !== undefined && (cached.expiresAt === null || cached.expiresAt > now)) {
141
+ return cached.promise;
142
+ }
143
+ if (cached !== undefined)
144
+ versionProbeCache.delete(key);
145
+ const pending = probeClaudeVersionOnce(bin, env, start, dependencies);
146
+ const inFlight = { promise: pending, expiresAt: null };
147
+ versionProbeCache.set(key, inFlight);
148
+ void pending.then((version) => {
149
+ if (versionProbeCache.get(key) !== inFlight)
150
+ return;
151
+ if (version === null) {
152
+ versionProbeCache.delete(key);
153
+ return;
154
+ }
155
+ versionProbeCache.set(key, {
156
+ promise: Promise.resolve(version),
157
+ expiresAt: Date.now() + CLAUDE_VERSION_PROBE_SUCCESS_TTL_MS,
158
+ });
159
+ });
160
+ return pending;
161
+ }
162
+ const canonicalDirectoryIdentity = (directory, platform) => {
163
+ if (platform !== "win32")
164
+ return posix.normalize(directory);
165
+ let normalized = directory.replaceAll("/", "\\");
166
+ const lower = normalized.toLowerCase();
167
+ if (lower.startsWith("\\\\?\\unc\\"))
168
+ normalized = `\\\\${normalized.slice(8)}`;
169
+ else if (lower.startsWith("\\\\?\\"))
170
+ normalized = normalized.slice(4);
171
+ return win32.normalize(normalized).toLowerCase();
172
+ };
173
+ export function orderedUniqueClaudeDirectories(directories, excluded = [], platform = process.platform) {
174
+ const seen = new Set(excluded.map((directory) => canonicalDirectoryIdentity(directory, platform)));
175
+ const ordered = [];
176
+ for (const directory of directories) {
177
+ const identity = canonicalDirectoryIdentity(directory, platform);
178
+ if (seen.has(identity))
179
+ continue;
180
+ seen.add(identity);
181
+ ordered.push(directory);
182
+ }
183
+ return Object.freeze(ordered);
184
+ }
185
+ const canonicalizeWithinDeadline = async (directory, canonicalize) => {
186
+ let timeout;
187
+ try {
188
+ return await Promise.race([
189
+ Promise.resolve().then(() => canonicalize(directory)).catch(() => directory),
190
+ new Promise((resolve) => {
191
+ timeout = setTimeout(() => resolve(directory), CLAUDE_REALPATH_TIMEOUT_MS);
192
+ }),
193
+ ]);
194
+ }
195
+ finally {
196
+ if (timeout !== undefined)
197
+ clearTimeout(timeout);
198
+ }
199
+ };
200
+ /** Preserve caller order while collapsing physical aliases; inaccessible paths fall back to lexical identity. */
201
+ export async function resolveOrderedUniqueClaudeDirectories(directories, excluded = [], canonicalize = realpath, platform = process.platform) {
202
+ const identity = async (directory) => {
203
+ const physical = await canonicalizeWithinDeadline(directory, canonicalize);
204
+ return canonicalDirectoryIdentity(physical, platform);
205
+ };
206
+ const [excludedIdentities, identities] = await Promise.all([
207
+ Promise.all(excluded.map(identity)),
208
+ Promise.all(directories.map(identity)),
209
+ ]);
210
+ const seen = new Set(excludedIdentities);
211
+ const ordered = [];
212
+ for (let index = 0; index < directories.length; index += 1) {
213
+ const directoryIdentity = identities[index];
214
+ if (seen.has(directoryIdentity))
215
+ continue;
216
+ seen.add(directoryIdentity);
217
+ ordered.push(directories[index]);
218
+ }
219
+ return Object.freeze(ordered);
220
+ }
221
+ const parseVersion = (value) => {
222
+ const match = /(?:^|[^0-9])(\d+)\.(\d+)\.(\d+)(?:[^0-9]|$)/u.exec(value);
223
+ if (match === null)
224
+ return null;
225
+ const version = match.slice(1, 4).map(Number);
226
+ return version.every(Number.isSafeInteger)
227
+ ? [version[0], version[1], version[2]]
228
+ : null;
229
+ };
230
+ export function isClaudeAdditionalDirectoryInstructionsSupported(versionOutput) {
231
+ if (versionOutput === null)
232
+ return false;
233
+ const version = parseVersion(versionOutput);
234
+ const minimum = parseVersion(CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_MIN_VERSION);
235
+ if (version === null || minimum === null)
236
+ return false;
237
+ for (let index = 0; index < version.length; index += 1) {
238
+ if (version[index] > minimum[index])
239
+ return true;
240
+ if (version[index] < minimum[index])
241
+ return false;
242
+ }
243
+ return true;
244
+ }
12
245
  export function buildClaudeArgs(input) {
13
246
  const args = [
14
247
  "--print",
@@ -28,10 +261,8 @@ export function buildClaudeArgs(input) {
28
261
  if (input.sessionId) {
29
262
  args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
30
263
  }
31
- if (input.projectSkillsDirectory)
32
- args.push("--add-dir", input.projectSkillsDirectory);
33
- if (input.agentRootDirectory)
34
- args.push("--add-dir", input.agentRootDirectory);
264
+ for (const directory of input.additionalDirectories ?? [])
265
+ args.push("--add-dir", directory);
35
266
  if (input.effectivePermission === undefined) {
36
267
  if (input.dangerous)
37
268
  args.push("--dangerously-skip-permissions");