@nowcrew/daemon 0.6.16 → 0.6.18

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 (43) hide show
  1. package/dist/agent-ability/runtime-context.js +7 -1
  2. package/dist/atomic-private-write.js +54 -1
  3. package/dist/automatic-install-target.js +40 -11
  4. package/dist/console.js +9 -0
  5. package/dist/control-plane-url.js +2 -2
  6. package/dist/daemon-migration-controller.js +198 -0
  7. package/dist/daemon-migration-wiring.js +22 -0
  8. package/dist/daemon-update-eligibility.js +1 -1
  9. package/dist/directory-projection-identity.js +32 -0
  10. package/dist/directory-projection.js +922 -0
  11. package/dist/execution-protocol.js +78 -11
  12. package/dist/execution-runner.js +50 -2
  13. package/dist/i18n.js +1 -0
  14. package/dist/local-execution-prompt.js +57 -0
  15. package/dist/local-executor.js +99 -40
  16. package/dist/machine-info.js +45 -9
  17. package/dist/main.js +0 -0
  18. package/dist/normalize.js +5 -0
  19. package/dist/profile-layout.js +41 -0
  20. package/dist/project-skills/controller.js +74 -14
  21. package/dist/project-skills/execution-adapter.js +11 -0
  22. package/dist/project-skills/initialized-reconciler.js +20 -0
  23. package/dist/project-skills/projection-set-switch.js +419 -0
  24. package/dist/project-skills/projection-state-domain.js +153 -0
  25. package/dist/project-skills/projection-state-store.js +841 -0
  26. package/dist/project-skills/projection-state-transaction.js +318 -0
  27. package/dist/project-skills/projection-state.js +3 -0
  28. package/dist/project-skills/reconciler.js +299 -68
  29. package/dist/project-skills/runtime-warning.js +6 -0
  30. package/dist/project-skills/scanner.js +30 -1
  31. package/dist/project-skills/types.js +9 -0
  32. package/dist/project-workspaces/resolver.js +179 -0
  33. package/dist/project-workspaces/types.js +1 -0
  34. package/dist/prompt.js +40 -0
  35. package/dist/runtimes/claude.js +235 -4
  36. package/dist/runtimes/codex-app-server-runner.js +100 -25
  37. package/dist/runtimes/codex-contract.js +123 -0
  38. package/dist/runtimes/codex.js +2 -0
  39. package/dist/serve.js +31 -17
  40. package/dist/session.js +3 -0
  41. package/dist/supervised-runtime.js +12 -4
  42. package/dist/workspace.js +14 -5
  43. package/package.json +10 -9
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AbilityRepositoryUrlSchema, SafeAbilityPathSchema, WorkspaceAbilityTargetSchema, } from "./agent-ability/types.js";
3
- import { MAX_AGENT_PROJECT_SKILL_BINDINGS } from "./project-skills/types.js";
3
+ import { compareProjectSkillRefs, MAX_AGENT_PROJECT_SKILL_BINDINGS, MAX_PROJECT_ID_LENGTH, MAX_PROJECT_SKILL_NAME_LENGTH, } from "./project-skills/types.js";
4
4
  const ExecutionIdSchema = z.string().uuid();
5
5
  const ProtocolVersionSchema = z.literal(1);
6
6
  const TimestampSchema = z.string().datetime({ offset: true });
@@ -20,9 +20,61 @@ export const AgentHandleSchema = z.string().min(1).max(64).refine((handle) => ha
20
20
  && !handle.endsWith(" ")
21
21
  && Buffer.byteLength(handle, "utf8") <= 255, "agent handle must be a cross-platform safe filesystem component");
22
22
  const ProjectSkillRefSchema = z.object({
23
- projectId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u),
24
- skillName: z.string().min(1).max(128).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
23
+ projectId: z.string().min(1).max(MAX_PROJECT_ID_LENGTH).regex(/^[a-z0-9][a-z0-9._-]*$/u),
24
+ skillName: z.string().min(1).max(MAX_PROJECT_SKILL_NAME_LENGTH).regex(/^[a-z0-9][a-z0-9._:-]*$/u),
25
25
  }).strict();
26
+ const ProjectSkillBindingsSchema = z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS);
27
+ const ProjectSkillProjectionBindingsSchema = ProjectSkillBindingsSchema;
28
+ const ProjectSkillBindingGenerationSchema = z.number().int().min(0).max(MAX_PG_INTEGER);
29
+ function validateCanonicalProjectSkillBindings(bindings, ctx) {
30
+ const seen = new Set();
31
+ for (let index = 0; index < bindings.length; index += 1) {
32
+ const binding = bindings[index];
33
+ const key = `${binding.projectId}\0${binding.skillName}`;
34
+ if (seen.has(key)) {
35
+ ctx.addIssue({
36
+ code: z.ZodIssueCode.custom,
37
+ message: "duplicate_project_skill_binding",
38
+ path: ["projectSkills", index],
39
+ });
40
+ return;
41
+ }
42
+ seen.add(key);
43
+ if (index > 0 && compareProjectSkillRefs(bindings[index - 1], binding) > 0) {
44
+ ctx.addIssue({
45
+ code: z.ZodIssueCode.custom,
46
+ message: "project_skill_bindings_not_sorted",
47
+ path: ["projectSkills", index],
48
+ });
49
+ return;
50
+ }
51
+ }
52
+ }
53
+ export const ProjectSkillProjectionV2FieldsSchema = z.object({
54
+ projectSkills: ProjectSkillProjectionBindingsSchema,
55
+ projectSkillBindingGeneration: ProjectSkillBindingGenerationSchema,
56
+ }).strict().superRefine((snapshot, ctx) => {
57
+ validateCanonicalProjectSkillBindings(snapshot.projectSkills, ctx);
58
+ });
59
+ const ProjectIdSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/u);
60
+ export const ProjectContextSchema = z.object({
61
+ projectIds: z.array(ProjectIdSchema).max(32),
62
+ primaryProjectId: ProjectIdSchema.optional(),
63
+ }).strict().superRefine((value, ctx) => {
64
+ for (let index = 1; index < value.projectIds.length; index += 1) {
65
+ if (value.projectIds[index] === value.projectIds[index - 1]) {
66
+ ctx.addIssue({ code: "custom", message: "duplicate_project_id" });
67
+ break;
68
+ }
69
+ if (value.projectIds[index] < value.projectIds[index - 1]) {
70
+ ctx.addIssue({ code: "custom", message: "project_ids_not_sorted" });
71
+ break;
72
+ }
73
+ }
74
+ if (value.primaryProjectId && !value.projectIds.includes(value.primaryProjectId)) {
75
+ ctx.addIssue({ code: "custom", message: "primary_project_not_bound" });
76
+ }
77
+ });
26
78
  export const AbilityReleaseSnapshotSchema = z.object({
27
79
  bindingId: z.string().uuid(),
28
80
  releaseId: z.string().uuid(),
@@ -107,21 +159,36 @@ export const ExecutionAttachmentSchema = z.object({
107
159
  mime: z.string().min(1).max(200),
108
160
  sizeBytes: z.number().int().nonnegative().max(25 * 1024 * 1024),
109
161
  }).strict();
162
+ const ExecutionAgentSchema = z.object({
163
+ id: z.string().min(1),
164
+ handle: AgentHandleSchema,
165
+ memoryEnabled: z.boolean().optional(),
166
+ memoryAgentId: z.string().regex(/^agt-[A-Za-z0-9_-]+$/).max(100).optional(),
167
+ projectSkills: ProjectSkillBindingsSchema.optional(),
168
+ projectSkillBindingGeneration: ProjectSkillBindingGenerationSchema.optional(),
169
+ abilityRelease: AbilityReleaseSnapshotSchema.optional(),
170
+ }).strict().superRefine((agent, ctx) => {
171
+ if (agent.projectSkillBindingGeneration === undefined)
172
+ return;
173
+ if (agent.projectSkills === undefined) {
174
+ ctx.addIssue({
175
+ code: z.ZodIssueCode.custom,
176
+ message: "project_skill_generation_requires_bindings",
177
+ path: ["projectSkillBindingGeneration"],
178
+ });
179
+ return;
180
+ }
181
+ validateCanonicalProjectSkillBindings(agent.projectSkills, ctx);
182
+ });
110
183
  export const ExecutionStartSchema = z.object({
111
184
  type: z.literal("execution:start"),
112
185
  protocolVersion: ProtocolVersionSchema,
113
186
  executionId: ExecutionIdSchema,
114
- agent: z.object({
115
- id: z.string().min(1),
116
- handle: AgentHandleSchema,
117
- memoryEnabled: z.boolean().optional(),
118
- memoryAgentId: z.string().regex(/^agt-[A-Za-z0-9_-]+$/).max(100).optional(),
119
- projectSkills: z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS).optional(),
120
- abilityRelease: AbilityReleaseSnapshotSchema.optional(),
121
- }).strict(),
187
+ agent: ExecutionAgentSchema,
122
188
  workspace: z.object({
123
189
  taskKey: z.string().min(1).max(200),
124
190
  resumeKey: z.string().min(1).max(200).optional(),
191
+ projectContext: ProjectContextSchema.optional(),
125
192
  }).strict(),
126
193
  instructions: z.object({
127
194
  language: z.enum(["zh", "en"]),
@@ -14,6 +14,10 @@ import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-de
14
14
  import { RuntimeCancelledError } from "./runtime-cancellation.js";
15
15
  import { supervisorLaunch } from "./supervised-runtime.js";
16
16
  import { appendAgentMemoryContext } from "./agent-memory/policy.js";
17
+ import { projectSkillExecutionProjection, projectSkillProjectionErrorCode } from "./project-skills/execution-adapter.js";
18
+ import { createProjectRegistry } from "./project-skills/registry.js";
19
+ import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
20
+ import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
17
21
  export { supervisorLaunch } from "./supervised-runtime.js";
18
22
  const ACTIVITY_KIND = {
19
23
  init: "working",
@@ -41,6 +45,15 @@ function canonical(value) {
41
45
  export function hashExecutionSpec(spec) {
42
46
  return createHash("sha256").update(JSON.stringify(canonical(spec))).digest("hex");
43
47
  }
48
+ /** Hashes logical identities only; local project roots are deliberately outside the encoding. */
49
+ export function projectSessionContextFingerprint(runtime, projectContext) {
50
+ const encoded = JSON.stringify(canonical({
51
+ runtime,
52
+ primaryProjectId: projectContext.primaryProjectId ?? null,
53
+ projectIds: [...new Set(projectContext.projectIds)].sort(),
54
+ }));
55
+ return createHash("sha256").update(encoded, "utf8").digest("hex");
56
+ }
44
57
  async function reportBestEffort(report, frame, timeoutMs) {
45
58
  let timeout;
46
59
  try {
@@ -201,6 +214,15 @@ function rejection(executionId, reason, message, at) {
201
214
  at,
202
215
  });
203
216
  }
217
+ export function projectWorkspaceCapabilityRejection(spec, capabilities, at) {
218
+ const projectContext = spec.workspace.projectContext;
219
+ const nativeRuntime = spec.runtime.name === "codex" || spec.runtime.name === "claude";
220
+ return projectContext !== undefined
221
+ && projectContext.projectIds.length > 0
222
+ && (!nativeRuntime || !capabilities?.includes(PROJECT_WORKSPACES_CAPABILITY))
223
+ ? rejection(spec.executionId, "capability_missing", `${PROJECT_WORKSPACES_CAPABILITY} is unavailable`, at)
224
+ : null;
225
+ }
204
226
  function admission(spec, config, dependencies, at) {
205
227
  const backend = executionBackendCapability(dependencies.platform ?? process.platform);
206
228
  if (!backend.supported) {
@@ -278,12 +300,15 @@ async function cancellable(promise, cancellation) {
278
300
  }
279
301
  function failedCompletion(spec, error, startedAt, finishedAt) {
280
302
  const message = error instanceof Error ? error.message : String(error);
303
+ const errorCode = error instanceof ProjectContextUnavailableError
304
+ ? error.code
305
+ : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
281
306
  return ExecutionCompletedSchema.parse({
282
307
  type: "execution:completed",
283
308
  protocolVersion: 1,
284
309
  executionId: spec.executionId,
285
310
  outcome: "failed",
286
- errorCode: "local_execution_failed",
311
+ errorCode,
287
312
  errorMessage: message || "Unknown local execution failure",
288
313
  runtime: spec.runtime.name,
289
314
  ...(spec.runtime.model === undefined ? {} : { model: spec.runtime.model }),
@@ -308,6 +333,12 @@ export async function runExecution(config, input, dependencies) {
308
333
  return { kind: "rejected", frame };
309
334
  }
310
335
  const spec = parsed.data;
336
+ const capabilityRejection = projectWorkspaceCapabilityRejection(spec, dependencies.capabilities, initialAt);
337
+ if (capabilityRejection !== null) {
338
+ const frame = ExecutionRejectedSchema.parse(boundExecutionFrame(capabilityRejection, config.executionLimits.maxEventBytes));
339
+ await dependencies.report(frame);
340
+ return { kind: "rejected", frame };
341
+ }
311
342
  const bestEffortTimeoutMs = telemetryDrainTimeout(dependencies.telemetryDrainTimeoutMs);
312
343
  const specHash = hashExecutionSpec(spec);
313
344
  const replay = await dependencies.journal.get(spec.executionId);
@@ -415,6 +446,19 @@ export async function runExecution(config, input, dependencies) {
415
446
  if (dependencies.slot !== undefined) {
416
447
  await cancellable(dependencies.slot.ready, dependencies.cancellation);
417
448
  }
449
+ let projectContext;
450
+ let sessionContextFingerprint;
451
+ const logicalProjectContext = spec.workspace.projectContext;
452
+ if (logicalProjectContext !== undefined && logicalProjectContext.projectIds.length > 0) {
453
+ const projectSnapshot = {
454
+ projectIds: logicalProjectContext.projectIds,
455
+ ...(logicalProjectContext.primaryProjectId === undefined
456
+ ? {}
457
+ : { primaryProjectId: logicalProjectContext.primaryProjectId }),
458
+ };
459
+ projectContext = await cancellable(resolveProjectContext(projectSnapshot, dependencies.projectWorkspaceRegistry ?? createProjectRegistry(config.agentsRoot)), dependencies.cancellation);
460
+ sessionContextFingerprint = projectSessionContextFingerprint(spec.runtime.name, projectSnapshot);
461
+ }
418
462
  let recalledMemory = "";
419
463
  if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
420
464
  try {
@@ -589,9 +633,13 @@ export async function runExecution(config, input, dependencies) {
589
633
  keyMode: "opaque",
590
634
  taskKey: spec.workspace.taskKey,
591
635
  ...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
636
+ ...(sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint }),
637
+ ...(projectContext === undefined ? {} : { projectContext }),
592
638
  ...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
593
639
  ...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
594
- ...(spec.agent.projectSkills === undefined ? {} : { projectSkills: spec.agent.projectSkills }),
640
+ ...(spec.agent.projectSkills === undefined
641
+ ? {}
642
+ : { projectSkills: projectSkillExecutionProjection(spec.agent.projectSkills, spec.agent.projectSkillBindingGeneration) }),
595
643
  ...(spec.agent.abilityRelease === undefined ? {} : { abilityRelease: spec.agent.abilityRelease }),
596
644
  maxSystemPromptBytes: systemPromptBudget,
597
645
  systemPrompt: boundedSystemPrompt,
package/dist/i18n.js CHANGED
@@ -18,6 +18,7 @@ export function detectDaemonLang(env = process.env) {
18
18
  const zh = {
19
19
  "Claude session started": "Claude 会话启动",
20
20
  "Codex session started": "Codex 会话启动",
21
+ "Plan updated": "计划更新",
21
22
  "Files changed": "文件变更",
22
23
  "Run failed": "运行出错",
23
24
  "Run finished": "本轮结束",
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 本机执行提示词的合成与预算裁剪。
3
+ *
4
+ * server 提示词是权威部分,绝不裁剪;项目工作区提示词是绑定执行的强制部分,放不下就直接失败;
5
+ * 只有本机事实(CREW_HOME / work-log / MEMORY 注入)是可选增强,按整段丢弃或按字节截断。
6
+ */
7
+ import { buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
8
+ export function truncateUtf8(value, maxBytes) {
9
+ if (maxBytes <= 0)
10
+ return "";
11
+ let bytes = 0;
12
+ let end = 0;
13
+ for (const character of value) {
14
+ const size = Buffer.byteLength(character, "utf8");
15
+ if (bytes + size > maxBytes)
16
+ break;
17
+ bytes += size;
18
+ end += character.length;
19
+ }
20
+ return value.slice(0, end);
21
+ }
22
+ function takeCompletePromptSections(sections, maxBytes) {
23
+ return sections.reduce((state, section) => {
24
+ if (!state.full)
25
+ return state;
26
+ const sectionBytes = Buffer.byteLength(section, "utf8");
27
+ if (state.bytes + sectionBytes > maxBytes)
28
+ return { ...state, full: false };
29
+ return { value: `${state.value}${section}`, bytes: state.bytes + sectionBytes, full: true };
30
+ }, { value: "", bytes: 0, full: true }).value;
31
+ }
32
+ /** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
33
+ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
34
+ return ({ workspace, resuming, projectContext }) => {
35
+ const memory = capMemoryForInject(workspace.memory);
36
+ const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
37
+ const mandatoryWorkspacePrompt = buildRuntimeWorkspacePrompt(projectContext);
38
+ const localFactSections = [`\n\n## Local execution facts
39
+ - $CREW_HOME: ${workspace.dir}
40
+ - $CREW_TASK_LOG: ${workspace.workLogPath}`,
41
+ ...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
42
+ ...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
43
+ ];
44
+ const localFacts = localFactSections.join("");
45
+ if (mandatoryWorkspacePrompt !== "") {
46
+ const serverPromptBytes = Buffer.byteLength(serverPrompt, "utf8");
47
+ const mandatoryBytes = Buffer.byteLength(mandatoryWorkspacePrompt, "utf8");
48
+ const requiredBytes = serverPromptBytes + mandatoryBytes;
49
+ if (requiredBytes > maxBytes)
50
+ throw new Error("project_workspace_prompt_budget_exceeded");
51
+ const optionalFacts = takeCompletePromptSections(localFactSections, maxBytes - requiredBytes);
52
+ return `${serverPrompt}${optionalFacts}${mandatoryWorkspacePrompt}`;
53
+ }
54
+ const remaining = maxBytes - Buffer.byteLength(serverPrompt, "utf8");
55
+ return `${serverPrompt}${truncateUtf8(localFacts, remaining)}`;
56
+ };
57
+ }
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline";
2
2
  import { readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
5
- import { spawnClaude } from "./runtimes/claude.js";
5
+ import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, resolveOrderedUniqueClaudeDirectories, spawnClaude, } from "./runtimes/claude.js";
6
6
  import { spawnCodex } from "./runtimes/codex.js";
7
7
  import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
8
8
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
@@ -11,7 +11,8 @@ import { augmentedPath } from "./runtime-path.js";
11
11
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
12
12
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
13
13
  import { createConsoleFormatter } from "./console-formatter.js";
14
- import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
14
+ import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
15
+ export { withLocalExecutionFacts, } from "./local-execution-prompt.js";
15
16
  import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
16
17
  import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
17
18
  import { routeRuntimeAttachments, runtimeCapability, } from "./runtime-capabilities.js";
@@ -19,6 +20,7 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
19
20
  import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
20
21
  import { dslog } from "./slog.js";
21
22
  import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
23
+ import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
22
24
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
23
25
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
24
26
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
@@ -53,39 +55,10 @@ function logMemoryPruneDiagnosticsFailure(input, traceId, phase, error) {
53
55
  error_code: errorCode,
54
56
  });
55
57
  }
56
- function truncateUtf8(value, maxBytes) {
57
- if (maxBytes <= 0)
58
- return "";
59
- let bytes = 0;
60
- let end = 0;
61
- for (const character of value) {
62
- const size = Buffer.byteLength(character, "utf8");
63
- if (bytes + size > maxBytes)
64
- break;
65
- bytes += size;
66
- end += character.length;
67
- }
68
- return value.slice(0, end);
69
- }
70
- /** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
71
- export function withLocalExecutionFacts(serverPrompt, maxBytes) {
72
- return ({ workspace, resuming }) => {
73
- const memory = capMemoryForInject(workspace.memory);
74
- const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
75
- const localFacts = `\n\n## Local execution facts
76
- - $CREW_HOME: ${workspace.dir}
77
- - $CREW_TASK_LOG: ${workspace.workLogPath}${memory
78
- ? `\n\n## Injected MEMORY.md (bounded local context)\n${memory}`
79
- : ""}${workLog
80
- ? `\n\n## Injected work-log (bounded local context)\n${workLog}`
81
- : ""}`;
82
- const remaining = maxBytes - Buffer.byteLength(serverPrompt, "utf8");
83
- return `${serverPrompt}${truncateUtf8(localFacts, remaining)}`;
84
- };
85
- }
86
58
  const STDERR_TAIL_CAP = 2_000;
87
59
  const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
88
60
  const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
61
+ const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
89
62
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
90
63
  const RESERVED_ENV = new Set([
91
64
  "PATH",
@@ -95,6 +68,16 @@ const RESERVED_ENV = new Set([
95
68
  "GH_CONFIG_DIR",
96
69
  "CLOUDSDK_CONFIG",
97
70
  ]);
71
+ function withoutClaudeInstructionFlag(env) {
72
+ if (!(CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV in env))
73
+ return env;
74
+ const next = { ...env };
75
+ delete next[CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV];
76
+ return next;
77
+ }
78
+ const warnRuntimeLocally = (code) => {
79
+ process.stderr.write(`[runtime-warning] ${code}\n`);
80
+ };
98
81
  export function sanitizeEnvVars(raw) {
99
82
  if (!raw)
100
83
  return {};
@@ -155,14 +138,15 @@ async function launchLegacyRuntime(request) {
155
138
  ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
156
139
  };
157
140
  if (request.runtime === "claude") {
141
+ const additionalDirectories = request.claudeAdditionalDirectories
142
+ ?? (request.agentRoot === undefined
143
+ ? undefined
144
+ : [join(request.agentRoot, ".crew", "claude-skills"), request.agentRoot]);
158
145
  return wrapChild(spawnClaude({
159
146
  ...common,
160
147
  systemPromptPath: request.systemPromptPath,
161
148
  wakePrompt: request.wakePrompt,
162
- ...(request.agentRoot === undefined ? {} : {
163
- projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
164
- agentRootDirectory: request.agentRoot,
165
- }),
149
+ ...(additionalDirectories === undefined ? {} : { additionalDirectories }),
166
150
  ...(request.sessionId === undefined ? {} : {
167
151
  sessionId: request.sessionId,
168
152
  resume: request.resume,
@@ -221,6 +205,7 @@ export async function executeLocal(input, callbacks = {}, dependencies = {}) {
221
205
  input.handle,
222
206
  input.keyMode ?? "legacy",
223
207
  input.resumeKey ?? input.taskKey ?? "",
208
+ ...(input.sessionContextFingerprint === undefined ? [] : [input.sessionContextFingerprint]),
224
209
  ]);
225
210
  return withKeyedLease(leaseKey, () => executeLocalUnlocked(input, callbacks, dependencies), dependencies.cancellation);
226
211
  }
@@ -247,6 +232,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
247
232
  ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
248
233
  ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
249
234
  ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
235
+ ...(input.sessionContextFingerprint === undefined
236
+ ? {}
237
+ : { sessionContextFingerprint: input.sessionContextFingerprint }),
250
238
  ...(input.launch.description ? { description: input.launch.description } : {}),
251
239
  }), dependencies.cancellation);
252
240
  }
@@ -278,9 +266,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
278
266
  }
279
267
  const supportsNativeResume = runtime.name === "claude"
280
268
  || (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
281
- const currentPrior = input.session.enabled && supportsNativeResume
269
+ const storedCurrentPrior = input.session.enabled && supportsNativeResume
282
270
  ? await readSession(workspace.sessionDir)
283
271
  : null;
272
+ const currentPrior = storedCurrentPrior !== null
273
+ && storedCurrentPrior.sessionContextFingerprint === input.sessionContextFingerprint
274
+ ? storedCurrentPrior
275
+ : null;
284
276
  // Interactive V1 uses collision-resistant session directories, while the
285
277
  // older legacy path stored metadata under tasks/<safe thread key>. Import
286
278
  // that metadata once so an existing thread can cross the protocol boundary
@@ -289,6 +281,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
289
281
  && supportsNativeResume
290
282
  && input.keyMode === "opaque"
291
283
  && input.resumeKey !== undefined
284
+ && input.sessionContextFingerprint === undefined
292
285
  ? join(input.launch.agentsRoot, input.handle, "tasks", safeKey(input.resumeKey))
293
286
  : null;
294
287
  const legacyCwdMarker = legacyRunDir === null
@@ -337,6 +330,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
337
330
  resuming,
338
331
  rotatedForBudget,
339
332
  nearBudget,
333
+ ...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
340
334
  };
341
335
  const systemPrompt = resolvePrompt(input.systemPrompt, promptContext);
342
336
  if (input.attachments && input.attachments.length > 0) {
@@ -442,6 +436,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
442
436
  CREW_TOKEN: input.launch.token,
443
437
  CREW_CHANNEL: input.channelId,
444
438
  CREW_HOME: workspace.dir,
439
+ CREW_TASK_DIR: executionWorkspace.runDir,
445
440
  CREW_TASK_LOG: executionWorkspace.workLogPath,
446
441
  ...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
447
442
  XDG_CONFIG_HOME: join(workspace.homeDir, ".config"),
@@ -520,15 +515,61 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
520
515
  skill_count: abilityContext.skillCount,
521
516
  });
522
517
  }
518
+ const codexSkillRoots = runtime.name === "codex" && input.projectContext !== undefined
519
+ ? [
520
+ ...(input.projectSkills === undefined
521
+ ? []
522
+ : [join(workspace.dir, ".agents", "skills")]),
523
+ ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
524
+ ]
525
+ : [];
526
+ const claudeAdditionalDirectories = runtime.name === "claude"
527
+ ? input.projectContext === undefined
528
+ ? abilityContext?.skillRoot === undefined
529
+ ? []
530
+ : await resolveOrderedUniqueClaudeDirectories([
531
+ join(workspace.dir, ".crew", "claude-skills"),
532
+ workspace.dir,
533
+ abilityContext.skillRoot,
534
+ ])
535
+ : await resolveOrderedUniqueClaudeDirectories([
536
+ ...input.projectContext.secondary.map((project) => project.root),
537
+ ...(input.projectSkills === undefined
538
+ ? []
539
+ : [join(workspace.dir, ".crew", "claude-skills")]),
540
+ ...(abilityContext?.skillRoot === undefined ? [] : [abilityContext.skillRoot]),
541
+ ], input.projectContext.primary === undefined ? [] : [input.projectContext.primary.root])
542
+ : [];
543
+ let runtimeEnv = childEnv;
544
+ if (runtime.name === "claude"
545
+ && input.projectContext !== undefined
546
+ && claudeAdditionalDirectories.length > 0) {
547
+ const version = await (dependencies.probeClaudeVersion ?? probeClaudeVersion)(runtime.name, childEnv)
548
+ .catch(() => null);
549
+ if (isClaudeAdditionalDirectoryInstructionsSupported(version)) {
550
+ runtimeEnv = {
551
+ ...childEnv,
552
+ [CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV]: "1",
553
+ };
554
+ }
555
+ else {
556
+ runtimeEnv = withoutClaudeInstructionFlag(childEnv);
557
+ (dependencies.warnRuntime ?? warnRuntimeLocally)(CLAUDE_INSTRUCTION_WARNING);
558
+ callbacks.onConsole?.({
559
+ stream: "system",
560
+ text: `[runtime-warning] ${CLAUDE_INSTRUCTION_WARNING}`,
561
+ });
562
+ }
563
+ }
523
564
  const launchRequest = {
524
565
  runtime: runtime.name,
525
566
  bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
526
- cwd: executionWorkspace.runDir,
567
+ cwd: input.projectContext?.primary?.root ?? executionWorkspace.runDir,
527
568
  ...(input.projectSkills === undefined && input.abilityRelease === undefined ? {} : { agentRoot: workspace.dir }),
528
569
  systemPromptPath: workspace.systemPromptPath,
529
570
  systemPrompt: effectiveSystemPrompt,
530
571
  wakePrompt,
531
- env: childEnv,
572
+ env: runtimeEnv,
532
573
  effectivePermission: input.effectivePermission,
533
574
  ...(launchModel === undefined ? {} : { model: launchModel }),
534
575
  ...(launchReasoning === undefined ? {} : { reasoning: launchReasoning }),
@@ -537,6 +578,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
537
578
  ...(attachmentPlan.nativeImagePaths.length > 0
538
579
  ? { imagePaths: attachmentPlan.nativeImagePaths }
539
580
  : {}),
581
+ ...(codexSkillRoots.length === 0
582
+ ? {}
583
+ : { codexSkillRoots: Object.freeze(codexSkillRoots) }),
584
+ ...(claudeAdditionalDirectories.length === 0
585
+ ? {}
586
+ : { claudeAdditionalDirectories }),
587
+ ...(input.projectContext === undefined ? {} : {
588
+ workspaceRoots: Object.freeze(input.projectContext.secondary.map((project) => project.root)),
589
+ projectContext: input.projectContext,
590
+ }),
540
591
  };
541
592
  memoryPruneFailurePhase = "runtime_launch";
542
593
  return launchRuntime(launchRequest);
@@ -545,7 +596,12 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
545
596
  ? dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, launchPreparedRuntime)
546
597
  : launchPreparedRuntime();
547
598
  const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
548
- ? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility)
599
+ ? await (Array.isArray(input.projectSkills)
600
+ ? dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility)
601
+ : dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, launchWithAbility, (warning) => callbacks.onConsole?.({
602
+ stream: "system",
603
+ text: formatProjectSkillRuntimeWarning(warning),
604
+ })))
549
605
  : await launchWithAbility();
550
606
  localMemoryTelemetry?.markRuntimeStarted();
551
607
  memoryPruneFailurePhase = "runtime_execution";
@@ -721,6 +777,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
721
777
  turns: resuming ? (prior?.turns ?? 0) + 1 : 1,
722
778
  model: currentModel,
723
779
  providerFingerprint: providerFp,
780
+ ...(input.sessionContextFingerprint === undefined
781
+ ? {}
782
+ : { sessionContextFingerprint: input.sessionContextFingerprint }),
724
783
  lastExitOk: exitCode === 0,
725
784
  ...(contextTokens === undefined ? {} : { contextTokens }),
726
785
  });
@@ -19,9 +19,15 @@ import { probeHermesAcp } from "./runtimes/hermes.js";
19
19
  import { probeOpenCodeRun } from "./runtimes/opencode.js";
20
20
  import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
21
21
  import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
22
+ import { daemonHome } from "./computer-profile.js";
22
23
  export { executableRuntimes } from "./runtime-capabilities.js";
23
24
  const execFileP = promisify(execFile);
24
- export const DAEMON_CAPABILITIES = [
25
+ export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
26
+ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
27
+ resolverReady: true,
28
+ nativeRuntimeAdaptersReady: true,
29
+ });
30
+ export const DAEMON_CAPABILITIES = Object.freeze([
25
31
  "scheduled_job_v1",
26
32
  "reply_origin_v1",
27
33
  "origin_decision_v1",
@@ -33,17 +39,35 @@ export const DAEMON_CAPABILITIES = [
33
39
  "execution_agent_memory_policy_v1",
34
40
  "execution_agent_memory_mapping_v1",
35
41
  "project_skills_v1",
42
+ PROJECT_WORKSPACES_CAPABILITY,
36
43
  RUNTIME_HEALTH_PROBE_CAPABILITY,
37
44
  "agent_ability_release_v1",
38
45
  "agent_ability_workspace_v1",
39
46
  "agent_ability_runtime_assets_v1",
40
- ];
41
- export const daemonCapabilities = (runtimePlatform = process.platform) => runtimePlatform === "darwin" || runtimePlatform === "linux"
42
- ? DAEMON_CAPABILITIES
43
- : DAEMON_CAPABILITIES.filter((capability) => capability !== "project_skills_v1"
44
- && capability !== "agent_ability_release_v1"
45
- && capability !== "agent_ability_workspace_v1"
46
- && capability !== "agent_ability_runtime_assets_v1");
47
+ ]);
48
+ export const daemonCapabilities = (runtimePlatform = process.platform, readiness = NATIVE_PROJECT_WORKSPACE_READINESS) => {
49
+ // Windows stays fail-closed until the native workspace adapter gate is verified on a real host.
50
+ const nativeWorkspacePlatform = runtimePlatform === "darwin" || runtimePlatform === "linux";
51
+ const nativeWorkspaceReady = nativeWorkspacePlatform
52
+ && readiness.resolverReady
53
+ && readiness.nativeRuntimeAdaptersReady;
54
+ return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
55
+ && (nativeWorkspacePlatform
56
+ || (capability !== "project_skills_v1"
57
+ && capability !== "agent_ability_release_v1"
58
+ && capability !== "agent_ability_workspace_v1"
59
+ && capability !== "agent_ability_runtime_assets_v1")))));
60
+ };
61
+ export const daemonCapabilityBindings = (runtimePlatform = process.platform, injectedCapabilities) => {
62
+ const snapshot = injectedCapabilities === undefined
63
+ ? daemonCapabilities(runtimePlatform)
64
+ : Object.freeze([...injectedCapabilities]);
65
+ return Object.freeze({
66
+ controlPlaneUrl: snapshot,
67
+ machineHello: snapshot,
68
+ executionRunner: snapshot,
69
+ });
70
+ };
47
71
  export const EXECUTION_PROTOCOL = Object.freeze({ min: 1, max: 1 });
48
72
  /** 候选 runtime CLI:展示名 → 可执行文件名。 */
49
73
  const RUNTIME_BINS = [
@@ -152,7 +176,19 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
152
176
  daemonVersion: daemonVersion(),
153
177
  runtimes,
154
178
  executionRuntimes,
155
- capabilities: [...daemonCapabilities(runtimePlatform), ...additionalCapabilities],
179
+ capabilities: Object.freeze([
180
+ ...(dependencies.capabilities ?? daemonCapabilities(runtimePlatform)),
181
+ ...additionalCapabilities,
182
+ ]),
183
+ ...(dependencies.profileName === undefined ? {} : {
184
+ layoutVersion: 1,
185
+ profileName: dependencies.profileName,
186
+ daemonHome: dependencies.daemonHome ?? daemonHome(),
187
+ agentsRoot,
188
+ ...(dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)?.[1] === undefined
189
+ ? {}
190
+ : { localInstanceName: dependencies.profileName.match(/^nw-(?:in|dev)-([a-z0-9][a-z0-9_-]{0,39})$/u)[1] }),
191
+ }),
156
192
  ...(backend.supported ? {
157
193
  executionProtocol: EXECUTION_PROTOCOL,
158
194
  executionLimits: Object.freeze({
package/dist/main.js CHANGED
File without changes
package/dist/normalize.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * 这是 daemon 的关键职责:前端/server 看到的是"在读历史/领任务/发消息",而非裸 Bash。
5
5
  * 纯函数,无 IO,完整单测。
6
6
  */
7
+ import { boundedCodexPlan, codexPlanText } from "./runtimes/codex-contract.js";
7
8
  /**
8
9
  * Strip /bin/zsh -lc, /bin/sh -c, or /bin/bash -lc shell wrappers (single OR double quotes),
9
10
  * including multi-line heredoc forms. [\s\S]+ matches across newlines. Backreference \1 ensures
@@ -63,6 +64,10 @@ function parseKimiBashCommand(args) {
63
64
  /** 把一个 stream-json 事件归一化为 0..N 个活动。 */
64
65
  export function normalizeEvent(event) {
65
66
  const e = (event ?? {});
67
+ if (e.type === "turn.plan.updated") {
68
+ const plan = boundedCodexPlan(e.plan);
69
+ return plan === null ? [] : [{ kind: "tool", label: "计划更新", detail: codexPlanText(plan) }];
70
+ }
66
71
  if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
67
72
  || e.type === "deepseek-harness.acp.text_delta"
68
73
  || e.type === "opencode.text_delta") && e.text?.trim()) {