@nowcrew/daemon 0.5.33 → 0.5.35
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/README.md +46 -0
- package/dist/atomic-private-write.js +18 -0
- package/dist/computer-profile.js +3 -17
- package/dist/execution-protocol.js +7 -1
- package/dist/execution-runner.js +2 -0
- package/dist/local-executor.js +9 -2
- package/dist/machine-info.js +5 -1
- package/dist/project-skills/agent-projection-coordinator.js +10 -0
- package/dist/project-skills/controller.js +112 -0
- package/dist/project-skills/reconciler.js +116 -0
- package/dist/project-skills/registry.js +101 -0
- package/dist/project-skills/scanner.js +103 -0
- package/dist/project-skills/types.js +12 -0
- package/dist/promise-tail.js +25 -0
- package/dist/runner.js +4 -0
- package/dist/runtimes/claude.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -1
- package/dist/scheduled-report.js +3 -0
- package/dist/serve.js +95 -7
- package/dist/skill-frontmatter.js +23 -0
- package/dist/skills.js +2 -19
- package/dist/supervised-runtime.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,6 +166,52 @@ Old daemons that omit `executionRuntimes` are conservatively interpreted as the
|
|
|
166
166
|
CLIs and server-supported adapters. Unknown required protocol semantics are rejected before side effects.
|
|
167
167
|
Protocol-0 `agent:start` remains only for the server-governed compatibility window.
|
|
168
168
|
|
|
169
|
+
## Project Skills
|
|
170
|
+
|
|
171
|
+
On macOS and Linux, an owner or admin can register multiple local project repositories from a Computer
|
|
172
|
+
profile and bind selected Skills to individual Agents. The daemon keeps the only durable copy of each
|
|
173
|
+
absolute path in:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
<CREW_AGENTS_ROOT>/.crew/projects.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Each project is scanned at `<project>/.agents/skills/*/SKILL.md`. The server stores only a bounded,
|
|
180
|
+
path-free inventory and logical `(projectId, skillName)` bindings. The local path crosses the server only
|
|
181
|
+
while an add command is forwarded to the selected online Computer; it is not written to the workspace
|
|
182
|
+
database, structured logs, inventory frames, or responses. Project Skills are disabled on Windows.
|
|
183
|
+
|
|
184
|
+
One binding set is projected into both Runtime discovery layouts:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
<agentsRoot>/<handle>/.agents/skills/<name> # Codex symlink
|
|
188
|
+
<agentsRoot>/<handle>/.crew/claude-skills/.claude/skills/<name> # Claude symlink
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The two complete directories switch under one per-Agent reconcile/launch lock. A failed switch restores
|
|
192
|
+
the prior complete projection; a missing project or Skill removes only that link and does not block other
|
|
193
|
+
work. These are ordinary writable symlinks, not snapshots or a sandbox: an Agent running as the daemon
|
|
194
|
+
user can modify the source Skill through them. That write-through risk is intentionally accepted.
|
|
195
|
+
|
|
196
|
+
Codex protocol-v1 task directories are opaque
|
|
197
|
+
`<agentsRoot>/<handle>/tasks/<safe-prefix>-<sha256>` paths. The daemon writes `.nowwork-root` at the Agent
|
|
198
|
+
root and adds it to Codex `project_root_markers`, so upward discovery reaches `.agents/skills` without
|
|
199
|
+
changing the task cwd. Claude receives
|
|
200
|
+
`--add-dir <agentsRoot>/<handle>/.crew/claude-skills`; Claude treats those Skills as `projectSettings`,
|
|
201
|
+
so a machine policy that disables project settings also disables this source even when the projection is
|
|
202
|
+
healthy.
|
|
203
|
+
|
|
204
|
+
Existing user-level discovery is unchanged. Claude may still load `~/.claude/skills`; Codex may still
|
|
205
|
+
load `$CODEX_HOME/skills`. The DeepSeek Codex variant uses its Agent-private `.codex-deepseek` home.
|
|
206
|
+
Project bindings do not hide or rewrite any of those sources.
|
|
207
|
+
|
|
208
|
+
Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
|
|
209
|
+
complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
|
|
210
|
+
links left by offline unbinds. Every non-empty execution snapshot is also reconciled immediately before
|
|
211
|
+
Runtime launch, which recovers from a missed notification or daemon restart. Skill file content changes
|
|
212
|
+
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
213
|
+
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
214
|
+
|
|
169
215
|
## Reliability
|
|
170
216
|
|
|
171
217
|
Protocol-v1 lifecycle is `accepted → started → completed`, followed by a server
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
export async function atomicPrivateWrite(path, content, beforeCommit) {
|
|
5
|
+
const directory = dirname(path);
|
|
6
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
7
|
+
await chmod(directory, 0o700);
|
|
8
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
9
|
+
try {
|
|
10
|
+
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
11
|
+
await beforeCommit?.(temporary);
|
|
12
|
+
await rename(temporary, path);
|
|
13
|
+
await chmod(path, 0o600);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await rm(temporary, { force: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/computer-profile.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { access,
|
|
1
|
+
import { access, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { posix, resolve, win32 } from "node:path";
|
|
7
6
|
import { spawn } from "node:child_process";
|
|
8
7
|
import { z } from "zod";
|
|
9
8
|
import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
10
9
|
import { formatDaemonText } from "./i18n.js";
|
|
10
|
+
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
11
11
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
12
12
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
13
13
|
const url = new URL(value);
|
|
@@ -130,20 +130,6 @@ export function validateProfileName(name) {
|
|
|
130
130
|
export function profilePath(name, home = daemonHome()) {
|
|
131
131
|
return resolve(home, "profiles", `${validateProfileName(name)}.json`);
|
|
132
132
|
}
|
|
133
|
-
async function atomicPrivateWrite(path, content, beforeCommit) {
|
|
134
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
135
|
-
await chmod(dirname(path), 0o700);
|
|
136
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
137
|
-
try {
|
|
138
|
-
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
139
|
-
await beforeCommit?.(temporary);
|
|
140
|
-
await rename(temporary, path);
|
|
141
|
-
await chmod(path, 0o600);
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
await rm(temporary, { force: true });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
133
|
const DPAPI_PROTECT = "$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)";
|
|
148
134
|
const DPAPI_UNPROTECT = "$p=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
|
|
149
135
|
async function powershellStdin(script, input) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { MAX_AGENT_PROJECT_SKILL_BINDINGS } from "./project-skills/types.js";
|
|
2
3
|
const ExecutionIdSchema = z.string().uuid();
|
|
3
4
|
const ProtocolVersionSchema = z.literal(1);
|
|
4
5
|
const TimestampSchema = z.string().datetime({ offset: true });
|
|
@@ -10,13 +11,17 @@ const ExitCodeSchema = z.number().int().min(MIN_SIGNED_32_INTEGER).max(MAX_PG_IN
|
|
|
10
11
|
const RuntimeSchema = z.enum(["claude", "codex", "kimi"]);
|
|
11
12
|
const UnsafePathCharacterSchema = /[\p{Cc}<>:"/\\|?*]/u;
|
|
12
13
|
const WindowsReservedNameSchema = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
13
|
-
const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
|
|
14
|
+
export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => handle !== "."
|
|
14
15
|
&& handle !== ".."
|
|
15
16
|
&& !UnsafePathCharacterSchema.test(handle)
|
|
16
17
|
&& !WindowsReservedNameSchema.test(handle)
|
|
17
18
|
&& !handle.endsWith(".")
|
|
18
19
|
&& !handle.endsWith(" ")
|
|
19
20
|
&& Buffer.byteLength(handle, "utf8") <= 255, "agent handle must be a cross-platform safe filesystem component");
|
|
21
|
+
const ProjectSkillRefSchema = z.object({
|
|
22
|
+
projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
|
|
23
|
+
skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
|
|
24
|
+
}).strict();
|
|
20
25
|
export const ReasoningSchema = z.enum([
|
|
21
26
|
"default",
|
|
22
27
|
"none",
|
|
@@ -99,6 +104,7 @@ export const ExecutionStartSchema = z.object({
|
|
|
99
104
|
id: z.string().min(1),
|
|
100
105
|
handle: AgentHandleSchema,
|
|
101
106
|
memoryEnabled: z.boolean().optional(),
|
|
107
|
+
projectSkills: z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS).optional(),
|
|
102
108
|
}).strict(),
|
|
103
109
|
workspace: z.object({
|
|
104
110
|
taskKey: z.string().min(1).max(200),
|
package/dist/execution-runner.js
CHANGED
|
@@ -505,6 +505,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
505
505
|
...(dependencies.startupTimeoutMs === undefined
|
|
506
506
|
? {}
|
|
507
507
|
: { startupTimeoutMs: dependencies.startupTimeoutMs }),
|
|
508
|
+
...(dependencies.projectSkills === undefined ? {} : { projectSkills: dependencies.projectSkills }),
|
|
508
509
|
launchRuntime: async (request) => {
|
|
509
510
|
if (launchClosed || dependencies.cancellation?.isRequested())
|
|
510
511
|
throw new ExecutionCancelledError();
|
|
@@ -572,6 +573,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
572
573
|
...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
|
|
573
574
|
...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
|
|
574
575
|
...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
|
|
576
|
+
...(spec.agent.projectSkills === undefined ? {} : { projectSkills: spec.agent.projectSkills }),
|
|
575
577
|
systemPrompt: boundedSystemPrompt,
|
|
576
578
|
wakePrompt: spec.instructions.wakePrompt,
|
|
577
579
|
runtime: {
|
package/dist/local-executor.js
CHANGED
|
@@ -116,6 +116,9 @@ async function launchLegacyRuntime(request) {
|
|
|
116
116
|
...common,
|
|
117
117
|
systemPromptPath: request.systemPromptPath,
|
|
118
118
|
wakePrompt: request.wakePrompt,
|
|
119
|
+
...(request.agentRoot === undefined ? {} : {
|
|
120
|
+
projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
|
|
121
|
+
}),
|
|
119
122
|
...(request.sessionId === undefined ? {} : {
|
|
120
123
|
sessionId: request.sessionId,
|
|
121
124
|
resume: request.resume,
|
|
@@ -318,10 +321,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
318
321
|
}
|
|
319
322
|
}
|
|
320
323
|
const runtimeLaunchAt = Date.now();
|
|
321
|
-
const
|
|
324
|
+
const launchRequest = {
|
|
322
325
|
runtime: runtime.name,
|
|
323
326
|
bin: runtime.name,
|
|
324
327
|
cwd: workspace.runDir,
|
|
328
|
+
...(input.projectSkills === undefined ? {} : { agentRoot: workspace.dir }),
|
|
325
329
|
systemPromptPath: workspace.systemPromptPath,
|
|
326
330
|
systemPrompt,
|
|
327
331
|
wakePrompt,
|
|
@@ -334,7 +338,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
334
338
|
...(attachmentPlan.nativeImagePaths.length > 0
|
|
335
339
|
? { imagePaths: attachmentPlan.nativeImagePaths }
|
|
336
340
|
: {}),
|
|
337
|
-
}
|
|
341
|
+
};
|
|
342
|
+
const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
|
|
343
|
+
? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
|
|
344
|
+
: await launchRuntime(launchRequest);
|
|
338
345
|
if (child.cancel !== undefined) {
|
|
339
346
|
dependencies.cancellation?.register(child.cancel);
|
|
340
347
|
}
|
package/dist/machine-info.js
CHANGED
|
@@ -27,7 +27,11 @@ export const DAEMON_CAPABILITIES = [
|
|
|
27
27
|
"execution_answer_stream_v1",
|
|
28
28
|
"execution_machine_queue_v1",
|
|
29
29
|
"execution_agent_memory_policy_v1",
|
|
30
|
+
"project_skills_v1",
|
|
30
31
|
];
|
|
32
|
+
export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
|
|
33
|
+
? DAEMON_CAPABILITIES
|
|
34
|
+
: DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1");
|
|
31
35
|
export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
|
|
32
36
|
/** 候选 runtime CLI:展示名 → 可执行文件名。 */
|
|
33
37
|
const RUNTIME_BINS = [
|
|
@@ -117,7 +121,7 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
117
121
|
daemonVersion: daemonVersion(),
|
|
118
122
|
runtimes,
|
|
119
123
|
executionRuntimes,
|
|
120
|
-
capabilities: [...
|
|
124
|
+
capabilities: [...daemonCapabilities(runtimePlatform), ...additionalCapabilities],
|
|
121
125
|
...(backend.supported ? {
|
|
122
126
|
executionProtocol: EXECUTION_PROTOCOL,
|
|
123
127
|
executionLimits: Object.freeze({
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createKeyedPromiseTail } from "../promise-tail.js";
|
|
2
|
+
export function createAgentProjectionCoordinator() {
|
|
3
|
+
const tails = createKeyedPromiseTail();
|
|
4
|
+
return {
|
|
5
|
+
runExclusive(agentsRoot, handle, operation) {
|
|
6
|
+
const key = JSON.stringify([agentsRoot, handle]);
|
|
7
|
+
return tails.enqueue(key, operation);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { scanProjects } from "./scanner.js";
|
|
3
|
+
import { ProjectRegistryError } from "./registry.js";
|
|
4
|
+
import { isProjectId, isProjectSkillName, MAX_AGENT_PROJECT_SKILL_BINDINGS, } from "./types.js";
|
|
5
|
+
import { AgentHandleSchema } from "../execution-protocol.js";
|
|
6
|
+
import { createPromiseTail } from "../promise-tail.js";
|
|
7
|
+
const ProjectIdSchema = z.string().refine(isProjectId);
|
|
8
|
+
const ProjectCommandSchema = z.discriminatedUnion("type", [
|
|
9
|
+
z.object({
|
|
10
|
+
type: z.literal("project:add"),
|
|
11
|
+
reqId: z.string().min(1).max(128),
|
|
12
|
+
projectId: ProjectIdSchema,
|
|
13
|
+
root: z.string().min(1).max(4_096),
|
|
14
|
+
}).strict(),
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("project:remove"),
|
|
17
|
+
reqId: z.string().min(1).max(128),
|
|
18
|
+
projectId: ProjectIdSchema,
|
|
19
|
+
}).strict(),
|
|
20
|
+
z.object({
|
|
21
|
+
type: z.literal("project:rescan"),
|
|
22
|
+
reqId: z.string().min(1).max(128),
|
|
23
|
+
projectId: ProjectIdSchema,
|
|
24
|
+
}).strict(),
|
|
25
|
+
z.object({
|
|
26
|
+
type: z.literal("agent:skills:sync"),
|
|
27
|
+
reqId: z.string().min(1).max(128),
|
|
28
|
+
handle: AgentHandleSchema,
|
|
29
|
+
bindings: z.array(z.object({
|
|
30
|
+
projectId: ProjectIdSchema,
|
|
31
|
+
skillName: z.string().refine(isProjectSkillName),
|
|
32
|
+
}).strict()).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
|
|
33
|
+
}).strict(),
|
|
34
|
+
]);
|
|
35
|
+
export function createProjectSkillsController(deps) {
|
|
36
|
+
let scanned = Object.freeze([]);
|
|
37
|
+
const operationTail = createPromiseTail();
|
|
38
|
+
let initialization = null;
|
|
39
|
+
let initializationState = "idle";
|
|
40
|
+
const refresh = async () => {
|
|
41
|
+
scanned = await scanProjects(await deps.registry.list());
|
|
42
|
+
};
|
|
43
|
+
const publishCurrent = async () => {
|
|
44
|
+
await deps.publish(Object.freeze({
|
|
45
|
+
type: "machine:projects",
|
|
46
|
+
projects: Object.freeze(scanned.map((project) => project.inventory)),
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
const enqueue = (operation) => operationTail.enqueue(operation);
|
|
50
|
+
return {
|
|
51
|
+
initialize() {
|
|
52
|
+
if (initialization !== null)
|
|
53
|
+
return initialization;
|
|
54
|
+
initializationState = "initializing";
|
|
55
|
+
initialization = enqueue(refresh).then(() => { initializationState = "ready"; }, (error) => {
|
|
56
|
+
initializationState = "failed";
|
|
57
|
+
throw error;
|
|
58
|
+
});
|
|
59
|
+
return initialization;
|
|
60
|
+
},
|
|
61
|
+
publishCurrent: () => enqueue(publishCurrent),
|
|
62
|
+
scannedProjects: () => scanned,
|
|
63
|
+
handle(input) {
|
|
64
|
+
return enqueue(async () => {
|
|
65
|
+
const parsed = ProjectCommandSchema.safeParse(input);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
return { ok: false, error: "invalid_project_command" };
|
|
68
|
+
if (initializationState !== "ready") {
|
|
69
|
+
return { ok: false, error: "project_skills_unavailable" };
|
|
70
|
+
}
|
|
71
|
+
const command = parsed.data;
|
|
72
|
+
try {
|
|
73
|
+
if (command.type === "agent:skills:sync") {
|
|
74
|
+
if (deps.reconcile === undefined)
|
|
75
|
+
return { ok: false, error: "skill_projection_failed" };
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
data: { bindings: await deps.reconcile(command.handle, command.bindings) },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (command.type === "project:add") {
|
|
82
|
+
await deps.registry.add(command.projectId, command.root);
|
|
83
|
+
}
|
|
84
|
+
else if (command.type === "project:remove") {
|
|
85
|
+
await deps.registry.remove(command.projectId);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const projects = await deps.registry.list();
|
|
89
|
+
if (!projects.some((project) => project.projectId === command.projectId)) {
|
|
90
|
+
return { ok: false, error: "project_not_found" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await refresh();
|
|
94
|
+
await publishCurrent();
|
|
95
|
+
return { ok: true, data: { projectId: command.projectId } };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: error instanceof ProjectRegistryError
|
|
101
|
+
? error.code
|
|
102
|
+
: error.code === "skill_name_conflict"
|
|
103
|
+
? "skill_name_conflict"
|
|
104
|
+
: error.code === "skill_projection_failed"
|
|
105
|
+
? "skill_projection_failed"
|
|
106
|
+
: "project_operation_failed",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { lstat, mkdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export class ProjectProjectionError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code) {
|
|
7
|
+
super(code);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "ProjectProjectionError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const exists = async (path) => lstat(path).then(() => true, () => false);
|
|
13
|
+
async function switchProjectionSet(targets) {
|
|
14
|
+
const moved = [];
|
|
15
|
+
try {
|
|
16
|
+
for (const target of targets) {
|
|
17
|
+
const hadPrevious = await exists(target.target);
|
|
18
|
+
if (hadPrevious)
|
|
19
|
+
await rename(target.target, target.backup);
|
|
20
|
+
const state = { target, hadPrevious, activated: false };
|
|
21
|
+
moved.push(state);
|
|
22
|
+
await rename(target.staging, target.target);
|
|
23
|
+
state.activated = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
for (const state of [...moved].reverse()) {
|
|
28
|
+
if (state.activated)
|
|
29
|
+
await rm(state.target.target, { recursive: true, force: true }).catch(() => { });
|
|
30
|
+
if (state.hadPrevious)
|
|
31
|
+
await rename(state.target.backup, state.target.target).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
await Promise.all(targets.map((target) => rm(target.backup, { recursive: true, force: true })));
|
|
36
|
+
}
|
|
37
|
+
export function createProjectSkillsReconciler(deps) {
|
|
38
|
+
const platform = deps.platform ?? process.platform;
|
|
39
|
+
const reconcileUnlocked = async (handle, bindings) => {
|
|
40
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
41
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
42
|
+
}
|
|
43
|
+
const projects = deps.scannedProjects();
|
|
44
|
+
const uniqueBindings = [...new Map(bindings.map((binding) => [
|
|
45
|
+
`${binding.projectId}\0${binding.skillName}`,
|
|
46
|
+
binding,
|
|
47
|
+
])).values()].sort((left, right) => left.projectId.localeCompare(right.projectId) || left.skillName.localeCompare(right.skillName));
|
|
48
|
+
const names = new Map();
|
|
49
|
+
for (const binding of uniqueBindings) {
|
|
50
|
+
const owner = names.get(binding.skillName);
|
|
51
|
+
if (owner !== undefined && owner !== binding.projectId) {
|
|
52
|
+
throw new ProjectProjectionError("skill_name_conflict");
|
|
53
|
+
}
|
|
54
|
+
names.set(binding.skillName, binding.projectId);
|
|
55
|
+
}
|
|
56
|
+
const linked = [];
|
|
57
|
+
const resolutions = [];
|
|
58
|
+
for (const binding of uniqueBindings) {
|
|
59
|
+
const project = projects.find((candidate) => candidate.inventory.projectId === binding.projectId
|
|
60
|
+
&& candidate.inventory.status === "available");
|
|
61
|
+
const skill = project?.resolved.find((candidate) => candidate.name === binding.skillName);
|
|
62
|
+
const available = skill !== undefined && (await stat(skill.sourcePath).catch(() => null))?.isDirectory() === true;
|
|
63
|
+
resolutions.push(Object.freeze({ ...binding, status: available ? "linked" : "unavailable" }));
|
|
64
|
+
if (available && skill !== undefined)
|
|
65
|
+
linked.push({ binding, skill });
|
|
66
|
+
}
|
|
67
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
68
|
+
const id = randomUUID();
|
|
69
|
+
const targets = [
|
|
70
|
+
{
|
|
71
|
+
runtime: "codex",
|
|
72
|
+
target: join(agentRoot, ".agents", "skills"),
|
|
73
|
+
staging: join(agentRoot, ".agents", `.skills-next-${id}`),
|
|
74
|
+
backup: join(agentRoot, ".agents", `.skills-previous-${id}`),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
runtime: "claude",
|
|
78
|
+
target: join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
79
|
+
staging: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-next-${id}`),
|
|
80
|
+
backup: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-previous-${id}`),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
try {
|
|
84
|
+
for (const target of targets) {
|
|
85
|
+
await mkdir(dirname(target.target), { recursive: true });
|
|
86
|
+
await mkdir(target.staging, { mode: 0o700 });
|
|
87
|
+
for (const item of linked) {
|
|
88
|
+
await symlink(item.skill.sourcePath, join(target.staging, item.binding.skillName), "dir");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const target of targets)
|
|
92
|
+
await deps.beforeSwitch?.(target.runtime);
|
|
93
|
+
await switchProjectionSet(targets);
|
|
94
|
+
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
95
|
+
return Object.freeze(resolutions);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
await Promise.all(targets.map((target) => rm(target.staging, { recursive: true, force: true })));
|
|
99
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
reconcile(handle, bindings) {
|
|
104
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, () => reconcileUnlocked(handle, bindings));
|
|
105
|
+
},
|
|
106
|
+
prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
107
|
+
if (agentsRoot !== deps.agentsRoot) {
|
|
108
|
+
return Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
109
|
+
}
|
|
110
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
111
|
+
await reconcileUnlocked(handle, bindings);
|
|
112
|
+
return launch();
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { isProjectId, MAX_MACHINE_PROJECTS } from "./types.js";
|
|
5
|
+
import { createPromiseTail } from "../promise-tail.js";
|
|
6
|
+
import { atomicPrivateWrite } from "../atomic-private-write.js";
|
|
7
|
+
const RegistryFileSchema = z.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
projects: z.record(z.object({ root: z.string().min(1).max(4_096) }).strict())
|
|
10
|
+
.refine((projects) => Object.keys(projects).length <= MAX_MACHINE_PROJECTS),
|
|
11
|
+
}).strict();
|
|
12
|
+
export class ProjectRegistryError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super(code);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "ProjectRegistryError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const immutableList = (projects) => Object.freeze(Object.entries(projects)
|
|
21
|
+
.map(([projectId, project]) => Object.freeze({ projectId, root: project.root }))
|
|
22
|
+
.sort((left, right) => left.projectId.localeCompare(right.projectId)));
|
|
23
|
+
export function createProjectRegistry(agentsRoot) {
|
|
24
|
+
const path = join(agentsRoot, ".crew", "projects.json");
|
|
25
|
+
const writeTail = createPromiseTail();
|
|
26
|
+
const load = async () => {
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = await readFile(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === "ENOENT")
|
|
33
|
+
return Object.freeze({});
|
|
34
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const parsed = RegistryFileSchema.parse(JSON.parse(raw));
|
|
38
|
+
for (const [projectId, project] of Object.entries(parsed.projects)) {
|
|
39
|
+
if (!isProjectId(projectId) || !isAbsolute(project.root)) {
|
|
40
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(Object.fromEntries(Object.entries(parsed.projects).map(([projectId, project]) => [
|
|
44
|
+
projectId,
|
|
45
|
+
Object.freeze({ root: resolve(project.root) }),
|
|
46
|
+
])));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof ProjectRegistryError)
|
|
50
|
+
throw error;
|
|
51
|
+
throw new ProjectRegistryError("project_registry_corrupt");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const save = async (projects) => {
|
|
55
|
+
await atomicPrivateWrite(path, `${JSON.stringify({ version: 1, projects }, null, 2)}\n`);
|
|
56
|
+
};
|
|
57
|
+
const enqueue = (operation) => writeTail.enqueue(operation);
|
|
58
|
+
return {
|
|
59
|
+
path,
|
|
60
|
+
async list() {
|
|
61
|
+
await writeTail.wait();
|
|
62
|
+
return immutableList(await load());
|
|
63
|
+
},
|
|
64
|
+
add(projectId, root) {
|
|
65
|
+
return enqueue(async () => {
|
|
66
|
+
if (!isProjectId(projectId))
|
|
67
|
+
throw new ProjectRegistryError("project_id_invalid");
|
|
68
|
+
if (!isAbsolute(root))
|
|
69
|
+
throw new ProjectRegistryError("project_path_invalid");
|
|
70
|
+
const normalizedRoot = resolve(root);
|
|
71
|
+
const rootStat = await stat(normalizedRoot).catch(() => null);
|
|
72
|
+
if (!rootStat?.isDirectory())
|
|
73
|
+
throw new ProjectRegistryError("project_path_invalid");
|
|
74
|
+
const projects = await load();
|
|
75
|
+
const existing = projects[projectId];
|
|
76
|
+
if (existing?.root === normalizedRoot)
|
|
77
|
+
return Object.freeze({ projectId, root: normalizedRoot });
|
|
78
|
+
if (existing !== undefined)
|
|
79
|
+
throw new ProjectRegistryError("project_id_conflict");
|
|
80
|
+
if (Object.keys(projects).length >= MAX_MACHINE_PROJECTS) {
|
|
81
|
+
throw new ProjectRegistryError("project_limit_exceeded");
|
|
82
|
+
}
|
|
83
|
+
const next = Object.freeze({ ...projects, [projectId]: Object.freeze({ root: normalizedRoot }) });
|
|
84
|
+
await save(next);
|
|
85
|
+
return Object.freeze({ projectId, root: normalizedRoot });
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
remove(projectId) {
|
|
89
|
+
return enqueue(async () => {
|
|
90
|
+
if (!isProjectId(projectId))
|
|
91
|
+
throw new ProjectRegistryError("project_id_invalid");
|
|
92
|
+
const projects = await load();
|
|
93
|
+
if (projects[projectId] === undefined)
|
|
94
|
+
return false;
|
|
95
|
+
const next = Object.fromEntries(Object.entries(projects).filter(([candidate]) => candidate !== projectId));
|
|
96
|
+
await save(Object.freeze(next));
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isProjectSkillName, MAX_MACHINE_PROJECT_SKILLS, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILLS_PER_PROJECT, } from "./types.js";
|
|
4
|
+
import { parseSkillFrontmatter } from "../skill-frontmatter.js";
|
|
5
|
+
const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
|
|
6
|
+
inventory: Object.freeze({
|
|
7
|
+
projectId,
|
|
8
|
+
status: "unavailable",
|
|
9
|
+
skills: Object.freeze([]),
|
|
10
|
+
invalidSkillCount: 0,
|
|
11
|
+
errorCode,
|
|
12
|
+
scannedAt,
|
|
13
|
+
}),
|
|
14
|
+
resolved: Object.freeze([]),
|
|
15
|
+
});
|
|
16
|
+
export async function scanProject(project, now = () => new Date()) {
|
|
17
|
+
const scannedAt = now().toISOString();
|
|
18
|
+
const projectStat = await stat(project.root).catch(() => null);
|
|
19
|
+
if (!projectStat?.isDirectory())
|
|
20
|
+
return unavailable(project.projectId, scannedAt, "project_path_unavailable");
|
|
21
|
+
const skillsRoot = join(project.root, ".agents", "skills");
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === "ENOENT") {
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
inventory: Object.freeze({
|
|
30
|
+
projectId: project.projectId,
|
|
31
|
+
status: "available",
|
|
32
|
+
skills: Object.freeze([]),
|
|
33
|
+
invalidSkillCount: 0,
|
|
34
|
+
errorCode: null,
|
|
35
|
+
scannedAt,
|
|
36
|
+
}),
|
|
37
|
+
resolved: Object.freeze([]),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return unavailable(project.projectId, scannedAt, "project_skills_unreadable");
|
|
41
|
+
}
|
|
42
|
+
let invalidSkillCount = 0;
|
|
43
|
+
const candidates = [];
|
|
44
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
45
|
+
if (entry.name.startsWith(".") || !entry.isDirectory())
|
|
46
|
+
continue;
|
|
47
|
+
const sourcePath = join(skillsRoot, entry.name);
|
|
48
|
+
const markdown = await readFile(join(sourcePath, "SKILL.md"), "utf8").catch(() => null);
|
|
49
|
+
if (markdown === null) {
|
|
50
|
+
invalidSkillCount += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const frontmatter = parseSkillFrontmatter(markdown);
|
|
54
|
+
const name = frontmatter?.name ?? entry.name;
|
|
55
|
+
const description = frontmatter?.description ?? "";
|
|
56
|
+
if (!isProjectSkillName(name) || description.length > MAX_PROJECT_SKILL_DESCRIPTION_LENGTH) {
|
|
57
|
+
invalidSkillCount += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
candidates.push(Object.freeze({ name, description, sourcePath }));
|
|
61
|
+
}
|
|
62
|
+
const counts = new Map();
|
|
63
|
+
for (const candidate of candidates)
|
|
64
|
+
counts.set(candidate.name, (counts.get(candidate.name) ?? 0) + 1);
|
|
65
|
+
const resolved = candidates.filter((candidate) => {
|
|
66
|
+
if (counts.get(candidate.name) === 1)
|
|
67
|
+
return true;
|
|
68
|
+
invalidSkillCount += 1;
|
|
69
|
+
return false;
|
|
70
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
71
|
+
const skills = resolved.map(({ name, description }) => Object.freeze({ name, description }));
|
|
72
|
+
if (skills.length > MAX_PROJECT_SKILLS_PER_PROJECT) {
|
|
73
|
+
return unavailable(project.projectId, scannedAt, "project_skill_limit_exceeded");
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
inventory: Object.freeze({
|
|
77
|
+
projectId: project.projectId,
|
|
78
|
+
status: "available",
|
|
79
|
+
skills: Object.freeze(skills),
|
|
80
|
+
invalidSkillCount,
|
|
81
|
+
errorCode: null,
|
|
82
|
+
scannedAt,
|
|
83
|
+
}),
|
|
84
|
+
resolved: Object.freeze(resolved),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function boundScannedProjects(projects) {
|
|
88
|
+
let skillCount = 0;
|
|
89
|
+
return Object.freeze(projects.map((project) => {
|
|
90
|
+
if (project.inventory.status !== "available")
|
|
91
|
+
return project;
|
|
92
|
+
const nextCount = skillCount + project.inventory.skills.length;
|
|
93
|
+
if (nextCount <= MAX_MACHINE_PROJECT_SKILLS) {
|
|
94
|
+
skillCount = nextCount;
|
|
95
|
+
return project;
|
|
96
|
+
}
|
|
97
|
+
return unavailable(project.inventory.projectId, project.inventory.scannedAt, "machine_project_skill_limit_exceeded");
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
export async function scanProjects(projects, now = () => new Date()) {
|
|
101
|
+
const sorted = [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
102
|
+
return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))));
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const PROJECT_SKILLS_CAPABILITY = "project_skills_v1";
|
|
2
|
+
export const MAX_PROJECT_ID_LENGTH = 64;
|
|
3
|
+
export const MAX_PROJECT_SKILL_NAME_LENGTH = 128;
|
|
4
|
+
export const MAX_PROJECT_SKILL_DESCRIPTION_LENGTH = 1_000;
|
|
5
|
+
export const MAX_PROJECT_SKILLS_PER_PROJECT = 1_000;
|
|
6
|
+
export const MAX_MACHINE_PROJECTS = 100;
|
|
7
|
+
export const MAX_MACHINE_PROJECT_SKILLS = 1_000;
|
|
8
|
+
export const MAX_AGENT_PROJECT_SKILL_BINDINGS = 500;
|
|
9
|
+
const PROJECT_ID = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
10
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9._:-]*$/u;
|
|
11
|
+
export const isProjectId = (value) => value.length > 0 && value.length <= MAX_PROJECT_ID_LENGTH && PROJECT_ID.test(value);
|
|
12
|
+
export const isProjectSkillName = (value) => value.length > 0 && value.length <= MAX_PROJECT_SKILL_NAME_LENGTH && SKILL_NAME.test(value);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function createKeyedPromiseTail() {
|
|
2
|
+
const tails = new Map();
|
|
3
|
+
return {
|
|
4
|
+
enqueue(key, operation) {
|
|
5
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
6
|
+
const current = previous.catch(() => { }).then(operation);
|
|
7
|
+
const tail = current.then(() => undefined);
|
|
8
|
+
tails.set(key, tail);
|
|
9
|
+
void tail.finally(() => {
|
|
10
|
+
if (tails.get(key) === tail)
|
|
11
|
+
tails.delete(key);
|
|
12
|
+
}).catch(() => { });
|
|
13
|
+
return current;
|
|
14
|
+
},
|
|
15
|
+
pending: (key) => tails.get(key),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function createPromiseTail() {
|
|
19
|
+
const queue = createKeyedPromiseTail();
|
|
20
|
+
const key = "singleton";
|
|
21
|
+
return {
|
|
22
|
+
enqueue: (operation) => queue.enqueue(key, operation),
|
|
23
|
+
wait: () => queue.pending(key)?.catch(() => { }) ?? Promise.resolve(),
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -154,6 +154,9 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
154
154
|
exitCode: local.exitCode,
|
|
155
155
|
finalText: local.finalText,
|
|
156
156
|
errorMessage: local.errorMessage,
|
|
157
|
+
...(input.scheduled.serverOwnsFailureNotice?.() === true
|
|
158
|
+
? { suppressFailureNotice: true }
|
|
159
|
+
: {}),
|
|
157
160
|
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
158
161
|
content,
|
|
159
162
|
force: true,
|
|
@@ -194,6 +197,7 @@ export async function reportScheduledStartFailure(config, input) {
|
|
|
194
197
|
exitCode: -1,
|
|
195
198
|
finalText: null,
|
|
196
199
|
errorMessage: input.errorMessage,
|
|
200
|
+
...(input.serverOwnsFailureNotice?.() === true ? { suppressFailureNotice: true } : {}),
|
|
197
201
|
send: (content) => sendAgentMessage(config.serverUrl, credential.token, input.channelId, {
|
|
198
202
|
content,
|
|
199
203
|
force: true,
|
package/dist/runtimes/claude.js
CHANGED
|
@@ -28,6 +28,8 @@ export function buildClaudeArgs(input) {
|
|
|
28
28
|
if (input.sessionId) {
|
|
29
29
|
args.push(input.resume ? "--resume" : "--session-id", input.sessionId);
|
|
30
30
|
}
|
|
31
|
+
if (input.projectSkillsDirectory)
|
|
32
|
+
args.push("--add-dir", input.projectSkillsDirectory);
|
|
31
33
|
if (input.effectivePermission === undefined) {
|
|
32
34
|
if (input.dangerous)
|
|
33
35
|
args.push("--dangerously-skip-permissions");
|
|
@@ -14,6 +14,7 @@ const RunnerInputSchema = z.object({
|
|
|
14
14
|
reasoning: z.string().min(1).optional(),
|
|
15
15
|
sessionId: z.string().min(1).optional(),
|
|
16
16
|
imagePaths: z.array(z.string().min(1)).optional(),
|
|
17
|
+
projectRootMarkers: z.array(z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/u)).max(16).optional(),
|
|
17
18
|
resume: z.boolean(),
|
|
18
19
|
}).strict();
|
|
19
20
|
const RPC_TIMEOUT_MS = 30_000;
|
|
@@ -31,6 +32,14 @@ const MAX_TRANSIENT_TURN_RETRIES = 2;
|
|
|
31
32
|
const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
|
|
32
33
|
const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
|
|
33
34
|
const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
|
|
35
|
+
export function codexAppServerArgs(projectRootMarkers) {
|
|
36
|
+
return [
|
|
37
|
+
...(projectRootMarkers === undefined
|
|
38
|
+
? []
|
|
39
|
+
: ["-c", `project_root_markers=${JSON.stringify(projectRootMarkers)}`]),
|
|
40
|
+
"app-server", "--listen", "stdio://",
|
|
41
|
+
];
|
|
42
|
+
}
|
|
34
43
|
const TRANSIENT_TURN_ERROR_PATTERNS = [
|
|
35
44
|
/at capacity/i,
|
|
36
45
|
/overloaded/i,
|
|
@@ -345,7 +354,7 @@ async function stopChildTree(child) {
|
|
|
345
354
|
}
|
|
346
355
|
async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs) {
|
|
347
356
|
const attemptStartedAt = Date.now();
|
|
348
|
-
const child = spawn(bin,
|
|
357
|
+
const child = spawn(bin, codexAppServerArgs(input.projectRootMarkers), {
|
|
349
358
|
cwd: process.cwd(),
|
|
350
359
|
env: process.env,
|
|
351
360
|
stdio: ["pipe", "pipe", "pipe"],
|
package/dist/scheduled-report.js
CHANGED
|
@@ -18,6 +18,9 @@ export async function deliverScheduledReport(input) {
|
|
|
18
18
|
let source = "none";
|
|
19
19
|
let content = null;
|
|
20
20
|
if (input.exitCode !== 0) {
|
|
21
|
+
if (input.suppressFailureNotice === true) {
|
|
22
|
+
return { required: false, attempted: false, delivered: false, source: "none" };
|
|
23
|
+
}
|
|
21
24
|
source = "failure_notice";
|
|
22
25
|
const detail = input.errorMessage?.trim().slice(0, 500);
|
|
23
26
|
content = `Scheduled job "${title}" failed${detail ? `: ${detail}` : ` (exit ${input.exitCode})`}.`;
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
9
|
import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
10
|
import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello,
|
|
11
|
+
import { collectMachineHello, daemonCapabilities, EXECUTION_PROTOCOL, } from "./machine-info.js";
|
|
12
12
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
13
13
|
import { listSkills } from "./skills.js";
|
|
14
14
|
import { inspectRaftWorkspace, importRaftWorkspace } from "./workspace-import.js";
|
|
@@ -35,26 +35,35 @@ import { detectDaemonUpdateEligibility, } from "./daemon-update-eligibility.js";
|
|
|
35
35
|
import { createDaemonUpdateController } from "./daemon-update-controller.js";
|
|
36
36
|
import { installExactDaemonUpdate } from "./daemon-updater.js";
|
|
37
37
|
import { scheduleServiceRestart } from "./computer-service.js";
|
|
38
|
+
import { createProjectRegistry } from "./project-skills/registry.js";
|
|
39
|
+
import { createProjectSkillsController, } from "./project-skills/controller.js";
|
|
40
|
+
import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
41
|
+
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
42
|
+
import { createProjectSkillsReconciler, ProjectProjectionError, } from "./project-skills/reconciler.js";
|
|
38
43
|
// normalize.ts 的活动种类 → activity 枚举
|
|
39
44
|
const ACTIVITY_MAP = {
|
|
40
45
|
init: "working", text: "thinking", reading: "reading", sending: "sending",
|
|
41
46
|
checking: "checking", claiming: "claiming", crew: "working", tool: "working",
|
|
42
47
|
tool_result: "working", done: "done", error: "error",
|
|
43
48
|
};
|
|
44
|
-
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe) {
|
|
49
|
+
export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, projectSkillsAvailable = true) {
|
|
45
50
|
const query = new URLSearchParams({ key: machineToken });
|
|
46
51
|
if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
|
|
47
52
|
query.set("execution_min", String(EXECUTION_PROTOCOL.min));
|
|
48
53
|
query.set("execution_max", String(EXECUTION_PROTOCOL.max));
|
|
49
54
|
}
|
|
50
|
-
for (const capability of
|
|
51
|
-
|
|
55
|
+
for (const capability of daemonCapabilities(runtimePlatform)) {
|
|
56
|
+
if (capability !== PROJECT_SKILLS_CAPABILITY || projectSkillsAvailable) {
|
|
57
|
+
query.append("capability", capability);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
52
60
|
return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
|
|
53
61
|
}
|
|
54
62
|
export function serve(config, opts = {}) {
|
|
55
|
-
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken);
|
|
56
63
|
let stopped = false;
|
|
57
64
|
let ws = null;
|
|
65
|
+
// 当前连接的 server 能力(ready 帧下发;重连后由新 ready 帧刷新)。
|
|
66
|
+
let serverCapabilities = new Set();
|
|
58
67
|
let backoff = 1000;
|
|
59
68
|
const maxBackoff = opts.maxBackoffMs ?? 30_000;
|
|
60
69
|
const testShutdown = readTestShutdownConfiguration(process.env);
|
|
@@ -97,6 +106,45 @@ export function serve(config, opts = {}) {
|
|
|
97
106
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
98
107
|
},
|
|
99
108
|
});
|
|
109
|
+
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
110
|
+
let projectSkillsController;
|
|
111
|
+
const projectSkillsReconciler = opts.projectSkills?.reconciler ?? createProjectSkillsReconciler({
|
|
112
|
+
agentsRoot: config.agentsRoot,
|
|
113
|
+
coordinator: projectionCoordinator,
|
|
114
|
+
scannedProjects: () => projectSkillsController.scannedProjects(),
|
|
115
|
+
});
|
|
116
|
+
projectSkillsController = opts.projectSkills?.controller ?? createProjectSkillsController({
|
|
117
|
+
registry: createProjectRegistry(config.agentsRoot),
|
|
118
|
+
publish: (frame) => {
|
|
119
|
+
if (!serverCapabilities.has(PROJECT_SKILLS_CAPABILITY))
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
if (ws?.readyState === WebSocket.OPEN)
|
|
123
|
+
ws.send(JSON.stringify(frame));
|
|
124
|
+
}
|
|
125
|
+
catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
|
|
126
|
+
},
|
|
127
|
+
reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
|
|
128
|
+
});
|
|
129
|
+
const projectSkillsInitialized = projectSkillsController.initialize().then(() => true, (error) => {
|
|
130
|
+
dslog("project_skills.registry_failed", "本机项目注册表读取失败", {
|
|
131
|
+
level: "ERROR", error_code: error.code ?? "project_registry_corrupt",
|
|
132
|
+
});
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
let projectSkillsAvailable = false;
|
|
136
|
+
const initializedProjectSkillsReconciler = {
|
|
137
|
+
reconcile(handle, bindings) {
|
|
138
|
+
return projectSkillsAvailable
|
|
139
|
+
? projectSkillsReconciler.reconcile(handle, bindings)
|
|
140
|
+
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
141
|
+
},
|
|
142
|
+
prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
143
|
+
return projectSkillsAvailable
|
|
144
|
+
? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
|
|
145
|
+
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
146
|
+
},
|
|
147
|
+
};
|
|
100
148
|
const safeExecutionSend = (frame) => {
|
|
101
149
|
try {
|
|
102
150
|
if (ws?.readyState !== WebSocket.OPEN)
|
|
@@ -205,6 +253,7 @@ export function serve(config, opts = {}) {
|
|
|
205
253
|
function connect() {
|
|
206
254
|
if (stopped)
|
|
207
255
|
return;
|
|
256
|
+
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsAvailable);
|
|
208
257
|
ws = createWebSocket(wsUrl);
|
|
209
258
|
ws.on("open", () => {
|
|
210
259
|
const openedSocket = ws;
|
|
@@ -234,9 +283,15 @@ export function serve(config, opts = {}) {
|
|
|
234
283
|
void helloPromise
|
|
235
284
|
.then((hello) => {
|
|
236
285
|
detectedExecutionRuntimes = hello.executionRuntimes;
|
|
286
|
+
const effectiveHello = projectSkillsAvailable
|
|
287
|
+
? hello
|
|
288
|
+
: {
|
|
289
|
+
...hello,
|
|
290
|
+
capabilities: hello.capabilities.filter((capability) => capability !== PROJECT_SKILLS_CAPABILITY),
|
|
291
|
+
};
|
|
237
292
|
try {
|
|
238
|
-
ws?.send(JSON.stringify(
|
|
239
|
-
log(`📤 已上报机器信息: ${
|
|
293
|
+
ws?.send(JSON.stringify(effectiveHello));
|
|
294
|
+
log(`📤 已上报机器信息: ${effectiveHello.hostname} · ${effectiveHello.os} · installed=[${effectiveHello.runtimes.join(",")}] · executable=[${effectiveHello.executionRuntimes.join(",")}] · agents=[${effectiveHello.agentHandles.join(",")}]`);
|
|
240
295
|
}
|
|
241
296
|
catch { /* 非 OPEN,忽略 */ }
|
|
242
297
|
})
|
|
@@ -437,6 +492,7 @@ export function serve(config, opts = {}) {
|
|
|
437
492
|
}
|
|
438
493
|
const execution = executeProtocol(config, spec, {
|
|
439
494
|
...opts.execution?.dependencies,
|
|
495
|
+
projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkillsReconciler,
|
|
440
496
|
...(agentMemory === undefined ? {} : { agentMemory }),
|
|
441
497
|
journal: executionJournal,
|
|
442
498
|
facts: {
|
|
@@ -506,6 +562,14 @@ export function serve(config, opts = {}) {
|
|
|
506
562
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
507
563
|
const r = msg;
|
|
508
564
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
565
|
+
// 旧 server 无此字段 → 空集合(daemon 保持全部旧行为)。
|
|
566
|
+
serverCapabilities = new Set(Array.isArray(r.serverCapabilities)
|
|
567
|
+
? r.serverCapabilities.filter((c) => typeof c === "string")
|
|
568
|
+
: []);
|
|
569
|
+
if (serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)) {
|
|
570
|
+
if (await projectSkillsInitialized)
|
|
571
|
+
await projectSkillsController.publishCurrent();
|
|
572
|
+
}
|
|
509
573
|
return;
|
|
510
574
|
}
|
|
511
575
|
if (msg.type === "error") {
|
|
@@ -517,6 +581,22 @@ export function serve(config, opts = {}) {
|
|
|
517
581
|
}
|
|
518
582
|
return;
|
|
519
583
|
}
|
|
584
|
+
if (msg.type === "project:add"
|
|
585
|
+
|| msg.type === "project:remove"
|
|
586
|
+
|| msg.type === "project:rescan"
|
|
587
|
+
|| msg.type === "agent:skills:sync") {
|
|
588
|
+
const request = msg;
|
|
589
|
+
if (typeof request.reqId !== "string")
|
|
590
|
+
return;
|
|
591
|
+
const result = serverCapabilities.has(PROJECT_SKILLS_CAPABILITY)
|
|
592
|
+
? await projectSkillsController.handle(msg)
|
|
593
|
+
: { ok: false, error: "capability_unavailable" };
|
|
594
|
+
try {
|
|
595
|
+
ws?.send(JSON.stringify({ type: "fs:result", reqId: request.reqId, ...result }));
|
|
596
|
+
}
|
|
597
|
+
catch { /* server 会按请求超时处理 */ }
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
520
600
|
// 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
|
|
521
601
|
if (msg.type === "raft:inspect" || msg.type === "raft:import") {
|
|
522
602
|
const req = msg;
|
|
@@ -579,6 +659,9 @@ export function serve(config, opts = {}) {
|
|
|
579
659
|
: {}),
|
|
580
660
|
})
|
|
581
661
|
: null;
|
|
662
|
+
// 活取值:发报告的时刻按"当前连接"的 server 能力判定(滚动发布中 run 可能跨重连;
|
|
663
|
+
// 断线时集合被清空 → 回退为 daemon 自行发通知,方向安全)。
|
|
664
|
+
const serverOwnsFailureNotice = () => serverCapabilities.has("scheduled_failure_notice_v1");
|
|
582
665
|
const threadId = msg.wake?.threadId;
|
|
583
666
|
const taskKey = scheduled ? scheduled.runId : (threadId ?? msg.channelId);
|
|
584
667
|
const key = `${msg.agentHandle}:${taskKey}`;
|
|
@@ -744,6 +827,7 @@ export function serve(config, opts = {}) {
|
|
|
744
827
|
title: scheduled.title,
|
|
745
828
|
outputPolicy: scheduled.outputPolicy,
|
|
746
829
|
externalNotificationPolicy: scheduled.externalNotificationPolicy,
|
|
830
|
+
serverOwnsFailureNotice,
|
|
747
831
|
},
|
|
748
832
|
} : {}),
|
|
749
833
|
...(wakeOrigin ? { wakeOrigin, originDecisionAttempt: attempt } : {}),
|
|
@@ -852,6 +936,7 @@ export function serve(config, opts = {}) {
|
|
|
852
936
|
channelId: msg.channelId,
|
|
853
937
|
scheduled,
|
|
854
938
|
errorMessage: e.message,
|
|
939
|
+
serverOwnsFailureNotice,
|
|
855
940
|
});
|
|
856
941
|
}
|
|
857
942
|
catch { /* token 也不可用时只能让 server 按 runtime failure 终态化 */ }
|
|
@@ -889,6 +974,8 @@ export function serve(config, opts = {}) {
|
|
|
889
974
|
ws.on("close", (code) => {
|
|
890
975
|
if (stopped)
|
|
891
976
|
return;
|
|
977
|
+
// server 能力随连接失效;下个连接的 ready 帧重新声明(防降级重连后沿用过期能力)。
|
|
978
|
+
serverCapabilities = new Set();
|
|
892
979
|
completionRetransmitter.pause();
|
|
893
980
|
// 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
|
|
894
981
|
// error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
|
|
@@ -926,6 +1013,7 @@ export function serve(config, opts = {}) {
|
|
|
926
1013
|
flush: flushSlog,
|
|
927
1014
|
writeStderr: (line) => process.stderr.write(line),
|
|
928
1015
|
});
|
|
1016
|
+
projectSkillsAvailable = await projectSkillsInitialized;
|
|
929
1017
|
connect();
|
|
930
1018
|
})();
|
|
931
1019
|
const stop = () => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function parseSkillFrontmatter(markdown) {
|
|
2
|
+
const match = markdown.match(/^---[\t ]*\r?\n([\s\S]*?)\r?\n---(?:[\t ]*\r?\n|$)/u);
|
|
3
|
+
if (!match)
|
|
4
|
+
return Object.freeze({});
|
|
5
|
+
let name;
|
|
6
|
+
let description;
|
|
7
|
+
for (const line of match[1]?.split(/\r?\n/u) ?? []) {
|
|
8
|
+
const field = line.match(/^(name|description)[\t ]*:[\t ]*(.*?)[\t ]*$/u);
|
|
9
|
+
if (!field)
|
|
10
|
+
continue;
|
|
11
|
+
const value = (field[2] ?? "")
|
|
12
|
+
.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, "$1$2")
|
|
13
|
+
.trim();
|
|
14
|
+
if (field[1] === "name")
|
|
15
|
+
name = value;
|
|
16
|
+
if (field[1] === "description")
|
|
17
|
+
description = value;
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
...(name === undefined ? {} : { name }),
|
|
21
|
+
...(description === undefined ? {} : { description }),
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/skills.js
CHANGED
|
@@ -9,25 +9,8 @@
|
|
|
9
9
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { parseSkillFrontmatter } from "./skill-frontmatter.js";
|
|
12
13
|
const globalSkillsDir = () => process.env.CREW_GLOBAL_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
13
|
-
/** 从 SKILL.md 顶部 YAML frontmatter 取 name / description (简易解析,够用)。 */
|
|
14
|
-
function parseFrontmatter(md) {
|
|
15
|
-
const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
16
|
-
if (!m)
|
|
17
|
-
return {};
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const line of m[1].split("\n")) {
|
|
20
|
-
const kv = line.match(/^(name|description)\s*:\s*(.+?)\s*$/);
|
|
21
|
-
if (kv) {
|
|
22
|
-
const val = kv[2].replace(/^["']|["']$/g, "");
|
|
23
|
-
if (kv[1] === "name")
|
|
24
|
-
out.name = val;
|
|
25
|
-
else
|
|
26
|
-
out.description = val;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
14
|
async function readSkillsFrom(dir, scope) {
|
|
32
15
|
const names = await readdir(dir).catch(() => []);
|
|
33
16
|
const skills = [];
|
|
@@ -39,7 +22,7 @@ async function readSkillsFrom(dir, scope) {
|
|
|
39
22
|
if (!s || !s.isFile())
|
|
40
23
|
continue;
|
|
41
24
|
const md = await readFile(skillMd, "utf8").catch(() => "");
|
|
42
|
-
const fm =
|
|
25
|
+
const fm = parseSkillFrontmatter(md);
|
|
43
26
|
skills.push({ scope, name: fm.name || name, description: fm.description ?? "" });
|
|
44
27
|
}
|
|
45
28
|
return skills;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { startDormantSupervisor, } from "./execution-supervisor.js";
|
|
3
4
|
import { buildClaudeArgs } from "./runtimes/claude.js";
|
|
4
5
|
import { RuntimeCancelledError } from "./runtime-cancellation.js";
|
|
@@ -19,6 +20,9 @@ export function supervisorLaunch(request) {
|
|
|
19
20
|
cwd: request.cwd,
|
|
20
21
|
env: request.env,
|
|
21
22
|
systemPromptPath: request.systemPromptPath,
|
|
23
|
+
...(request.agentRoot === undefined ? {} : {
|
|
24
|
+
projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
|
|
25
|
+
}),
|
|
22
26
|
...(request.sessionId === undefined ? {} : {
|
|
23
27
|
sessionId: request.sessionId,
|
|
24
28
|
resume: request.resume,
|
|
@@ -45,6 +49,9 @@ export function supervisorLaunch(request) {
|
|
|
45
49
|
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
46
50
|
...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
|
|
47
51
|
...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
|
|
52
|
+
...(request.agentRoot === undefined ? {} : {
|
|
53
|
+
projectRootMarkers: [".git", ".nowwork-root"],
|
|
54
|
+
}),
|
|
48
55
|
resume: request.resume,
|
|
49
56
|
}),
|
|
50
57
|
};
|