@nowcrew/daemon 0.6.77 → 0.6.78

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 (52) hide show
  1. package/README.md +27 -0
  2. package/dist/automatic-daemon-installer.js +44 -24
  3. package/dist/automatic-install-target.js +34 -9
  4. package/dist/computer-cli.js +43 -14
  5. package/dist/computer-profile.js +15 -2
  6. package/dist/computer-service.js +26 -5
  7. package/dist/config.js +28 -8
  8. package/dist/control-plane-url.js +3 -9
  9. package/dist/daemon-installation-lease.js +7 -4
  10. package/dist/daemon-local-home.js +108 -0
  11. package/dist/daemon-migration-controller.js +15 -4
  12. package/dist/daemon-registry.js +20 -0
  13. package/dist/daemon-update-controller.js +1 -0
  14. package/dist/daemon-update-eligibility.js +17 -7
  15. package/dist/daemon-updater.js +26 -26
  16. package/dist/execution-runner.js +7 -7
  17. package/dist/host-execution-coordinator.js +4 -3
  18. package/dist/local-executor.js +23 -17
  19. package/dist/local-memory/runtime.js +5 -3
  20. package/dist/machine-info.js +3 -0
  21. package/dist/main.js +13 -2
  22. package/dist/managed-service-diagnostics.js +1 -0
  23. package/dist/managed-service-registry.js +12 -7
  24. package/dist/managed-service-startup.js +4 -1
  25. package/dist/npm-command.js +9 -0
  26. package/dist/project-skills/controller.js +6 -71
  27. package/dist/project-skills/execution-adapter.js +7 -0
  28. package/dist/project-skills/execution-projection.js +163 -0
  29. package/dist/project-skills/initialized-reconciler.js +10 -8
  30. package/dist/project-skills/legacy-cleanup.js +224 -0
  31. package/dist/project-skills/machine-hello-publisher.js +20 -0
  32. package/dist/project-skills/reconciler.js +114 -500
  33. package/dist/project-skills/resolution-records.js +63 -0
  34. package/dist/project-skills/runtime-launch.js +0 -42
  35. package/dist/project-skills/runtime-warning.js +1 -3
  36. package/dist/project-skills/scanner.js +1 -17
  37. package/dist/project-skills/startup-cleanup.js +24 -0
  38. package/dist/runner.js +2 -1
  39. package/dist/serve.js +49 -49
  40. package/dist/slog.js +14 -1
  41. package/dist/storage-control.js +12 -1
  42. package/dist/storage-identity.js +15 -1
  43. package/dist/storage-migration-controller.js +25 -4
  44. package/dist/storage-ownership.js +55 -0
  45. package/dist/storage-runtime-controller.js +29 -7
  46. package/dist/storage-runtime-readiness.js +26 -4
  47. package/dist/storage-runtime.js +18 -2
  48. package/dist/storage-status-delivery.js +4 -0
  49. package/dist/storage-status-replay.js +16 -1
  50. package/dist/storage-worker-executor.js +1 -0
  51. package/dist/storage-worker-operation.js +4 -0
  52. package/package.json +1 -1
@@ -43,6 +43,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
43
43
  "execution_agent_memory_policy_v1",
44
44
  "execution_agent_memory_mapping_v1",
45
45
  "project_skills_v1",
46
+ "project_skill_projection_v2",
46
47
  PROJECT_WORKSPACES_CAPABILITY,
47
48
  TRAINING_PROJECT_DEPENDENCIES_CAPABILITY,
48
49
  RUNTIME_HEALTH_PROBE_CAPABILITY,
@@ -50,6 +51,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
50
51
  "agent_ability_workspace_v1",
51
52
  "agent_ability_runtime_assets_v1",
52
53
  ]);
54
+ /** 能力只按平台过滤,进程存活期间静态(CLAUDE.md 红线)。Windows 在 junction/copy 真机 smoke 通过前不宣告 Project Skills。 */
53
55
  export const daemonCapabilities = (runtimePlatform = process.platform, readiness = NATIVE_PROJECT_WORKSPACE_READINESS) => {
54
56
  // Windows stays fail-closed until the native workspace adapter gate is verified on a real host.
55
57
  const nativeWorkspacePlatform = runtimePlatform === "darwin" || runtimePlatform === "linux";
@@ -59,6 +61,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
59
61
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
60
62
  && (nativeWorkspacePlatform
61
63
  || (capability !== "project_skills_v1"
64
+ && capability !== "project_skill_projection_v2"
62
65
  && capability !== "agent_ability_release_v1"
63
66
  && capability !== "agent_ability_workspace_v1"
64
67
  && capability !== "agent_ability_runtime_assets_v1")))));
package/dist/main.js CHANGED
@@ -30,6 +30,7 @@ import { storageMigrationStatusReplay } from "./storage-status-replay.js";
30
30
  import { runStorageWorkerCommand } from "./storage-worker-command.js";
31
31
  import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
32
32
  import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
33
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
33
34
  async function main() {
34
35
  const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
35
36
  if (workerResult !== null) {
@@ -73,8 +74,16 @@ async function main() {
73
74
  if (values["daemon-home"])
74
75
  process.env.CREW_DAEMON_HOME = values["daemon-home"];
75
76
  let activeDaemonHome;
77
+ const localHome = resolveDaemonLocalStateRoot({
78
+ userHome: homedir(),
79
+ platform: process.platform,
80
+ explicit: process.env.NOWWORK_LOCAL_HOME,
81
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE
82
+ ?? process.env.NOWWORK_STORAGE_MODE
83
+ ?? (values["daemon-home"] === undefined ? undefined : "legacy"),
84
+ });
76
85
  if (values.profile) {
77
- const home = daemonHome();
86
+ const home = daemonHome(process.env, values.profile);
78
87
  activeDaemonHome = home;
79
88
  const profile = await loadProfile(values.profile, home);
80
89
  if (cmd === "serve") {
@@ -83,6 +92,7 @@ async function main() {
83
92
  platform: process.platform,
84
93
  profile: profile.name,
85
94
  userHome: homedir(),
95
+ localHome,
86
96
  uid: process.getuid?.(),
87
97
  daemonHome: home,
88
98
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
@@ -141,7 +151,7 @@ async function main() {
141
151
  : daemonGlobalInstallation(builtDaemonEntry(), process.platform);
142
152
  const installationLease = installation === null
143
153
  ? null
144
- : await acquireDaemonInstallationLease(installation.npmPrefix);
154
+ : await acquireDaemonInstallationLease(installation.npmPrefix, { localHome });
145
155
  const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
146
156
  ? { ready: false, reason: "not_legacy_source" }
147
157
  : await probeStorageMigrationRuntime({
@@ -198,6 +208,7 @@ async function main() {
198
208
  await runWithDaemonInstallationLease(installationLease, async () => {
199
209
  const service = serve(runtimeConfig, {
200
210
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
211
+ localHome,
201
212
  ...(storageMigration === undefined ? {} : { storageMigration }),
202
213
  });
203
214
  await runServeLifecycle(service);
@@ -16,6 +16,7 @@ export async function inspectManagedServiceDiagnostics(input) {
16
16
  const inspectLease = input.inspectInstallationLease
17
17
  ?? (() => inspectDaemonInstallationLease(input.expectedRecord.npmPrefix, {
18
18
  ...(userHome === undefined ? {} : { userHome }),
19
+ ...(registry.localHome === undefined ? {} : { localHome: registry.localHome }),
19
20
  }));
20
21
  let services = null;
21
22
  let registryCheck;
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { dirname, isAbsolute, resolve } from "node:path";
6
6
  import { z } from "zod";
7
7
  import { acquireProfileSaveLock } from "./computer-profile-lock.js";
8
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
8
9
  const AbsolutePathSchema = z.string().min(1).refine(isAbsolute, "must be an absolute path");
9
10
  const ManagedServiceRecordSchema = z.object({
10
11
  version: z.literal(1),
@@ -42,11 +43,15 @@ export class ManagedServiceConflictError extends ManagedServiceRegistryError {
42
43
  this.owner = owner;
43
44
  }
44
45
  }
45
- function registryDirectory(userHome) {
46
- return resolve(userHome, ".crew", "daemon", "managed-services");
46
+ function registryDirectory(userHome, localHome) {
47
+ const root = localHome ?? resolveDaemonLocalStateRoot({
48
+ userHome,
49
+ platform: process.platform,
50
+ });
51
+ return resolve(root, "daemon", "managed-services");
47
52
  }
48
- export function managedServiceRegistryPath(userHome = homedir()) {
49
- return resolve(registryDirectory(userHome), "registry.json");
53
+ export function managedServiceRegistryPath(userHome = homedir(), localHome) {
54
+ return resolve(registryDirectory(userHome, localHome), "registry.json");
50
55
  }
51
56
  export async function listManagedServiceDescriptorPaths(platform, userHome = homedir()) {
52
57
  const directory = platform === "darwin"
@@ -219,14 +224,14 @@ export function managedServiceConflict(services, candidate) {
219
224
  }
220
225
  export async function withManagedServiceRegistryLock(options, operation) {
221
226
  const userHome = options.userHome ?? homedir();
222
- const directory = registryDirectory(userHome);
227
+ const directory = registryDirectory(userHome, options.localHome);
223
228
  const release = await acquireProfileSaveLock(directory, {
224
229
  ...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
225
230
  ...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
226
231
  });
227
232
  let operationError;
228
233
  try {
229
- const path = managedServiceRegistryPath(userHome);
234
+ const path = managedServiceRegistryPath(userHome, options.localHome);
230
235
  const services = await readRegistryFile(path);
231
236
  return await operation({
232
237
  services,
@@ -258,7 +263,7 @@ export async function withManagedServiceRegistryLock(options, operation) {
258
263
  }
259
264
  }
260
265
  export async function readManagedServiceRegistry(options = {}) {
261
- return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir()));
266
+ return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir(), options.localHome));
262
267
  }
263
268
  export async function registerManagedService(input, options = {}) {
264
269
  const candidate = normalizeManagedServiceRecord(input);
@@ -17,7 +17,10 @@ export async function assertManagedServiceStartup(input) {
17
17
  ...(input.environment === undefined ? {} : { environment: input.environment }),
18
18
  });
19
19
  const readRegistry = input.readManagedServices
20
- ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
20
+ ?? (() => readManagedServiceRegistry({
21
+ userHome: input.userHome,
22
+ ...(input.localHome === undefined ? {} : { localHome: input.localHome }),
23
+ }));
21
24
  const listDescriptors = input.listManagedDescriptorPaths
22
25
  ?? (() => listManagedServiceDescriptorPaths(input.platform, input.userHome));
23
26
  const readDescriptor = input.readServiceDescriptor ?? readServiceDescriptor;
@@ -0,0 +1,9 @@
1
+ import { posix, win32 } from "node:path";
2
+ /** Execute POSIX npm through its owning Node binary so service PATH is irrelevant. */
3
+ export function npmInvocation(platform, nodePath = process.execPath) {
4
+ const paths = platform === "win32" ? win32 : posix;
5
+ const npmPath = paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
6
+ return platform === "win32"
7
+ ? { command: npmPath, argsPrefix: [] }
8
+ : { command: nodePath, argsPrefix: [npmPath] };
9
+ }
@@ -1,11 +1,9 @@
1
1
  import { z } from "zod";
2
- import { projectSkillResolutionRecords, scanProjects } from "./scanner.js";
2
+ import { scanProjects } from "./scanner.js";
3
3
  import { ProjectRegistryError } from "./registry.js";
4
4
  import { isProjectId, isProjectSkillName, compareProjectSkillRefs, MAX_AGENT_PROJECT_SKILL_BINDINGS, } from "./types.js";
5
- import { ProjectProjectionError, } from "./reconciler.js";
6
5
  import { AgentHandleSchema } from "../execution-protocol.js";
7
6
  import { createPromiseTail } from "../promise-tail.js";
8
- import { createProjectSkillSnapshotEpoch, MAX_REMEMBERED_PROJECT_SKILL_AGENTS, toPathPrivateConvergenceResults, } from "./controller-convergence.js";
9
7
  const ProjectIdSchema = z.string().refine(isProjectId);
10
8
  const AgentSkillsSyncSchema = z.object({
11
9
  type: z.literal("agent:skills:sync"),
@@ -50,7 +48,7 @@ const ProjectCommandSchema = z.discriminatedUnion("type", [
50
48
  }
51
49
  });
52
50
  const DEFAULT_PROJECT_SCAN_TIMEOUT_MS = 10_000;
53
- class ProjectScanTimeoutError extends Error {
51
+ export class ProjectScanTimeoutError extends Error {
54
52
  code = "project_scan_timeout";
55
53
  constructor() {
56
54
  super("project_scan_timeout");
@@ -87,7 +85,6 @@ export function createProjectSkillsController(deps) {
87
85
  let initializationState = "idle";
88
86
  const scan = deps.scan ?? scanProjects;
89
87
  const scanTimeoutMs = deps.scanTimeoutMs ?? DEFAULT_PROJECT_SCAN_TIMEOUT_MS;
90
- const snapshotEpoch = createProjectSkillSnapshotEpoch();
91
88
  let connectionEpochIdentity = Object.freeze({});
92
89
  const assertCurrentEpoch = (token) => {
93
90
  if (token !== undefined && token !== connectionEpochIdentity)
@@ -110,51 +107,6 @@ export function createProjectSkillsController(deps) {
110
107
  }));
111
108
  };
112
109
  const enqueue = (operation) => operationTail.enqueue(operation);
113
- const applyV2Snapshot = async (handle, snapshot, epochToken) => {
114
- if (deps.ensureSnapshot === undefined)
115
- throw new ProjectProjectionError("skill_projection_failed");
116
- assertCurrentEpoch(epochToken);
117
- const acceptedEpochIdentity = connectionEpochIdentity;
118
- const rememberedEntries = snapshotEpoch.entries();
119
- const firstAcceptedInEpoch = !rememberedEntries.some(([candidate]) => candidate === handle);
120
- if (firstAcceptedInEpoch && rememberedEntries.length >= MAX_REMEMBERED_PROJECT_SKILL_AGENTS) {
121
- throw new ProjectProjectionError("skill_projection_failed");
122
- }
123
- if (firstAcceptedInEpoch) {
124
- await deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }));
125
- }
126
- else {
127
- await deps.ensureSnapshot(handle, snapshot);
128
- }
129
- assertCurrentEpoch(epochToken);
130
- if (acceptedEpochIdentity === connectionEpochIdentity)
131
- snapshotEpoch.remember(handle, snapshot);
132
- return Object.freeze(projectSkillResolutionRecords(snapshot.bindings, scanned)
133
- .map((record) => Object.freeze({
134
- projectId: record.projectId,
135
- skillName: record.skillName,
136
- status: record.mode === "resolved" ? "linked" : "unavailable",
137
- })));
138
- };
139
- const convergeV2Snapshots = async (refreshCopyProjection, epochToken) => {
140
- assertCurrentEpoch(epochToken);
141
- const acceptedEpochIdentity = connectionEpochIdentity;
142
- const entries = snapshotEpoch.entries();
143
- if (deps.ensureSnapshot === undefined || entries.length === 0)
144
- return;
145
- const results = await Promise.allSettled(entries.map(([handle, snapshot]) => refreshCopyProjection
146
- ? deps.ensureSnapshot(handle, snapshot, Object.freeze({ refreshCopyProjection: true }))
147
- : deps.ensureSnapshot(handle, snapshot)));
148
- if (acceptedEpochIdentity !== connectionEpochIdentity) {
149
- if (epochToken !== undefined)
150
- throw new ProjectSkillStaleEpochError();
151
- return;
152
- }
153
- try {
154
- deps.onConvergence?.(toPathPrivateConvergenceResults(entries, results));
155
- }
156
- catch { /* convergence diagnostics must not change a successful inventory mutation */ }
157
- };
158
110
  const initialize = () => {
159
111
  if (initializationState === "ready" && initialization !== null)
160
112
  return initialization;
@@ -173,10 +125,10 @@ export function createProjectSkillsController(deps) {
173
125
  return {
174
126
  beginConnectionEpoch: () => {
175
127
  connectionEpochIdentity = Object.freeze({});
176
- snapshotEpoch.begin();
177
128
  return connectionEpochIdentity;
178
129
  },
179
130
  initialize,
131
+ ensureScanned: () => initialize(),
180
132
  publishCurrent: () => enqueue(publishCurrent),
181
133
  scannedProjects: () => scanned,
182
134
  async handle(input, epochToken) {
@@ -208,25 +160,9 @@ export function createProjectSkillsController(deps) {
208
160
  try {
209
161
  assertCurrentEpoch(epochToken);
210
162
  if (command.type === "agent:skills:sync") {
211
- if (command.generation !== undefined) {
212
- const snapshot = Object.freeze({
213
- bindings: Object.freeze(command.bindings.map((binding) => Object.freeze({ ...binding }))),
214
- generation: command.generation,
215
- });
216
- return {
217
- ok: true,
218
- data: { bindings: await applyV2Snapshot(command.handle, snapshot, epochToken) },
219
- };
220
- }
221
- if (deps.reconcile === undefined)
222
- return { ok: false, error: "skill_projection_failed" };
223
163
  const bindings = await deps.reconcile(command.handle, command.bindings);
224
164
  assertCurrentEpoch(epochToken);
225
- snapshotEpoch.forget(command.handle);
226
- return {
227
- ok: true,
228
- data: { bindings },
229
- };
165
+ return { ok: true, data: { bindings } };
230
166
  }
231
167
  if (command.type === "project:add") {
232
168
  const existed = (await deps.registry.list())
@@ -260,7 +196,6 @@ export function createProjectSkillsController(deps) {
260
196
  assertCurrentEpoch(epochToken);
261
197
  await publishCurrent();
262
198
  assertCurrentEpoch(epochToken);
263
- await convergeV2Snapshots(command.type === "project:rescan", epochToken);
264
199
  return { ok: true, data: { projectId: command.projectId } };
265
200
  }
266
201
  catch (error) {
@@ -274,8 +209,8 @@ export function createProjectSkillsController(deps) {
274
209
  ? "skill_projection_stale"
275
210
  : error.code === "skill_projection_failed"
276
211
  ? "skill_projection_failed"
277
- : error.code === "skill_projection_snapshot_corrupt"
278
- ? "skill_projection_snapshot_corrupt"
212
+ : error.code === "project_skills_unavailable"
213
+ ? "project_skills_unavailable"
279
214
  : "project_operation_failed",
280
215
  };
281
216
  }
@@ -8,4 +8,11 @@ export function projectSkillExecutionProjection(bindings, generation) {
8
8
  generation,
9
9
  });
10
10
  }
11
+ /**
12
+ * 投影门控与 ownership 判定的唯一谓词:只有绑定了 Project Skills 且运行时是 codex/claude 的执行
13
+ * 才会有投影目录,也才需要在停止无法证明时保留它。两处判定分开写过一次,结果 kimi + projectSkills
14
+ * 会在取消路径上错抛 ownership 未确认。
15
+ */
16
+ export const projectSkillProjectionApplies = (spec) => spec.agent.projectSkills !== undefined
17
+ && (spec.runtime.name === "codex" || spec.runtime.name === "claude");
11
18
  export const projectSkillProjectionErrorCode = (error) => error instanceof ProjectProjectionError ? error.code : null;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * 按执行投影:每次 execution 在 Agent 目录下独占一个短命目录,投影绑定的 Project Skills;
3
+ * 用完即删,启动时按 journal 清扫。没有共享状态,所以没有 manifest / 目录身份 / 租约 / GC。
4
+ * (取代 2026-08-21 版本化 runtime root;incident: 2026-09-04 st_dev 漂移摘掉整机能力)
5
+ */
6
+ import { lstat, mkdir, readdir, rm } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { DirectoryProjectionError, projectDirectory } from "../directory-projection.js";
9
+ /** 稳定错误:code 与既有 skill_projection_failed 对齐,reason 是 path-free 的细分原因。 */
10
+ export class ExecutionProjectionError extends Error {
11
+ reason;
12
+ code = "skill_projection_failed";
13
+ constructor(reason) {
14
+ super(`execution projection failed: ${reason}`);
15
+ this.reason = reason;
16
+ this.name = "ExecutionProjectionError";
17
+ }
18
+ }
19
+ export const EXECUTION_PROJECTION_PARENT = Object.freeze([".crew", "project-skills"]);
20
+ const DIRECTORY_PREFIX = "exec-";
21
+ const SAFE_EXECUTION_ID = /^[A-Za-z0-9._-]{1,128}$/u;
22
+ const projectionParent = (agentRoot) => join(agentRoot, ...EXECUTION_PROJECTION_PARENT);
23
+ export function executionProjectionDescriptor(agentRoot, executionId) {
24
+ if (!SAFE_EXECUTION_ID.test(executionId) || executionId === "." || executionId === "..") {
25
+ throw new ExecutionProjectionError("unmanaged_path");
26
+ }
27
+ const rootDirectory = join(projectionParent(agentRoot), `${DIRECTORY_PREFIX}${executionId}`);
28
+ const runtime = join(rootDirectory, "runtime");
29
+ return Object.freeze({
30
+ executionId,
31
+ rootDirectory,
32
+ codexSkillRoot: join(runtime, ".claude", "skills"),
33
+ claudeAdditionalDirectory: runtime,
34
+ });
35
+ }
36
+ const ownedByCurrentUser = (uid) => process.getuid === undefined || uid === process.getuid();
37
+ /** 每一层都必须是本用户的真实目录;symlink / 他人目录 → unmanaged_path。 */
38
+ const assertManagedDirectory = async (path) => {
39
+ let info;
40
+ try {
41
+ info = await lstat(path);
42
+ }
43
+ catch {
44
+ throw new ExecutionProjectionError("unmanaged_path");
45
+ }
46
+ if (!info.isDirectory() || info.isSymbolicLink() || !ownedByCurrentUser(info.uid)) {
47
+ throw new ExecutionProjectionError("unmanaged_path");
48
+ }
49
+ };
50
+ const isErrno = (error, code) => typeof error === "object" && error !== null && error.code === code;
51
+ export async function materializeExecutionProjection(input) {
52
+ const platform = input.platform ?? process.platform;
53
+ const project = input.projectDirectory ?? projectDirectory;
54
+ const root = executionProjectionDescriptor(input.agentRoot, input.executionId);
55
+ const parent = projectionParent(input.agentRoot);
56
+ // 逐层 assert-then-create:先证明这一层是本用户的真实目录,再在其下创建下一层。
57
+ // 反过来(先 recursive mkdir 再 assert)会让 `.crew` 是 symlink 的情况先在别人的树里建好目录。
58
+ const crewRoot = join(input.agentRoot, EXECUTION_PROJECTION_PARENT[0]);
59
+ await assertManagedDirectory(input.agentRoot);
60
+ try {
61
+ await mkdir(crewRoot, { recursive: true, mode: 0o700 });
62
+ }
63
+ catch {
64
+ throw new ExecutionProjectionError("mkdir_failed");
65
+ }
66
+ await assertManagedDirectory(crewRoot);
67
+ try {
68
+ await mkdir(parent, { recursive: true, mode: 0o700 });
69
+ }
70
+ catch {
71
+ throw new ExecutionProjectionError("mkdir_failed");
72
+ }
73
+ await assertManagedDirectory(parent);
74
+ try {
75
+ await mkdir(root.rootDirectory, { mode: 0o700 });
76
+ }
77
+ catch (error) {
78
+ throw new ExecutionProjectionError(isErrno(error, "EEXIST") ? "execution_projection_exists" : "mkdir_failed");
79
+ }
80
+ const modes = [];
81
+ try {
82
+ await mkdir(root.codexSkillRoot, { recursive: true, mode: 0o700 });
83
+ for (const record of input.resolutions) {
84
+ if (record.mode !== "resolved" || record.sourcePath === null)
85
+ continue;
86
+ let projected;
87
+ try {
88
+ // `projectDirectory`'s symlink path never lstats the source (POSIX symlink() happily
89
+ // creates a dangling link), so a source that vanished between scan and materialize would
90
+ // otherwise be silently projected as a broken link instead of failing loudly.
91
+ const sourceExists = await lstat(record.sourcePath).then(() => true, () => false);
92
+ if (!sourceExists)
93
+ throw new ExecutionProjectionError("link_failed");
94
+ projected = await project(record.sourcePath, join(root.codexSkillRoot, record.skillName), platform);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof ExecutionProjectionError)
98
+ throw error;
99
+ const copying = platform === "win32" && error instanceof DirectoryProjectionError
100
+ && error.code === "directory_projection_limit_exceeded";
101
+ throw new ExecutionProjectionError(copying ? "copy_failed" : "link_failed");
102
+ }
103
+ modes.push({ skillName: record.skillName, mode: projected.mode });
104
+ }
105
+ }
106
+ catch (error) {
107
+ await rm(root.rootDirectory, { recursive: true, force: true }).catch(() => undefined);
108
+ if (error instanceof ExecutionProjectionError)
109
+ throw error;
110
+ throw new ExecutionProjectionError("mkdir_failed");
111
+ }
112
+ return Object.freeze({ root, modes: Object.freeze(modes) });
113
+ }
114
+ export async function removeExecutionProjection(agentRoot, executionId) {
115
+ const root = executionProjectionDescriptor(agentRoot, executionId);
116
+ await rm(root.rootDirectory, { recursive: true, force: true });
117
+ }
118
+ export async function sweepExecutionProjections(agentRoot, isLive) {
119
+ const parent = projectionParent(agentRoot);
120
+ const parentExists = await lstat(parent).then(() => true, (error) => {
121
+ if (isErrno(error, "ENOENT"))
122
+ return false;
123
+ throw error;
124
+ });
125
+ if (!parentExists)
126
+ return Object.freeze({ removed: Object.freeze([]), kept: Object.freeze([]) });
127
+ // Every layer must still be a real, owned directory — a symlink swapped in for `.crew` or
128
+ // `.crew/project-skills` (or the agent root itself) must never let readdir/rm follow it out
129
+ // of the managed tree and delete something outside it.
130
+ await assertManagedDirectory(agentRoot);
131
+ await assertManagedDirectory(join(agentRoot, EXECUTION_PROJECTION_PARENT[0]));
132
+ await assertManagedDirectory(parent);
133
+ const entries = await readdir(parent, { withFileTypes: true });
134
+ const removed = [];
135
+ const kept = [];
136
+ for (const entry of entries) {
137
+ if (!entry.isDirectory() || !entry.name.startsWith(DIRECTORY_PREFIX))
138
+ continue;
139
+ const candidate = join(parent, entry.name);
140
+ const candidateInfo = await lstat(candidate).catch(() => null);
141
+ if (candidateInfo === null || candidateInfo.isSymbolicLink())
142
+ continue;
143
+ const executionId = entry.name.slice(DIRECTORY_PREFIX.length);
144
+ if (!SAFE_EXECUTION_ID.test(executionId))
145
+ continue;
146
+ let live;
147
+ try {
148
+ live = await isLive(executionId);
149
+ }
150
+ catch {
151
+ // An `isLive` failure is ambiguous, not evidence of death — err toward keeping the
152
+ // directory rather than deleting a possibly-live execution's projection.
153
+ live = true;
154
+ }
155
+ if (live) {
156
+ kept.push(executionId);
157
+ continue;
158
+ }
159
+ await rm(candidate, { recursive: true, force: true });
160
+ removed.push(executionId);
161
+ }
162
+ return Object.freeze({ removed: Object.freeze(removed.sort()), kept: Object.freeze(kept.sort()) });
163
+ }
@@ -1,18 +1,20 @@
1
- import { ProjectProjectionError, } from "./reconciler.js";
2
- /** Gates every projection entrypoint on the daemon's bounded registry/scanner initialization. */
3
- export function initializedProjectSkillsReconciler(ensureInitialized, reconciler) {
1
+ import { ProjectProjectionError } from "./reconciler.js";
2
+ /** 执行前等待注册表首次扫描;超时/失败 → 本次执行 project_skills_unavailable,不影响能力宣告。 */
3
+ export function initializedProjectSkillsReconciler(ensureScanned, reconciler) {
4
4
  const ready = async () => {
5
- if (await ensureInitialized())
5
+ try {
6
+ await ensureScanned();
6
7
  return reconciler;
7
- throw new ProjectProjectionError("skill_projection_failed");
8
+ }
9
+ catch (error) {
10
+ const code = error.code;
11
+ throw new ProjectProjectionError("project_skills_unavailable", code === "project_scan_timeout" ? "registry_scan_timeout" : "registry_unavailable");
12
+ }
8
13
  };
9
14
  return {
10
15
  async reconcile(handle, bindings) {
11
16
  return (await ready()).reconcile(handle, bindings);
12
17
  },
13
- async ensureSnapshot(handle, snapshot, options) {
14
- return (await ready()).ensureSnapshot(handle, snapshot, options);
15
- },
16
18
  async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
17
19
  return (await ready()).prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning);
18
20
  },