@nowcrew/daemon 0.6.0 → 0.6.1

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.
@@ -3,8 +3,8 @@ import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symli
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { parse } from "yaml";
5
5
  import { dslog } from "../slog.js";
6
- import { abilitySha256, abilityTreeDigest, collectAbilityFiles, safeAbilityName, } from "./resolver.js";
7
- import { DaemonAbilityLockSchema, DaemonAbilityManifestSchema } from "./types.js";
6
+ import { abilitySha256, abilityTreeDigest, collectAbilityFiles, resolveAbilityRelease, safeAbilityName, } from "./resolver.js";
7
+ import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema } from "./types.js";
8
8
  const exists = (path) => lstat(path).then(() => true, () => false);
9
9
  const repositoryKey = (url) => createHash("sha256").update(url).digest("hex");
10
10
  const posixPath = (value) => value.split(sep).join("/");
@@ -48,18 +48,20 @@ const switchDirectory = async (target, staging, backup) => {
48
48
  if (hadPrevious)
49
49
  await rename(target, backup);
50
50
  await rename(staging, target);
51
- if (hadPrevious)
51
+ if (hadPrevious) {
52
+ await writableTree(backup);
52
53
  await rm(backup, { recursive: true, force: true });
54
+ }
53
55
  }
54
56
  catch (error) {
55
57
  if (await exists(target))
56
- await rm(target, { recursive: true, force: true }).catch(() => { });
58
+ await removeManagedTree(target).catch(() => { });
57
59
  if (hadPrevious && await exists(backup))
58
60
  await rename(backup, target).catch(() => { });
59
61
  throw error;
60
62
  }
61
63
  };
62
- const copyExistingSkills = async (target, staging, managedReleaseRoot) => {
64
+ const copyExistingSkills = async (target, staging, managedRoots) => {
63
65
  const names = new Set();
64
66
  if (!await exists(target))
65
67
  return names;
@@ -70,7 +72,7 @@ const copyExistingSkills = async (target, staging, managedReleaseRoot) => {
70
72
  if (info.isSymbolicLink()) {
71
73
  const link = await readlink(source);
72
74
  const resolvedLink = resolve(dirname(source), link);
73
- if (resolvedLink === managedReleaseRoot || resolvedLink.startsWith(`${managedReleaseRoot}${sep}`))
75
+ if (managedRoots.some((root) => resolvedLink === root || resolvedLink.startsWith(`${root}${sep}`)))
74
76
  continue;
75
77
  await symlink(link, destination, "dir");
76
78
  }
@@ -80,10 +82,10 @@ const copyExistingSkills = async (target, staging, managedReleaseRoot) => {
80
82
  }
81
83
  return names;
82
84
  };
83
- const projectNativeSkills = async (agentRoot, releaseRoot, skills) => {
85
+ const projectNativeSkills = async (agentRoot, trainingRoot, skills) => {
84
86
  const nativeSkills = [];
85
87
  for (const skill of skills) {
86
- const source = join(releaseRoot, "skills", safeAbilityName(skill.name));
88
+ const source = join(trainingRoot, "skills", safeAbilityName(skill.name));
87
89
  if (await exists(join(source, "SKILL.md"))) {
88
90
  nativeSkills.push({ name: skill.name, source });
89
91
  continue;
@@ -99,14 +101,14 @@ const projectNativeSkills = async (agentRoot, releaseRoot, skills) => {
99
101
  join(agentRoot, ".agents", "skills"),
100
102
  join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
101
103
  ];
102
- const managedReleaseRoot = join(agentRoot, ".nowcrew", "ability", "releases");
104
+ const managedRoots = [trainingRoot, join(agentRoot, ".nowcrew", "ability", "releases")];
103
105
  for (const target of targets) {
104
106
  const staging = join(dirname(target), `.ability-skills-next-${id}`);
105
107
  const backup = join(dirname(target), `.ability-skills-previous-${id}`);
106
108
  await mkdir(dirname(target), { recursive: true });
107
109
  await mkdir(staging, { mode: 0o700 });
108
110
  try {
109
- const names = await copyExistingSkills(target, staging, managedReleaseRoot);
111
+ const names = await copyExistingSkills(target, staging, managedRoots);
110
112
  for (const skill of nativeSkills) {
111
113
  if (names.has(skill.name))
112
114
  throw new Error(`ability_skill_name_conflict:${skill.name}`);
@@ -192,24 +194,56 @@ const verifyCache = async (agentsRoot, ability) => {
192
194
  if (ability.memory.reduce((bytes, memory) => bytes + Buffer.byteLength(memory.content, "utf8"), 0) > 64 * 1024) {
193
195
  throw new Error("ability_memory_snapshot_size_exceeded");
194
196
  }
195
- return root;
197
+ return { root, manifest };
196
198
  };
197
- const materializeRelease = async (agentsRoot, handle, ability) => {
198
- const sourceRoot = await verifyCache(agentsRoot, ability);
199
+ const layerRelativePath = (path, layer) => path.startsWith(`${layer}/`) ? path.slice(layer.length + 1) : path;
200
+ const readManagedCatalog = async (trainingRoot) => JSON.parse(await readFile(join(trainingRoot, "ACTIVE_RELEASE.json"), "utf8"));
201
+ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
202
+ let verified;
203
+ try {
204
+ verified = await verifyCache(agentsRoot, ability);
205
+ }
206
+ catch (error) {
207
+ if (!(error instanceof Error) || error.message !== "ability_artifact_cache_missing")
208
+ throw error;
209
+ const resolved = await resolveAbilityRelease(agentsRoot, {
210
+ type: "agent:ability:resolve",
211
+ reqId: `workspace-apply-${ability.releaseId}`,
212
+ handle,
213
+ repositoryUrl: ability.repositoryUrl,
214
+ branchGlob: ability.branch,
215
+ desiredCommit: ability.rootCommit,
216
+ });
217
+ if (resolved.artifactDigest !== ability.artifactDigest || resolved.rootCommit !== ability.rootCommit) {
218
+ throw new Error("ability_workspace_projection_source_mismatch");
219
+ }
220
+ verified = await verifyCache(agentsRoot, ability);
221
+ }
222
+ const sourceRoot = verified.root;
199
223
  const agentRoot = join(agentsRoot, handle);
200
- const abilityRoot = join(agentRoot, ".nowcrew", "ability");
201
- const releaseRoot = join(abilityRoot, "releases", ability.releaseId);
202
- const catalogPath = join(releaseRoot, "catalog.json");
203
- if (await exists(catalogPath)) {
204
- const catalog = JSON.parse(await readFile(catalogPath, "utf8"));
205
- if (catalog.artifactDigest === ability.artifactDigest)
206
- return releaseRoot;
207
- await removeManagedTree(releaseRoot);
224
+ const trainingRoot = join(agentRoot, "training");
225
+ const projectionDigest = abilitySha256(JSON.stringify({
226
+ artifactDigest: ability.artifactDigest,
227
+ memory: ability.memory,
228
+ workspace: ability.workspace,
229
+ }));
230
+ let previousCatalog = null;
231
+ if (await exists(trainingRoot)) {
232
+ previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
233
+ if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
234
+ throw new Error("ability_workspace_training_conflict");
235
+ }
236
+ if (previousCatalog.releaseId === ability.releaseId
237
+ && previousCatalog.artifactDigest === ability.artifactDigest
238
+ && previousCatalog.projectionDigest === projectionDigest)
239
+ return trainingRoot;
208
240
  }
209
- const staging = join(abilityRoot, "releases", `.next-${ability.releaseId}-${randomUUID()}`);
241
+ await mkdir(agentRoot, { recursive: true });
242
+ const staging = join(agentRoot, `.training-next-${ability.releaseId}-${randomUUID()}`);
243
+ const backup = join(agentRoot, `.training-previous-${ability.releaseId}-${randomUUID()}`);
210
244
  await mkdir(staging, { recursive: true, mode: 0o700 });
211
245
  try {
212
- const instructionsTarget = join(staging, "instructions", ability.instructions.path);
246
+ const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
213
247
  await mkdir(dirname(instructionsTarget), { recursive: true });
214
248
  await cp(join(sourceRoot, ability.instructions.path), instructionsTarget, { preserveTimestamps: true });
215
249
  for (const skill of ability.skills) {
@@ -223,105 +257,127 @@ const materializeRelease = async (agentsRoot, handle, ability) => {
223
257
  await writeFile(target, memory.content, { encoding: "utf8", mode: 0o600 });
224
258
  }
225
259
  for (const asset of ability.workspace) {
226
- const relativeTarget = asset.target.replace(/^\.nowcrew\/ability\//u, "");
260
+ const relativeTarget = abilityWorkspaceProjectionPath(asset.target);
227
261
  const target = join(staging, "workspace", relativeTarget);
228
262
  await mkdir(dirname(target), { recursive: true });
229
- await cp(join(sourceRoot, asset.source), target, { recursive: true, preserveTimestamps: true });
263
+ const previous = join(trainingRoot, "workspace", relativeTarget);
264
+ const source = asset.mode === "managed-copy" && previousCatalog !== null && await exists(previous)
265
+ ? previous
266
+ : join(sourceRoot, asset.source);
267
+ await cp(source, target, { recursive: true, preserveTimestamps: true });
268
+ }
269
+ const evalPaths = [verified.manifest.spec.evals.policy, ...verified.manifest.spec.evals.cases];
270
+ for (const path of evalPaths) {
271
+ const target = join(staging, "evals", layerRelativePath(path, "evals"));
272
+ await mkdir(dirname(target), { recursive: true });
273
+ await cp(join(sourceRoot, path), target, { recursive: true, preserveTimestamps: true });
230
274
  }
231
- await writeFile(join(staging, "catalog.json"), JSON.stringify({
275
+ await writeFile(join(staging, "ACTIVE_RELEASE.json"), JSON.stringify({
276
+ managedBy: "nowcrew-agent-training",
232
277
  releaseId: ability.releaseId,
233
278
  rootCommit: ability.rootCommit,
234
279
  artifactDigest: ability.artifactDigest,
235
- instructions: ability.instructions.path,
280
+ projectionDigest,
281
+ instructions: `training/instructions/${layerRelativePath(ability.instructions.path, "instructions")}`,
236
282
  skills: ability.skills.map((skill) => skill.name),
237
283
  memory: ability.memory.map((memory) => ({ id: memory.id, layer: memory.layer })),
238
- workspace: ability.workspace,
239
- evalProvenance: ability.evalProvenance,
284
+ workspace: ability.workspace.map((asset) => ({
285
+ ...asset,
286
+ projectedPath: `training/workspace/${abilityWorkspaceProjectionPath(asset.target)}`,
287
+ })),
288
+ evals: evalPaths.map((path) => `training/evals/${layerRelativePath(path, "evals")}`),
240
289
  }, null, 2), { encoding: "utf8", mode: 0o600 });
241
290
  await readonlyTree(staging);
242
- await rename(staging, releaseRoot);
291
+ for (const asset of ability.workspace) {
292
+ if (asset.mode === "managed-copy") {
293
+ await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
294
+ }
295
+ }
296
+ await switchDirectory(trainingRoot, staging, backup);
243
297
  }
244
298
  catch (error) {
245
299
  await removeManagedTree(staging).catch(() => { });
246
300
  throw error;
247
301
  }
248
- return releaseRoot;
302
+ return trainingRoot;
249
303
  };
250
- const activatePointerAndWorkspace = async (agentsRoot, handle, ability, releaseRoot) => {
304
+ const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
251
305
  const agentRoot = join(agentsRoot, handle);
252
- const abilityRoot = join(agentRoot, ".nowcrew", "ability");
253
- const next = join(abilityRoot, `.current-next-${randomUUID()}`);
254
- const previous = join(abilityRoot, `.current-previous-${randomUUID()}`);
255
- await mkdir(abilityRoot, { recursive: true });
256
- await symlink(releaseRoot, next, "dir");
257
- const current = join(abilityRoot, "current");
258
- const hadCurrent = await exists(current);
259
- if (hadCurrent)
260
- await rename(current, previous);
261
- try {
262
- await rename(next, current);
263
- if (hadCurrent)
264
- await rm(previous, { recursive: true, force: true });
265
- }
266
- catch (error) {
267
- if (hadCurrent)
268
- await rename(previous, current).catch(() => { });
269
- throw error;
306
+ await projectNativeSkills(agentRoot, trainingRoot, ability.skills);
307
+ const legacyAbilityRoot = join(agentRoot, ".nowcrew", "ability");
308
+ if (await exists(join(legacyAbilityRoot, "releases")) || await exists(join(legacyAbilityRoot, "current"))) {
309
+ await removeManagedTree(legacyAbilityRoot);
270
310
  }
271
- for (const asset of ability.workspace) {
272
- const relativeTarget = asset.target.replace(/^\.nowcrew\/ability\//u, "");
273
- const source = join(current, "workspace", relativeTarget);
274
- const target = join(agentRoot, ...asset.target.split("/"));
275
- await mkdir(dirname(target), { recursive: true });
276
- if (asset.mode === "managed-copy") {
277
- if (!await exists(target))
278
- await cp(source, target, { recursive: true, preserveTimestamps: true });
279
- continue;
280
- }
281
- const staging = `${target}.next-${randomUUID()}`;
282
- await symlink(source, staging, "dir");
283
- if (await exists(target))
284
- await rm(target, { recursive: true, force: true });
285
- await rename(staging, target);
286
- }
287
- await projectNativeSkills(agentRoot, releaseRoot, ability.skills);
288
311
  await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
289
312
  };
290
313
  export function createAgentAbilityMaterializer(expectedAgentsRoot) {
291
314
  const tails = new Map();
315
+ const withAgentTurn = async (handle, operation) => {
316
+ const previous = tails.get(handle) ?? Promise.resolve();
317
+ let release;
318
+ const turn = new Promise((resolveTurn) => { release = resolveTurn; });
319
+ const tail = previous.then(() => turn);
320
+ tails.set(handle, tail);
321
+ await previous;
322
+ try {
323
+ return await operation();
324
+ }
325
+ finally {
326
+ release();
327
+ if (tails.get(handle) === tail)
328
+ tails.delete(handle);
329
+ }
330
+ };
331
+ const apply = async (agentsRoot, handle, ability) => {
332
+ if (agentsRoot !== expectedAgentsRoot)
333
+ throw new Error("ability_agents_root_mismatch");
334
+ const trainingRoot = await materializeWorkspaceTraining(agentsRoot, handle, ability);
335
+ await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
336
+ return { directory: "training", releaseId: ability.releaseId, artifactDigest: ability.artifactDigest };
337
+ };
292
338
  return {
339
+ apply(agentsRoot, handle, ability) {
340
+ return withAgentTurn(handle, async () => {
341
+ const started = Date.now();
342
+ try {
343
+ const result = await apply(agentsRoot, handle, ability);
344
+ dslog("ability.workspace.projected", "Agent training projected into workspace", {
345
+ level: "INFO", agent_handle: handle, release_id: ability.releaseId,
346
+ root_commit: ability.rootCommit, artifact_digest: ability.artifactDigest,
347
+ workspace_directory: result.directory, duration_ms: Date.now() - started,
348
+ });
349
+ return result;
350
+ }
351
+ catch (error) {
352
+ dslog("ability.workspace.projection_failed", "Agent training workspace projection failed", {
353
+ level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
354
+ root_commit: ability.rootCommit,
355
+ error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_workspace_projection_failed",
356
+ });
357
+ throw error;
358
+ }
359
+ });
360
+ },
293
361
  async prepareAndLaunch(agentsRoot, handle, ability, launch) {
294
- if (agentsRoot !== expectedAgentsRoot)
295
- throw new Error("ability_agents_root_mismatch");
296
- const previous = tails.get(handle) ?? Promise.resolve();
297
- let release;
298
- const turn = new Promise((resolveTurn) => { release = resolveTurn; });
299
- const tail = previous.then(() => turn);
300
- tails.set(handle, tail);
301
- await previous;
302
- const started = Date.now();
303
- try {
304
- const releaseRoot = await materializeRelease(agentsRoot, handle, ability);
305
- await activatePointerAndWorkspace(agentsRoot, handle, ability, releaseRoot);
306
- dslog("ability.execution.materialized", "Agent ability release materialized", {
307
- level: "INFO", agent_handle: handle, release_id: ability.releaseId,
308
- root_commit: ability.rootCommit, artifact_digest: ability.artifactDigest,
309
- duration_ms: Date.now() - started,
310
- });
311
- return await launch();
312
- }
313
- catch (error) {
314
- dslog("ability.execution.rejected", "Agent ability release materialization failed", {
315
- level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
316
- root_commit: ability.rootCommit, error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_materialization_failed",
317
- });
318
- throw error;
319
- }
320
- finally {
321
- release();
322
- if (tails.get(handle) === tail)
323
- tails.delete(handle);
324
- }
362
+ return withAgentTurn(handle, async () => {
363
+ const started = Date.now();
364
+ try {
365
+ await apply(agentsRoot, handle, ability);
366
+ dslog("ability.execution.materialized", "Agent ability release materialized", {
367
+ level: "INFO", agent_handle: handle, release_id: ability.releaseId,
368
+ root_commit: ability.rootCommit, artifact_digest: ability.artifactDigest,
369
+ workspace_directory: "training", duration_ms: Date.now() - started,
370
+ });
371
+ return await launch();
372
+ }
373
+ catch (error) {
374
+ dslog("ability.execution.rejected", "Agent ability release materialization failed", {
375
+ level: "ERROR", agent_handle: handle, release_id: ability.releaseId,
376
+ root_commit: ability.rootCommit, error_code: error instanceof Error ? error.message.slice(0, 120) : "ability_materialization_failed",
377
+ });
378
+ throw error;
379
+ }
380
+ });
325
381
  },
326
382
  };
327
383
  }
@@ -5,7 +5,7 @@ import { dirname, extname, join, relative, resolve, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { parse } from "yaml";
7
7
  import { z } from "zod";
8
- import { DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
8
+ import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
9
9
  const runFile = promisify(execFile);
10
10
  const MAX_FILES = 5_000;
11
11
  const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
@@ -281,13 +281,14 @@ export const evaluateAbilityCases = (files, manifest, lock, memory) => {
281
281
  const workspaceReferenceResolves = (reference) => {
282
282
  if (!reference.startsWith("workspace://"))
283
283
  return false;
284
- const target = `.nowcrew/ability/${reference.slice("workspace://".length)}`;
284
+ const target = reference.slice("workspace://".length);
285
285
  return manifest.spec.workspace.assets.some((asset) => {
286
- if (target === asset.target)
286
+ const projection = abilityWorkspaceProjectionPath(asset.target);
287
+ if (target === projection)
287
288
  return filePaths.has(asset.source);
288
- if (!target.startsWith(`${asset.target}/`))
289
+ if (!target.startsWith(`${projection}/`))
289
290
  return false;
290
- return filePaths.has(`${asset.source}/${target.slice(asset.target.length + 1)}`);
291
+ return filePaths.has(`${asset.source}/${target.slice(projection.length + 1)}`);
291
292
  });
292
293
  };
293
294
  const assertions = [];
@@ -1,20 +1,53 @@
1
+ import { z } from "zod";
2
+ import { AbilityReleaseSnapshotSchema, AgentHandleSchema } from "../execution-protocol.js";
1
3
  import { createAgentAbilityController } from "./controller.js";
2
4
  import { createAgentAbilityMaterializer } from "./materializer.js";
3
- import { AGENT_ABILITY_CAPABILITY } from "./types.js";
5
+ import { AGENT_ABILITY_CAPABILITY, AGENT_ABILITY_WORKSPACE_CAPABILITY } from "./types.js";
6
+ const AbilityApplyCommandSchema = z.object({
7
+ type: z.literal("agent:ability:apply"),
8
+ reqId: z.string().min(1).max(128),
9
+ handle: AgentHandleSchema,
10
+ ability: AbilityReleaseSnapshotSchema,
11
+ }).strict();
4
12
  export function createAgentAbilityRuntime(agentsRoot, options = {}) {
5
13
  const controller = options.controller ?? createAgentAbilityController({ agentsRoot });
6
14
  const materializer = options.materializer ?? createAgentAbilityMaterializer(agentsRoot);
7
15
  return {
8
16
  materializer,
9
17
  async tryHandleControlMessage(input, serverCapabilities, send) {
10
- if (input?.type !== "agent:ability:resolve")
18
+ const type = input?.type;
19
+ if (type !== "agent:ability:resolve" && type !== "agent:ability:apply")
11
20
  return false;
12
21
  const reqId = input.reqId;
13
22
  if (typeof reqId !== "string")
14
23
  return true;
15
- const result = serverCapabilities.has(AGENT_ABILITY_CAPABILITY)
16
- ? await controller.handle(input)
17
- : { ok: false, error: "capability_unavailable" };
24
+ let result;
25
+ const requiredCapability = type === "agent:ability:apply"
26
+ ? AGENT_ABILITY_WORKSPACE_CAPABILITY
27
+ : AGENT_ABILITY_CAPABILITY;
28
+ if (!serverCapabilities.has(requiredCapability)) {
29
+ result = { ok: false, error: "capability_unavailable" };
30
+ }
31
+ else if (type === "agent:ability:resolve") {
32
+ result = await controller.handle(input);
33
+ }
34
+ else {
35
+ const parsed = AbilityApplyCommandSchema.safeParse(input);
36
+ if (!parsed.success)
37
+ result = { ok: false, error: "invalid_ability_apply_command" };
38
+ else {
39
+ try {
40
+ result = { ok: true, data: await materializer.apply(agentsRoot, parsed.data.handle, parsed.data.ability) };
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : "ability_workspace_projection_failed";
44
+ result = {
45
+ ok: false,
46
+ error: /^[a-z0-9_:-]+$/u.test(message) ? message.slice(0, 200) : "ability_workspace_projection_failed",
47
+ };
48
+ }
49
+ }
50
+ }
18
51
  try {
19
52
  send({ type: "fs:result", reqId, ...result });
20
53
  }
@@ -1,10 +1,17 @@
1
1
  import { z } from "zod";
2
2
  export const AGENT_ABILITY_CAPABILITY = "agent_ability_release_v1";
3
+ export const AGENT_ABILITY_WORKSPACE_CAPABILITY = "agent_ability_workspace_v1";
3
4
  const DigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
4
5
  const CommitSchema = z.string().regex(/^[a-f0-9]{40}$/u);
5
6
  export const SafeAbilityPathSchema = z.string().min(1).max(512).refine((value) => !value.startsWith("/") && !value.includes("\\") && !value.includes("\0")
6
7
  && value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".."), "unsafe ability path");
7
- export const WorkspaceAbilityTargetSchema = SafeAbilityPathSchema.refine((value) => value.startsWith(".nowcrew/ability/"), "training workspace targets must stay under .nowcrew/ability/");
8
+ export const WorkspaceAbilityTargetSchema = SafeAbilityPathSchema.refine((value) => value.startsWith("training/workspace/") || value.startsWith(".nowcrew/ability/"), "training workspace targets must stay under training/workspace/");
9
+ export function abilityWorkspaceProjectionPath(target) {
10
+ WorkspaceAbilityTargetSchema.parse(target);
11
+ return target.startsWith("training/workspace/")
12
+ ? target.slice("training/workspace/".length)
13
+ : target.slice(".nowcrew/ability/".length);
14
+ }
8
15
  export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).superRefine((value, ctx) => {
9
16
  const scpLike = /^git@[a-z0-9.-]+:[A-Za-z0-9._~/-]+(?:\.git)?$/iu.test(value);
10
17
  let allowedUrl = false;
@@ -30,12 +37,11 @@ export function matchesAbilityBranch(glob, branch) {
30
37
  let pattern = "^";
31
38
  for (let index = 0; index < glob.length; index += 1) {
32
39
  const character = glob[index];
33
- if (character === "*" && glob[index + 1] === "*") {
40
+ if (character === "*") {
34
41
  pattern += ".*";
35
- index += 1;
42
+ if (glob[index + 1] === "*")
43
+ index += 1;
36
44
  }
37
- else if (character === "*")
38
- pattern += "[^/]*";
39
45
  else
40
46
  pattern += character.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
41
47
  }
@@ -23,7 +23,7 @@ const ProjectSkillRefSchema = z.object({
23
23
  projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
24
24
  skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
25
25
  }).strict();
26
- const AbilityReleaseSnapshotSchema = z.object({
26
+ export const AbilityReleaseSnapshotSchema = z.object({
27
27
  bindingId: z.string().uuid(),
28
28
  releaseId: z.string().uuid(),
29
29
  repositoryUrl: AbilityRepositoryUrlSchema,
@@ -34,11 +34,13 @@ export const DAEMON_CAPABILITIES = [
34
34
  "project_skills_v1",
35
35
  RUNTIME_HEALTH_PROBE_CAPABILITY,
36
36
  "agent_ability_release_v1",
37
+ "agent_ability_workspace_v1",
37
38
  ];
38
39
  export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
39
40
  ? DAEMON_CAPABILITIES
40
41
  : DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1"
41
- && capability !== "agent_ability_release_v1");
42
+ && capability !== "agent_ability_release_v1"
43
+ && capability !== "agent_ability_workspace_v1");
42
44
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
43
45
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
44
46
  const RUNTIME_BINS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",