@nowcrew/daemon 0.6.67 → 0.6.69

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.
@@ -470,6 +470,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
470
470
  const projectionDigest = abilitySha256(JSON.stringify({
471
471
  artifactDigest: ability.artifactDigest,
472
472
  workspace: ability.workspace,
473
+ git: verified.manifest.spec.git ?? null,
473
474
  }));
474
475
  let previousCatalog = null;
475
476
  if (await exists(trainingRoot)) {
@@ -531,6 +532,12 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
531
532
  : join(sourceRoot, asset.source);
532
533
  await cp(source, target, { recursive: true, preserveTimestamps: true });
533
534
  }
535
+ const gitManifestPath = verified.manifest.spec.git?.projectDependencies.path;
536
+ if (gitManifestPath !== undefined) {
537
+ const target = join(staging, "git", layerRelativePath(gitManifestPath, "git"));
538
+ await mkdir(dirname(target), { recursive: true });
539
+ await cp(join(sourceRoot, gitManifestPath), target, { preserveTimestamps: true });
540
+ }
534
541
  const evalPaths = [
535
542
  ...(verified.manifest.spec.evals.policy === undefined ? [] : [verified.manifest.spec.evals.policy]),
536
543
  ...verified.manifest.spec.evals.cases,
@@ -553,6 +560,9 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
553
560
  ...asset,
554
561
  projectedPath: `training/workspace/${abilityWorkspaceProjectionPath(asset.target)}`,
555
562
  })),
563
+ git: gitManifestPath === undefined ? null : {
564
+ projectDependenciesPath: `training/git/${layerRelativePath(gitManifestPath, "git")}`,
565
+ },
556
566
  evals: evalPaths.map((path) => `training/evals/${layerRelativePath(path, "evals")}`),
557
567
  }, null, 2), { encoding: "utf8", mode: 0o600 });
558
568
  await readonlyTree(staging, { rootWritable: true });
@@ -7,6 +7,7 @@ import { parse } from "yaml";
7
7
  import { z } from "zod";
8
8
  import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
9
9
  import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
10
+ import { normalizeRepositoryIdentity } from "../repository-identity.js";
10
11
  const runFile = promisify(execFile);
11
12
  const MAX_FILES = 5_000;
12
13
  const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
@@ -78,20 +79,30 @@ const git = async (args, cwd) => {
78
79
  return result.stdout.trim();
79
80
  };
80
81
  export const projectDependencyIds = (files, manifest) => {
81
- const assets = manifest.spec.workspace.assets.filter((asset) => asset.source === PROJECT_MANIFEST_SOURCE_PATH);
82
- if (assets.length === 0)
82
+ const path = manifest.spec.git?.projectDependencies.path;
83
+ if (path === undefined)
83
84
  return Object.freeze([]);
84
- if (assets.length > 1)
85
- throw new Error("ability_project_dependency_manifest_duplicate");
86
- const file = files.find((candidate) => candidate.path === PROJECT_MANIFEST_SOURCE_PATH);
85
+ if (path !== PROJECT_MANIFEST_SOURCE_PATH)
86
+ throw new Error("ability_project_dependency_manifest_path_invalid");
87
+ const file = files.find((candidate) => candidate.path === path);
87
88
  if (file === undefined)
88
89
  throw new Error("ability_project_dependency_manifest_missing");
90
+ let projects;
89
91
  try {
90
- return Object.freeze(ProjectDependencyManifestSchema.parse(parse(file.content.toString("utf8"))).projects.map((project) => project.id));
92
+ projects = ProjectDependencyManifestSchema.parse(parse(file.content.toString("utf8"))).projects;
91
93
  }
92
94
  catch {
93
95
  throw new Error("ability_project_dependency_manifest_invalid");
94
96
  }
97
+ return Object.freeze(projects.map((project) => {
98
+ try {
99
+ validateAbilityGitHost(project.repo);
100
+ }
101
+ catch {
102
+ throw new Error("ability_project_dependency_host_not_allowed");
103
+ }
104
+ return normalizeRepositoryIdentity(project.repo).repositoryKey;
105
+ }));
95
106
  };
96
107
  export const collectAbilityFiles = async (root) => {
97
108
  const files = [];
@@ -285,6 +296,8 @@ const scan = (files, manifest) => {
285
296
  required.add("memory-seed");
286
297
  if (manifest.spec.workspace.assets.length > 0)
287
298
  required.add("workspace");
299
+ if (manifest.spec.git !== undefined)
300
+ required.add("git");
288
301
  if (manifest.spec.evals.policy !== undefined)
289
302
  required.add("eval-policy");
290
303
  if (manifest.spec.evals.cases.length > 0)
@@ -297,6 +310,7 @@ const lockedManifestPaths = (manifest) => [
297
310
  ...manifest.spec.memory.policy.map((entry) => entry.path),
298
311
  ...manifest.spec.memory.seeds.map((entry) => entry.path),
299
312
  ...manifest.spec.workspace.assets.map((entry) => entry.source),
313
+ ...(manifest.spec.git === undefined ? [] : [manifest.spec.git.projectDependencies.path]),
300
314
  ...(manifest.spec.evals.policy === undefined ? [] : [manifest.spec.evals.policy]),
301
315
  ...manifest.spec.evals.cases,
302
316
  ];
@@ -19,7 +19,7 @@ export const AbilityRepositoryUrlSchema = z.string().trim().min(1).max(2_048).su
19
19
  try {
20
20
  const url = new URL(value);
21
21
  allowedUrl = (url.protocol === "https:" || url.protocol === "ssh:")
22
- && !url.username && !url.password && Boolean(url.hostname);
22
+ && !url.username && !url.password && !url.search && !url.hash && Boolean(url.hostname);
23
23
  }
24
24
  catch { /* handled below */ }
25
25
  if (!scpLike && !allowedUrl)
@@ -53,6 +53,10 @@ const LocalSkillSchema = z.object({ type: z.literal("local"), path: SafeAbilityP
53
53
  const GitSkillSchema = z.object({
54
54
  type: z.literal("git"), url: AbilityRepositoryUrlSchema, ref: z.string().min(1).max(256), path: SafeAbilityPathSchema,
55
55
  }).strict();
56
+ const GitProjectDependencyPathSchema = z.literal("git/project-dependencies.yaml");
57
+ const GitLayerSchema = z.object({
58
+ projectDependencies: z.object({ path: GitProjectDependencyPathSchema }).strict(),
59
+ }).strict();
56
60
  export const DaemonAbilityManifestSchema = z.object({
57
61
  apiVersion: z.literal("nowcrew.ai/v1alpha1"), kind: z.literal("AgentTraining"),
58
62
  metadata: z.object({ name: z.string().min(1).max(128), version: z.string().min(1).max(64), description: z.string().max(1_000).optional() }).strict(),
@@ -71,6 +75,7 @@ export const DaemonAbilityManifestSchema = z.object({
71
75
  workspace: z.object({ assets: z.array(z.object({
72
76
  source: SafeAbilityPathSchema, target: WorkspaceAbilityTargetSchema, mode: ModeSchema,
73
77
  }).strict()).max(1_000).default([]) }).strict().default({ assets: [] }),
78
+ git: GitLayerSchema.optional(),
74
79
  evals: z.object({
75
80
  policy: SafeAbilityPathSchema.optional(),
76
81
  cases: z.array(SafeAbilityPathSchema).max(1_000).default([]),
@@ -95,16 +100,6 @@ export const DaemonAbilityManifestSchema = z.object({
95
100
  unique(manifest.spec.skills.map((skill) => skill.name.replace(/[^A-Za-z0-9._-]/gu, "_")), ["spec", "skills"], "Skill projection");
96
101
  unique(manifest.spec.skills.map((skill) => skill.mount), ["spec", "skills"], "Skill mount");
97
102
  unique(manifest.spec.workspace.assets.map((asset) => asset.target), ["spec", "workspace", "assets"], "Workspace target");
98
- for (const asset of manifest.spec.workspace.assets) {
99
- if (asset.source === "workspace/project-dependencies.yaml"
100
- && asset.target !== "training/workspace/project-dependencies.yaml") {
101
- ctx.addIssue({
102
- code: z.ZodIssueCode.custom,
103
- path: ["spec", "workspace", "assets"],
104
- message: "project dependency manifest target must be training/workspace/project-dependencies.yaml",
105
- });
106
- }
107
- }
108
103
  unique(manifest.spec.evals.cases, ["spec", "evals", "cases"], "Eval case");
109
104
  });
110
105
  export const DaemonAbilityLockSchema = z.object({
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Claude 后台任务(Bash run_in_background + TaskOutput 轮询)的结果解析。纯函数,零 IO。
3
+ *
4
+ * TaskOutput 的 tool_result 是 XML 式文本,实测形状见
5
+ * docs/superpowers/evidence/2026-08-22-claude-background-task-stream-json.json。
6
+ * 注意轮询工具是 TaskOutput 而非 BashOutput——claude 2.1.238 实测确认。
7
+ */
8
+ /** 输出正文的字符上限,与 console 其它工具返回保持同量级。 */
9
+ const OUTPUT_CAP = 4000;
10
+ const tag = (text, name) => {
11
+ const match = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "u").exec(text);
12
+ return match ? match[1].trim() : undefined;
13
+ };
14
+ /**
15
+ * <output> 是协议里的最后一段,正文本身(命令的 stdout)可能包含字面量的 "</output>"
16
+ * (例如打印了一份 XML/日志)。非贪婪匹配会在第一个 "</output>" 处截断,把后面的真实内容
17
+ * 静默丢掉,产出一个"看起来对但其实错"的结果。这里改为贪婪匹配,吞到文本中*最后一个*
18
+ * "</output>",与协议里 output 恒为末段的事实对齐。
19
+ */
20
+ const extractOutputTag = (text) => {
21
+ const match = /<output>([\s\S]*)<\/output>/u.exec(text);
22
+ return match ? match[1].trim() : undefined;
23
+ };
24
+ export function parseTaskOutput(text) {
25
+ const jobId = tag(text, "task_id");
26
+ if (!jobId)
27
+ return null;
28
+ const status = tag(text, "status");
29
+ if (!status)
30
+ return null;
31
+ const rawExit = tag(text, "exit_code");
32
+ const exitCode = rawExit !== undefined && /^-?\d+$/u.test(rawExit) ? Number(rawExit) : undefined;
33
+ // status 只要不是 running,就按已结束处理(completed/failed/killed 等都归入 exited)。
34
+ const state = status === "running" ? "running" : "exited";
35
+ const rawOutput = extractOutputTag(text);
36
+ return {
37
+ jobId,
38
+ state,
39
+ ...(exitCode !== undefined ? { exitCode } : {}),
40
+ // lineCount 必须基于截断前的完整正文——否则前端拿到的"总行数"其实是预览的行数,
41
+ // 无法用来判断是否还有更多内容可展开(参照 console.ts 里 buildScriptResult 的做法)。
42
+ ...(rawOutput
43
+ ? {
44
+ output: rawOutput.slice(0, OUTPUT_CAP),
45
+ lineCount: rawOutput.split("\n").length,
46
+ ...(rawOutput.length > OUTPUT_CAP ? { truncated: true } : {}),
47
+ }
48
+ : {}),
49
+ };
50
+ }
@@ -3,11 +3,13 @@
3
3
  * 用 tool_use id 关联 Read 结果,避免将整文件作为普通 tool_result 倾泻到终端。
4
4
  */
5
5
  import { buildFilePreview } from "./console-payload.js";
6
+ import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
6
7
  import { buildScriptResult, toConsoleLines, TOOL_RESULT_CAP } from "./console.js";
7
8
  import { parseUnifiedDiff } from "./unified-diff.js";
8
9
  import { buildJsonResult } from "./json-result.js";
9
10
  import { buildCollapsedResult } from "./console-collapse.js";
10
11
  import { buildSkillPreview } from "./skill-preview.js";
12
+ import { parseTaskOutput } from "./background-job.js";
11
13
  const extract = (content) => {
12
14
  if (typeof content === "string")
13
15
  return content;
@@ -22,16 +24,71 @@ const clip = (text) => text.length > TOOL_RESULT_CAP
22
24
  const SCRIPT_TOOLS = new Set(["Bash", "PowerShell", "Shell"]);
23
25
  export function createConsoleFormatter() {
24
26
  const pending = new Map();
27
+ // listId → 上一条 todos 快照的指纹;仅用于丢弃「连续两次完全相同」的重复事件(无损去重)。
28
+ // 只比对相邻快照,不做跨快照合并——进度推进的中间态必须逐条下发,否则前端进度条会停在旧值。
29
+ const lastTodoFingerprint = new Map();
30
+ // jobId → 已下发次数(去重后);后台任务在滚动流里折叠成一个单元,次数供前端展示。
31
+ // 与去重表联动:被丢弃的重复轮询不会走到这里递增,所以计的是"不同状态数"而非"原始轮询数"。
32
+ const jobPollCount = new Map();
33
+ // jobId → 上一条轮询结果的指纹,用于丢弃「连续两次完全相同」的轮询(无损去重)。
34
+ const lastJobFingerprint = new Map();
35
+ /** 丢弃与该 listId 上一条完全相同的 todos 行;无 listId 的不参与去重。 */
36
+ const dedupeTodos = (chunks) => chunks.filter((chunk) => {
37
+ const payload = chunk.payload;
38
+ if (!payload || payload.kind !== "todos" || !payload.listId)
39
+ return true;
40
+ const fingerprint = JSON.stringify(payload.todos);
41
+ if (lastTodoFingerprint.get(payload.listId) === fingerprint)
42
+ return false;
43
+ lastTodoFingerprint.set(payload.listId, fingerprint);
44
+ return true;
45
+ });
46
+ // claude redacted-thinking(redact-thinking-2026-02-12):流式阶段 thinking_delta.thinking 为空串,
47
+ // 只携带 estimated_tokens 估算(数值会被修正,取最新值);完整 assistant 消息里 thinking 也为空。
48
+ // 这里累积估算值,在 assistant 消息落地时把空 thinking 块补成占位文本;
49
+ // 若未来 CLI 恢复明文思考,累积到的正文优先生效,占位逻辑自动失效。
50
+ let pendingThinking = null;
51
+ const consumePendingThinking = () => {
52
+ if (!pendingThinking)
53
+ return null;
54
+ const { text, tokens } = pendingThinking;
55
+ pendingThinking = null;
56
+ if (text.trim())
57
+ return text.trim();
58
+ const lang = detectDaemonLang();
59
+ return tokens > 0
60
+ ? formatDaemonText(lang, "Thinking (~{{tokens}} tokens, content not streamed by Claude Code)", { tokens })
61
+ : translateDaemon(lang, "Thinking (content not streamed by Claude Code)");
62
+ };
25
63
  return {
26
64
  format(event) {
27
65
  const rec = event && typeof event === "object" ? event : {};
28
66
  const message = rec.message && typeof rec.message === "object" ? rec.message : undefined;
67
+ if (rec.type === "stream_event" && rec.event && typeof rec.event === "object") {
68
+ const streamEvent = rec.event;
69
+ if (streamEvent.type === "content_block_delta" && streamEvent.delta?.type === "thinking_delta") {
70
+ const prev = pendingThinking ?? { text: "", tokens: 0 };
71
+ pendingThinking = {
72
+ text: prev.text + (typeof streamEvent.delta.thinking === "string" ? streamEvent.delta.thinking : ""),
73
+ tokens: typeof streamEvent.delta.estimated_tokens === "number" ? streamEvent.delta.estimated_tokens : prev.tokens,
74
+ };
75
+ }
76
+ return [];
77
+ }
29
78
  if (rec.type === "assistant" && Array.isArray(message?.content)) {
30
79
  for (const block of message.content) {
31
80
  if (block.type === "tool_use" && block.id && block.name) {
32
81
  pending.set(block.id, { name: block.name, ...(block.input ? { input: block.input } : {}) });
33
82
  }
34
83
  }
84
+ const hasEmptyThinking = message.content.some((block) => block.type === "thinking" && !block.thinking?.trim());
85
+ if (hasEmptyThinking) {
86
+ const filler = consumePendingThinking();
87
+ if (filler) {
88
+ const content = message.content.map((block) => block.type === "thinking" && !block.thinking?.trim() ? { ...block, thinking: filler } : block);
89
+ return dedupeTodos(toConsoleLines({ ...rec, message: { ...message, content } }));
90
+ }
91
+ }
35
92
  }
36
93
  if (rec.type === "user" && Array.isArray(message?.content)) {
37
94
  const out = [];
@@ -44,6 +101,24 @@ export function createConsoleFormatter() {
44
101
  const tool = block.tool_use_id ? pending.get(block.tool_use_id) : undefined;
45
102
  if (block.tool_use_id)
46
103
  pending.delete(block.tool_use_id);
104
+ if (!block.is_error && tool?.name === "TaskOutput") {
105
+ const parsed = parseTaskOutput(text);
106
+ if (parsed) {
107
+ const fingerprint = JSON.stringify(parsed);
108
+ if (lastJobFingerprint.get(parsed.jobId) === fingerprint)
109
+ continue;
110
+ lastJobFingerprint.set(parsed.jobId, fingerprint);
111
+ const pollCount = (jobPollCount.get(parsed.jobId) ?? 0) + 1;
112
+ jobPollCount.set(parsed.jobId, pollCount);
113
+ out.push({
114
+ stream: "tool_result",
115
+ text: `⏺ Background job ${parsed.jobId} · ${parsed.state}`,
116
+ payload: { ...parsed, kind: "background_job", pollCount },
117
+ });
118
+ continue;
119
+ }
120
+ // 解析不出来就走下面的通用分支,降级成纯文本行,不猜。
121
+ }
47
122
  if (!block.is_error && tool?.name === "Read" && typeof tool.input?.file_path === "string") {
48
123
  const start = typeof tool.input.offset === "number" && Number.isSafeInteger(tool.input.offset)
49
124
  ? Math.max(1, tool.input.offset) : 1;
@@ -71,7 +146,7 @@ export function createConsoleFormatter() {
71
146
  }
72
147
  return out;
73
148
  }
74
- return toConsoleLines(event);
149
+ return dedupeTodos(toConsoleLines(event));
75
150
  },
76
151
  };
77
152
  }
package/dist/console.js CHANGED
@@ -17,7 +17,7 @@ import { buildFilePreview, buildSnippetDiff } from "./console-payload.js";
17
17
  import { parseUnifiedDiff } from "./unified-diff.js";
18
18
  import { buildJsonResult } from "./json-result.js";
19
19
  import { buildCollapsedResult } from "./console-collapse.js";
20
- import { boundedCodexPlan, codexPlanText } from "./runtimes/codex-contract.js";
20
+ import { boundedCodexPlan } from "./runtimes/codex-contract.js";
21
21
  /** 单条工具返回正文上限(超出截断并标注),避免单条把终端/DB 撑爆。 */
22
22
  export const TOOL_RESULT_CAP = 4000;
23
23
  /** 工具输入摘要上限(标题行那一段)。 */
@@ -69,6 +69,12 @@ function summarizeToolInput(name, input) {
69
69
  .join(", ");
70
70
  return `⏺ ${name}(${clip(head.replace(/\s+/g, " "), TOOL_INPUT_CAP)})`;
71
71
  }
72
+ /** codex 计划步骤状态(pending/inProgress/completed) → ConsoleTodo 状态(pending/in_progress/completed)。 */
73
+ const CODEX_PLAN_STATUS_MAP = {
74
+ pending: "pending",
75
+ inProgress: "in_progress",
76
+ completed: "completed",
77
+ };
72
78
  const TODO_STATUSES = new Set(["pending", "in_progress", "completed"]);
73
79
  /** claude TodoWrite 的 todos 数组归一为 ConsoleTodo[](脏数据条目丢弃)。 */
74
80
  function normalizeTodos(raw) {
@@ -236,7 +242,13 @@ function codexItemChunks(item, td) {
236
242
  if (!todos.length)
237
243
  return [];
238
244
  const done = todos.filter((t) => t.status === "completed").length;
239
- return [{ stream: "tool", text: `⏺ Todos(${done}/${todos.length})`, payload: { kind: "todos", todos } }];
245
+ // codex 对同一份 plan 的多次更新复用同一个 item.id,透传给下游做合并键;无 id 时不伪造。
246
+ const listId = typeof item.id === "string" && item.id ? item.id : undefined;
247
+ return [{
248
+ stream: "tool",
249
+ text: `⏺ Todos(${done}/${todos.length})`,
250
+ payload: { kind: "todos", todos, ...(listId ? { listId } : {}) },
251
+ }];
240
252
  }
241
253
  if (item.type === "web_search" && item.query?.trim()) {
242
254
  return [{ stream: "tool", text: `⏺ WebSearch(${clip(item.query.trim(), TOOL_INPUT_CAP)})` }];
@@ -276,10 +288,17 @@ export function toConsoleLines(event) {
276
288
  }
277
289
  if (e.type === "turn.plan.updated") {
278
290
  const plan = boundedCodexPlan(e.plan);
279
- return plan === null ? [] : [{
291
+ if (plan === null)
292
+ return [];
293
+ const todos = plan.map(({ step, status }) => ({
294
+ text: step,
295
+ status: CODEX_PLAN_STATUS_MAP[status],
296
+ }));
297
+ const listId = typeof e.turn_id === "string" && e.turn_id ? e.turn_id : "codex-plan";
298
+ return [{
280
299
  stream: "tool",
281
300
  text: `⏺ ${td("Plan updated")} (${plan.length})`,
282
- payload: { kind: "plan", plan: codexPlanText(plan) },
301
+ payload: { kind: "todos", todos, listId },
283
302
  }];
284
303
  }
285
304
  if (e.type === "item.completed" && e.item) {
@@ -19,6 +19,7 @@ import { ProjectSkillRuntimeOwnershipUnverifiedError, } from "./project-skills/r
19
19
  import { projectSkillExecutionProjection, projectSkillProjectionErrorCode } from "./project-skills/execution-adapter.js";
20
20
  import { redactProjectSkillRuntimeRootError, } from "./project-skills/runtime-launch.js";
21
21
  import { createProjectRegistry } from "./project-skills/registry.js";
22
+ import { ProjectDependencyError, } from "./project-dependencies.js";
22
23
  import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
23
24
  import { PROJECT_WORKSPACES_CAPABILITY } from "./machine-info.js";
24
25
  import { PROJECT_SKILL_PROJECTION_V2_CAPABILITY } from "./project-skills/types.js";
@@ -315,7 +316,9 @@ function failedCompletion(spec, error, startedAt, finishedAt) {
315
316
  ? "queue_timeout"
316
317
  : error instanceof ProjectContextUnavailableError
317
318
  ? error.code
318
- : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
319
+ : error instanceof ProjectDependencyError
320
+ ? error.code
321
+ : projectSkillProjectionErrorCode(error) ?? "local_execution_failed";
319
322
  return ExecutionCompletedSchema.parse({
320
323
  type: "execution:completed",
321
324
  protocolVersion: 1,
@@ -503,16 +506,64 @@ export async function runExecution(config, input, dependencies) {
503
506
  await cancellable(dependencies.slot.ready, dependencies.cancellation);
504
507
  }
505
508
  let projectContext;
509
+ let trainingProjectContext;
506
510
  let sessionContextFingerprint;
507
511
  const logicalProjectContext = spec.workspace.projectContext;
508
- if (logicalProjectContext !== undefined && logicalProjectContext.projectIds.length > 0) {
512
+ dslog("project_dependencies.preflight", "项目依赖准备前置状态", {
513
+ execution_id: spec.executionId,
514
+ agent_handle: spec.agent.handle,
515
+ has_ability_release: spec.agent.abilityRelease !== undefined,
516
+ ability_release_id: spec.agent.abilityRelease?.releaseId,
517
+ ability_root_commit: spec.agent.abilityRelease?.rootCommit,
518
+ has_project_context: logicalProjectContext !== undefined,
519
+ project_ids: JSON.stringify(logicalProjectContext?.projectIds ?? []),
520
+ project_context_primary: logicalProjectContext?.primaryProjectId,
521
+ dependency_prepare_expected: spec.agent.abilityRelease !== undefined,
522
+ });
523
+ if (spec.agent.abilityRelease !== undefined && dependencies.projectDependencies !== undefined) {
524
+ if (dependencies.abilityRelease !== undefined) {
525
+ await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
526
+ }
527
+ const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
528
+ trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
529
+ reposRoot: join(config.agentsRoot, "repos"),
530
+ lockRoot: join(config.agentsRoot, ".crew", "project-dependencies"),
531
+ }), dependencies.cancellation);
532
+ trainingProjectContext = Object.freeze({
533
+ projects: Object.freeze(preparedProjectDependencies.map((project) => Object.freeze({
534
+ repositoryKey: project.repositoryKey,
535
+ displayName: project.displayName,
536
+ repo: project.repo,
537
+ root: project.localRoot,
538
+ branch: project.branch,
539
+ resolvedCommit: project.resolvedCommit,
540
+ }))),
541
+ });
542
+ for (const project of preparedProjectDependencies) {
543
+ dslog("project_dependencies.prepared", "Training project dependency prepared", {
544
+ execution_id: spec.executionId,
545
+ agent_handle: spec.agent.handle,
546
+ repository_key: project.repositoryKey,
547
+ project_branch: project.branch,
548
+ project_branch_source: project.branchSource,
549
+ project_commit: project.resolvedCommit,
550
+ project_checkout_mode: project.checkoutMode,
551
+ });
552
+ }
553
+ }
554
+ else if (spec.agent.abilityRelease === undefined
555
+ && logicalProjectContext !== undefined
556
+ && logicalProjectContext.projectIds.length > 0) {
509
557
  const projectSnapshot = {
510
558
  projectIds: logicalProjectContext.projectIds,
511
559
  ...(logicalProjectContext.primaryProjectId === undefined
512
560
  ? {}
513
561
  : { primaryProjectId: logicalProjectContext.primaryProjectId }),
514
562
  };
515
- projectContext = await cancellable(resolveProjectContext(projectSnapshot, dependencies.projectWorkspaceRegistry ?? createProjectRegistry(config.agentsRoot)), dependencies.cancellation);
563
+ const projectWorkspaceRegistry = dependencies.projectWorkspaceRegistry
564
+ ?? dependencies.projectRegistry
565
+ ?? createProjectRegistry(config.agentsRoot);
566
+ projectContext = await cancellable(resolveProjectContext(projectSnapshot, projectWorkspaceRegistry), dependencies.cancellation);
516
567
  sessionContextFingerprint = projectSessionContextFingerprint(spec.runtime.name, projectSnapshot);
517
568
  }
518
569
  let recalledMemory = "";
@@ -788,6 +839,7 @@ export async function runExecution(config, input, dependencies) {
788
839
  ...(spec.workspace.resumeKey === undefined ? {} : { resumeKey: spec.workspace.resumeKey }),
789
840
  ...(sessionContextFingerprint === undefined ? {} : { sessionContextFingerprint }),
790
841
  ...(projectContext === undefined ? {} : { projectContext }),
842
+ ...(trainingProjectContext === undefined ? {} : { trainingProjectContext }),
791
843
  ...(spec.context.wakeMessageId === undefined ? {} : { wakeMessageId: spec.context.wakeMessageId }),
792
844
  ...(spec.context.attachments === undefined ? {} : { attachments: spec.context.attachments }),
793
845
  ...(spec.agent.projectSkills === undefined
package/dist/i18n.js CHANGED
@@ -19,6 +19,8 @@ const zh = {
19
19
  "Claude session started": "Claude 会话启动",
20
20
  "Codex session started": "Codex 会话启动",
21
21
  "Plan updated": "计划更新",
22
+ "Thinking (~{{tokens}} tokens, content not streamed by Claude Code)": "思考(约 {{tokens}} tokens,Claude Code 未输出思考正文)",
23
+ "Thinking (content not streamed by Claude Code)": "思考(Claude Code 未输出思考正文)",
22
24
  "Files changed": "文件变更",
23
25
  "Run failed": "运行出错",
24
26
  "Run finished": "本轮结束",
@@ -5,6 +5,21 @@
5
5
  * 只有本机事实(CREW_HOME / work-log / MEMORY 注入)是可选增强,按整段丢弃或按字节截断。
6
6
  */
7
7
  import { buildCollaborationToolsPrompt, buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
8
+ const trainingProjectJson = (project) => JSON.stringify({
9
+ repositoryKey: project.repositoryKey,
10
+ displayName: project.displayName,
11
+ repo: project.repo,
12
+ branch: project.branch,
13
+ resolvedCommit: project.resolvedCommit,
14
+ root: project.root,
15
+ });
16
+ export function buildTrainingGitProjectPrompt(context) {
17
+ if (context === undefined || context.projects.length === 0)
18
+ return "";
19
+ return "\n\n## Training Git projects\n" + context.projects
20
+ .map((project) => "- " + trainingProjectJson(project) + "\n Run project commands with: cd <root> && <command>")
21
+ .join("\n");
22
+ }
8
23
  export function truncateUtf8(value, maxBytes) {
9
24
  if (maxBytes <= 0)
10
25
  return "";
@@ -31,11 +46,11 @@ function takeCompletePromptSections(sections, maxBytes) {
31
46
  }
32
47
  /** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
33
48
  export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
34
- return ({ workspace, resuming, projectContext }) => {
49
+ return ({ workspace, resuming, projectContext, trainingProjectContext }) => {
35
50
  const memory = capMemoryForInject(workspace.memory);
36
51
  const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
37
52
  const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
38
- const mandatoryWorkspacePrompt = buildRuntimeWorkspacePrompt(projectContext);
53
+ const mandatoryWorkspacePrompt = `${buildRuntimeWorkspacePrompt(projectContext)}${buildTrainingGitProjectPrompt(trainingProjectContext)}`;
39
54
  const localFactSections = [`\n\n## Local execution facts
40
55
  - $CREW_HOME: ${workspace.dir}
41
56
  - $CREW_TASK_LOG: ${workspace.workLogPath}`,
@@ -374,6 +374,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
374
374
  rotatedForBudget,
375
375
  nearBudget,
376
376
  ...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
377
+ ...(input.trainingProjectContext === undefined ? {} : { trainingProjectContext: input.trainingProjectContext }),
377
378
  };
378
379
  if (input.attachments && input.attachments.length > 0) {
379
380
  if (dependencies.cancellation?.isRequested())
@@ -590,19 +591,23 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
590
591
  skill_count: abilityContext.skillCount,
591
592
  });
592
593
  }
594
+ const trainingRoots = input.trainingProjectContext?.projects.map((project) => project.root) ?? [];
593
595
  const { codexSkillRoots, claudeAdditionalDirectories } = await projectSkillRuntimeDirectories({
594
596
  runtime: runtime.name,
595
597
  ...(runtimeRoot === undefined ? {} : { runtimeRoot }),
596
598
  agentRoot: workspace.dir,
597
599
  projectSkillsPresent: input.projectSkills !== undefined,
598
600
  ...(input.projectContext === undefined ? {} : { projectContext: input.projectContext }),
601
+ ...(input.trainingProjectContext === undefined ? {} : {
602
+ trainingProjectRoots: input.trainingProjectContext.projects.map((project) => project.root),
603
+ }),
599
604
  ...(abilityContext?.skillRoot === undefined
600
605
  ? {}
601
606
  : { abilitySkillRoot: abilityContext.skillRoot }),
602
607
  });
603
608
  let runtimeEnv = childEnv;
604
609
  if (runtime.name === "claude"
605
- && input.projectContext !== undefined
610
+ && (input.projectContext !== undefined || trainingRoots.length > 0)
606
611
  && claudeAdditionalDirectories.length > 0) {
607
612
  const version = await (dependencies.probeClaudeVersion ?? probeClaudeVersion)(runtime.name, childEnv)
608
613
  .catch(() => null);
@@ -621,6 +626,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
621
626
  });
622
627
  }
623
628
  }
629
+ const legacyRoots = input.projectContext?.secondary.map((project) => project.root) ?? [];
630
+ const workspaceRoots = [...legacyRoots, ...trainingRoots];
624
631
  const launchRequest = {
625
632
  runtime: runtime.name,
626
633
  bin: runtime.name === "deepseek-harness" ? "dsh-acp-demo" : runtime.name,
@@ -651,9 +658,14 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
651
658
  ? {}
652
659
  : { claudeAdditionalDirectories }),
653
660
  ...(input.projectContext === undefined ? {} : {
654
- workspaceRoots: Object.freeze(input.projectContext.secondary.map((project) => project.root)),
655
661
  projectContext: input.projectContext,
656
662
  }),
663
+ ...(input.projectContext === undefined && workspaceRoots.length === 0
664
+ ? {}
665
+ : { workspaceRoots: Object.freeze(workspaceRoots) }),
666
+ ...(input.trainingProjectContext === undefined ? {} : {
667
+ trainingProjectContext: input.trainingProjectContext,
668
+ }),
657
669
  };
658
670
  return launchRequest;
659
671
  };