@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.
- package/dist/agent-ability/controller.js +6 -2
- package/dist/agent-ability/resolver.js +6 -0
- package/dist/agent-ability/runtime-context.js +7 -1
- package/dist/agent-ability/runtime.js +2 -3
- package/dist/atomic-private-write.js +54 -1
- package/dist/automatic-install-target.js +40 -11
- package/dist/console.js +9 -0
- package/dist/control-plane-url.js +2 -2
- package/dist/daemon-migration-controller.js +198 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/directory-projection-identity.js +32 -0
- package/dist/directory-projection.js +922 -0
- package/dist/execution-protocol.js +78 -11
- package/dist/execution-runner.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/local-execution-prompt.js +57 -0
- package/dist/local-executor.js +99 -40
- package/dist/machine-info.js +45 -9
- package/dist/normalize.js +5 -0
- package/dist/profile-layout.js +41 -0
- package/dist/project-skills/controller.js +74 -14
- package/dist/project-skills/execution-adapter.js +11 -0
- package/dist/project-skills/initialized-reconciler.js +20 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +153 -0
- package/dist/project-skills/projection-state-store.js +841 -0
- package/dist/project-skills/projection-state-transaction.js +318 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/reconciler.js +299 -68
- package/dist/project-skills/runtime-warning.js +6 -0
- package/dist/project-skills/scanner.js +30 -1
- package/dist/project-skills/types.js +9 -0
- package/dist/project-workspaces/resolver.js +179 -0
- package/dist/project-workspaces/types.js +1 -0
- package/dist/prompt.js +64 -5
- package/dist/runner.js +1 -0
- package/dist/runtimes/claude.js +235 -4
- package/dist/runtimes/codex-app-server-runner.js +92 -23
- package/dist/runtimes/codex-contract.js +123 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/serve.js +31 -17
- package/dist/session.js +3 -0
- package/dist/supervised-runtime.js +12 -4
- package/dist/workspace.js +14 -5
- 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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
args.push("--add-dir",
|
|
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");
|