@nowcrew/daemon 0.6.77-beta.4 → 0.6.77-beta.6

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.
@@ -185,7 +185,7 @@ const materializeGit = async (cacheRoot, repositoryUrl, commit, target) => {
185
185
  await git(["--git-dir", bare, "fetch", "--no-tags", "--prune", "origin", commit]);
186
186
  const temp = `${target}.tmp-${randomUUID()}`;
187
187
  await rm(temp, { recursive: true, force: true });
188
- await git(["clone", "--no-checkout", "--shared", bare, temp]);
188
+ await git(["clone", "--shared", bare, temp]);
189
189
  await git(["checkout", "--detach", commit], temp);
190
190
  await rm(join(temp, ".git"), { recursive: true, force: true });
191
191
  await mkdir(dirname(target), { recursive: true });
@@ -10,7 +10,8 @@ import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js"
10
10
  import { daemonVersion } from "./machine-info.js";
11
11
  import { daemonReleaseProvenance } from "./release-provenance.js";
12
12
  import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
13
- import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
13
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
14
+ import { npmInvocation } from "./npm-command.js";
14
15
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
15
16
  function profileStorage(platform, userHome, runner) {
16
17
  return {
@@ -178,24 +179,34 @@ export function stableDaemonEntry(target, platform) {
178
179
  ? paths.join(target.npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js")
179
180
  : paths.join(target.npmPrefix, "lib", "node_modules", "@nowcrew", "daemon", "dist", "main.js");
180
181
  }
181
- export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl) {
182
- const command = platform === "win32" ? "npm.cmd" : "npm";
183
- const result = await runner(command, [
184
- "install",
185
- "--global",
186
- "--prefix",
187
- npmPrefix,
188
- "--ignore-scripts",
189
- "--no-audit",
190
- "--no-fund",
191
- ...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, {
192
- allowLoopbackHttp: daemonReleaseChannel(version) === "beta",
193
- })]),
194
- `@nowcrew/daemon@${version}`,
195
- ]);
196
- if (result.exitCode !== 0) {
197
- throw new Error(`Daemon package installation failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
182
+ export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl, nodePath = process.execPath) {
183
+ const registries = daemonRegistryCandidates({
184
+ betaUpdate: daemonReleaseChannel(version) === "beta",
185
+ ...(registryUrl === undefined ? {} : { registryUrl }),
186
+ });
187
+ if (registries === null)
188
+ throw new Error("Daemon package installation failed: invalid registry");
189
+ const npm = npmInvocation(platform, nodePath);
190
+ let lastFailure = "";
191
+ for (const registry of registries) {
192
+ const result = await runner(npm.command, [
193
+ ...npm.argsPrefix,
194
+ "install",
195
+ "--global",
196
+ "--prefix",
197
+ npmPrefix,
198
+ "--ignore-scripts",
199
+ "--no-audit",
200
+ "--no-fund",
201
+ "--registry",
202
+ registry,
203
+ `@nowcrew/daemon@${version}`,
204
+ ]);
205
+ if (result.exitCode === 0)
206
+ return;
207
+ lastFailure = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`;
198
208
  }
209
+ throw new Error(`Daemon package installation failed: ${lastFailure}`);
199
210
  }
200
211
  export async function waitForServiceRunning(spec, status, options = {}) {
201
212
  const timeoutMs = options.timeoutMs ?? 10_000;
@@ -6,6 +6,7 @@ import { assertProfileAgentsRootUnique, daemonHome, inspectProfileAgentsRoot, li
6
6
  import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, readServiceDescriptor, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
7
7
  import { releaseTagForServerUrl, assertReleaseTagForServerUrl } from "./automatic-install-target.js";
8
8
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
9
+ import { npmInvocation } from "./npm-command.js";
9
10
  import { defaultProcessController } from "./execution-journal.js";
10
11
  import { inspectJournalLock } from "./execution-journal-lock.js";
11
12
  import { detectDaemonLang, formatDaemonText } from "./i18n.js";
@@ -24,7 +25,7 @@ export function builtDaemonEntry(moduleUrl = import.meta.url) {
24
25
  : "";
25
26
  }
26
27
  function defaults() {
27
- const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
28
+ const npm = npmInvocation(process.platform, process.execPath);
28
29
  return {
29
30
  env: process.env,
30
31
  platform: process.platform,
@@ -35,7 +36,7 @@ function defaults() {
35
36
  runner: systemCommandRunner,
36
37
  ...(process.platform === "win32" ? { profileProtector: windowsDpapiProtector } : {}),
37
38
  resolveGlobalNodeModules: async () => {
38
- const result = await systemCommandRunner(npmCommand, ["root", "--global"]);
39
+ const result = await systemCommandRunner(npm.command, [...npm.argsPrefix, "root", "--global"]);
39
40
  if (result.exitCode !== 0 || !result.stdout.trim()) {
40
41
  throw new Error(`Unable to locate global npm modules: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
41
42
  }
@@ -279,7 +280,7 @@ export async function runComputerCommand(argv, overrides = {}) {
279
280
  await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix, releaseTag, configuredRegistryUrl === undefined
280
281
  ? restartProfile?.registryUrl
281
282
  ?? configuredRegistryUrl
282
- : normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }));
283
+ : normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }), deps.nodePath);
283
284
  }
284
285
  catch (error) {
285
286
  upgradeError = error;
@@ -5,6 +5,8 @@ import { createHash, randomUUID } from "node:crypto";
5
5
  import { promisify } from "node:util";
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
8
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
9
+ import { npmInvocation } from "./npm-command.js";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const WINDOWS_STOP_TIMEOUT_MS = 5_000;
10
12
  const WINDOWS_STOP_POLL_MS = 100;
@@ -542,10 +544,12 @@ export async function doctorService(spec, profilePrivate, runner = systemCommand
542
544
  { name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
543
545
  ];
544
546
  }
545
- export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl) {
547
+ export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl, nodePath = process.execPath) {
548
+ const npm = npmInvocation(platform, nodePath);
546
549
  return [
547
- platform === "win32" ? "npm.cmd" : "npm",
550
+ npm.command,
548
551
  [
552
+ ...npm.argsPrefix,
549
553
  "install",
550
554
  "--global",
551
555
  "--ignore-scripts",
@@ -557,9 +561,26 @@ export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest",
557
561
  ],
558
562
  ];
559
563
  }
560
- export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl) {
561
- const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registryUrl);
562
- await checked(runner, command, args, "Daemon upgrade");
564
+ export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl, nodePath = process.execPath) {
565
+ const registries = daemonRegistryCandidates({
566
+ betaUpdate: releaseTag === "beta",
567
+ ...(registryUrl === undefined ? {} : { registryUrl }),
568
+ env: process.env,
569
+ });
570
+ if (registries === null)
571
+ throw new Error("Daemon upgrade failed: invalid registry");
572
+ let lastFailure = "";
573
+ for (const registry of registries) {
574
+ const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registry, nodePath);
575
+ try {
576
+ await checked(runner, command, args, "Daemon upgrade");
577
+ return;
578
+ }
579
+ catch (error) {
580
+ lastFailure = error instanceof Error ? error.message : String(error);
581
+ }
582
+ }
583
+ throw new Error(lastFailure || "Daemon upgrade failed");
563
584
  }
564
585
  const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
565
586
  export async function hardenWindowsProfile(path, runner = systemCommandRunner) {
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** daemon 配置:从 env 读取,解析 crew CLI 路径 (注入给 agent 的 wrapper 用)。 */
2
2
  import { fileURLToPath } from "node:url";
3
- import { dirname, resolve } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { createRequire } from "node:module";
6
6
  import { detectDaemonLang, translateDaemon } from "./i18n.js";
@@ -59,11 +59,11 @@ function defaultCliPath() {
59
59
  return resolve(here, "../../cli/dist/main.js");
60
60
  }
61
61
  }
62
- // 临时 dev 验收名单。测试完成后删除此常量,恢复未配置时默认关闭的行为。
62
+ // 临时 dev 验收名单。这里必须填写 Server 生成的 Agent handle,而不是显示名称或频道 ID。
63
+ // 测试完成后删除此常量,恢复未配置时默认关闭的行为。
63
64
  const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
64
- "memory-v2-test-a",
65
- "memory-v2-test-b",
66
- "memory-v2-test-c",
65
+ "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-a
66
+ "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-b
67
67
  ];
68
68
  function isTemporaryDevServer(serverUrl) {
69
69
  try {
@@ -134,10 +134,18 @@ export function loadConfig(env = process.env) {
134
134
  maxFirstOutputWaitMs: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_FIRST_OUTPUT_WAIT_MS", DEFAULT_EXECUTION_LIMITS.maxFirstOutputWaitMs),
135
135
  firstOutputGraceMs: positiveIntegerEnv(env, "CREW_EXECUTION_FIRST_OUTPUT_GRACE_MS", DEFAULT_EXECUTION_LIMITS.firstOutputGraceMs),
136
136
  });
137
+ const agentsRoot = resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir());
138
+ const configuredReposRoot = env.NOWWORK_REPOS_ROOT?.trim();
139
+ const reposRoot = configuredReposRoot ? resolve(configuredReposRoot) : undefined;
140
+ const configuredQaReportRoot = env.QA_REPORT_HUB?.trim();
137
141
  return {
138
142
  serverUrl,
139
143
  machineToken,
140
- agentsRoot: resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir()),
144
+ agentsRoot,
145
+ ...(reposRoot === undefined ? {} : { reposRoot }),
146
+ qaReportRoot: configuredQaReportRoot
147
+ ? resolve(configuredQaReportRoot)
148
+ : join(dirname(reposRoot ?? agentsRoot), "qa-report-server-v2"),
141
149
  cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
142
150
  runtimeBin: env.CREW_RUNTIME ?? "claude",
143
151
  codexHomeMode: codexHomeModeEnv(env),
@@ -0,0 +1,20 @@
1
+ import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
2
+ export const DAEMON_PUBLIC_NPM_REGISTRY = normalizeDaemonNpmRegistry();
3
+ export const DAEMON_PUBLIC_NPM_MIRROR = "https://registry.npmmirror.com";
4
+ /** Keep every daemon package operation on the same ordered, channel-safe sources. */
5
+ export function daemonRegistryCandidates(input) {
6
+ try {
7
+ const requested = input.registryUrl ?? input.env?.CREW_NPM_REGISTRY;
8
+ const configured = requested === undefined
9
+ ? DAEMON_PUBLIC_NPM_REGISTRY
10
+ : normalizeDaemonNpmRegistry(requested, { allowLoopbackHttp: input.betaUpdate });
11
+ return [...new Set([
12
+ configured,
13
+ DAEMON_PUBLIC_NPM_REGISTRY,
14
+ ...(input.betaUpdate ? [DAEMON_PUBLIC_NPM_MIRROR] : []),
15
+ ])];
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
@@ -30,6 +30,7 @@ export function createDaemonUpdateController(deps) {
30
30
  targetVersion: message.targetVersion,
31
31
  packageRoot: eligibility.packageRoot,
32
32
  npmPrefix: eligibility.npmPrefix,
33
+ ...(eligibility.registryUrl === undefined ? {} : { registryUrl: eligibility.registryUrl }),
33
34
  onInstalling: () => {
34
35
  deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
35
36
  },
@@ -109,6 +109,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
109
109
  eligible: true,
110
110
  profileName,
111
111
  ...installation,
112
+ ...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
112
113
  serviceSpec: { ...spec, runtimeProcessIdentity: scope.processIdentity },
113
114
  };
114
115
  }
@@ -188,6 +189,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
188
189
  eligible: true,
189
190
  profileName,
190
191
  ...installation,
192
+ ...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
191
193
  serviceSpec: selectedSpec,
192
194
  };
193
195
  }
@@ -1,25 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { posix, resolve, win32 } from "node:path";
2
+ import { resolve } from "node:path";
3
3
  import { systemCommandRunner } from "./computer-service.js";
4
4
  import { daemonGlobalInstallation } from "./daemon-installation.js";
5
5
  import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
6
- import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
7
- const PUBLIC_NPM_REGISTRY = normalizeDaemonNpmRegistry();
8
- function configuredRegistries(betaUpdate) {
9
- try {
10
- const configured = process.env.CREW_NPM_REGISTRY === undefined
11
- ? PUBLIC_NPM_REGISTRY
12
- : normalizeDaemonNpmRegistry(process.env.CREW_NPM_REGISTRY, { allowLoopbackHttp: betaUpdate });
13
- return betaUpdate ? [...new Set([configured, PUBLIC_NPM_REGISTRY])] : [configured];
14
- }
15
- catch {
16
- return null;
17
- }
18
- }
19
- function npmCommand(platform, nodePath) {
20
- const paths = platform === "win32" ? win32 : posix;
21
- return paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
22
- }
6
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
7
+ import { npmInvocation } from "./npm-command.js";
23
8
  function registryArgs(registry) {
24
9
  return ["--registry", registry];
25
10
  }
@@ -51,10 +36,11 @@ async function readPackageVersion(packageRoot) {
51
36
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
52
37
  return typeof body.version === "string" ? body.version : "";
53
38
  }
54
- async function restoreInstalledVersion(input, runner, command, version, registryArgs) {
39
+ async function restoreInstalledVersion(input, runner, command, commandPrefix, version, registryArgs) {
55
40
  if (!isDaemonReleaseVersion(version))
56
41
  return false;
57
42
  const result = await runner(command, [
43
+ ...commandPrefix,
58
44
  "install",
59
45
  "--global",
60
46
  "--prefix",
@@ -110,7 +96,9 @@ export async function installExactDaemonUpdate(input) {
110
96
  }
111
97
  const runner = input.runner ?? systemCommandRunner;
112
98
  const platform = input.platform ?? process.platform;
113
- const command = npmCommand(platform, input.nodePath ?? process.execPath);
99
+ const npm = npmInvocation(platform, input.nodePath ?? process.execPath);
100
+ const command = npm.command;
101
+ const commandPrefix = npm.argsPrefix;
114
102
  let previousVersion = "";
115
103
  try {
116
104
  previousVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
@@ -124,8 +112,12 @@ export async function installExactDaemonUpdate(input) {
124
112
  await release();
125
113
  return { ok: false, errorCode: "install_failed" };
126
114
  }
127
- const registries = configuredRegistries(daemonReleaseChannel(previousVersion) === "beta"
128
- || daemonReleaseChannel(input.targetVersion) === "beta");
115
+ const registries = daemonRegistryCandidates({
116
+ betaUpdate: daemonReleaseChannel(previousVersion) === "beta"
117
+ || daemonReleaseChannel(input.targetVersion) === "beta",
118
+ ...(input.registryUrl === undefined ? {} : { registryUrl: input.registryUrl }),
119
+ env: process.env,
120
+ });
129
121
  if (registries === null) {
130
122
  await release();
131
123
  return { ok: false, errorCode: "install_failed" };
@@ -139,10 +131,12 @@ export async function installExactDaemonUpdate(input) {
139
131
  }
140
132
  let selectedRegistryArgs = [];
141
133
  let installed = false;
142
- let authFailed = false;
134
+ let authFailures = 0;
135
+ let failedAttempts = 0;
143
136
  for (const registry of registries) {
144
137
  const args = registryArgs(registry);
145
138
  const result = await runner(command, [
139
+ ...commandPrefix,
146
140
  "install",
147
141
  "--global",
148
142
  "--prefix",
@@ -158,12 +152,18 @@ export async function installExactDaemonUpdate(input) {
158
152
  installed = true;
159
153
  break;
160
154
  }
155
+ failedAttempts += 1;
161
156
  if (result !== null && isRegistryAuthFailure(result.stderr))
162
- authFailed = true;
157
+ authFailures += 1;
163
158
  }
164
159
  if (!installed) {
165
160
  await release();
166
- return { ok: false, errorCode: authFailed ? "registry_auth_failed" : "install_failed" };
161
+ return {
162
+ ok: false,
163
+ errorCode: failedAttempts > 0 && authFailures === failedAttempts
164
+ ? "registry_auth_failed"
165
+ : "install_failed",
166
+ };
167
167
  }
168
168
  let installedVersion = "";
169
169
  try {
@@ -173,7 +173,7 @@ export async function installExactDaemonUpdate(input) {
173
173
  // Version verification is authoritative; unreadable package metadata is a mismatch.
174
174
  }
175
175
  if (installedVersion !== input.targetVersion) {
176
- const restored = await restoreInstalledVersion(input, runner, command, previousVersion, selectedRegistryArgs);
176
+ const restored = await restoreInstalledVersion(input, runner, command, commandPrefix, previousVersion, selectedRegistryArgs);
177
177
  if (!restored) {
178
178
  console.error(`[daemon-update] version verification failed; restore of ${previousVersion} was unsuccessful`);
179
179
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { join } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
4
4
  import { JournalConflictError } from "./execution-journal.js";
5
5
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -524,10 +524,11 @@ export async function runExecution(config, input, dependencies) {
524
524
  if (dependencies.abilityRelease !== undefined) {
525
525
  await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
526
526
  }
527
+ const reposRoot = config.reposRoot ?? join(config.agentsRoot, "repos");
527
528
  const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
528
529
  trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
529
- reposRoot: join(config.agentsRoot, "repos"),
530
- lockRoot: join(config.agentsRoot, ".crew", "project-dependencies"),
530
+ reposRoot,
531
+ lockRoot: join(dirname(resolve(reposRoot)), ".crew", "project-dependencies"),
531
532
  }), dependencies.cancellation);
532
533
  trainingProjectContext = Object.freeze({
533
534
  projects: Object.freeze(preparedProjectDependencies.map((project) => Object.freeze({
@@ -862,6 +863,8 @@ export async function runExecution(config, input, dependencies) {
862
863
  token: credential.token,
863
864
  agentId: credential.agentId,
864
865
  agentsRoot: config.agentsRoot,
866
+ ...(config.reposRoot === undefined ? {} : { reposRoot: config.reposRoot }),
867
+ ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
865
868
  cliPath: config.cliPath,
866
869
  providerConfig,
867
870
  codexHomeMode: config.codexHomeMode,
@@ -466,6 +466,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
466
466
  CREW_TOKEN: input.launch.token,
467
467
  CREW_CHANNEL: input.channelId,
468
468
  CREW_HOME: workspace.dir,
469
+ ...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
470
+ ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
469
471
  // Expose only the training root; ability-specific workspace layout stays
470
472
  // owned by the training package rather than the NowWork daemon.
471
473
  QA_RUNTIME_ROOT: join(workspace.dir, "training"),
package/dist/main.js CHANGED
@@ -117,6 +117,23 @@ async function main() {
117
117
  }
118
118
  const provenance = daemonReleaseProvenance(daemonVersion());
119
119
  assertReleaseTagForServerUrl(config.serverUrl, provenance.releaseTag);
120
+ const storageRuntime = values.profile === undefined
121
+ ? null
122
+ : await loadStorageRuntime({
123
+ profile: values.profile,
124
+ serverUrl: config.serverUrl,
125
+ userHome: homedir(),
126
+ platform: process.platform,
127
+ environment: process.env,
128
+ });
129
+ const runtimeConfig = storageRuntime === null
130
+ ? config
131
+ : Object.freeze({
132
+ ...config,
133
+ agentsRoot: storageRuntime.layout.agentItemsRoot,
134
+ reposRoot: storageRuntime.layout.repoRoot,
135
+ qaReportRoot: storageRuntime.layout.qaReportRoot,
136
+ });
120
137
  if (cmd === "serve") {
121
138
  process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}, ${provenance.releaseChannel}/${provenance.releaseTag}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
122
139
  const installation = values.profile === undefined
@@ -125,15 +142,6 @@ async function main() {
125
142
  const installationLease = installation === null
126
143
  ? null
127
144
  : await acquireDaemonInstallationLease(installation.npmPrefix);
128
- const storageRuntime = values.profile === undefined
129
- ? null
130
- : await loadStorageRuntime({
131
- profile: values.profile,
132
- serverUrl: config.serverUrl,
133
- userHome: homedir(),
134
- platform: process.platform,
135
- environment: process.env,
136
- });
137
145
  const migrationRuntime = values.profile === undefined || activeDaemonHome === undefined || storageRuntime !== null
138
146
  ? { ready: false, reason: "not_legacy_source" }
139
147
  : await probeStorageMigrationRuntime({
@@ -188,7 +196,7 @@ async function main() {
188
196
  }
189
197
  : undefined;
190
198
  await runWithDaemonInstallationLease(installationLease, async () => {
191
- const service = serve(config, {
199
+ const service = serve(runtimeConfig, {
192
200
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
193
201
  ...(storageMigration === undefined ? {} : { storageMigration }),
194
202
  });
@@ -209,9 +217,9 @@ async function main() {
209
217
  gitSha: provenance.gitSha,
210
218
  artifactDigest: provenance.artifactDigest,
211
219
  ...(values.profile === undefined ? {} : { profileName: values.profile }),
212
- agentsRoot: config.agentsRoot,
220
+ agentsRoot: runtimeConfig.agentsRoot,
213
221
  }); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
214
- const result = await runAgent(config, {
222
+ const result = await runAgent(runtimeConfig, {
215
223
  handle: values.agent,
216
224
  channelId: values.channel,
217
225
  ...(values.wake ? { wake: values.wake } : {}),
@@ -0,0 +1,9 @@
1
+ import { posix, win32 } from "node:path";
2
+ /** Execute POSIX npm through its owning Node binary so service PATH is irrelevant. */
3
+ export function npmInvocation(platform, nodePath = process.execPath) {
4
+ const paths = platform === "win32" ? win32 : posix;
5
+ const npmPath = paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
6
+ return platform === "win32"
7
+ ? { command: npmPath, argsPrefix: [] }
8
+ : { command: nodePath, argsPrefix: [npmPath] };
9
+ }
@@ -111,8 +111,8 @@ async function ensureWorktreeExclude(mainRoot) {
111
111
  const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
112
112
  await writeFile(excludePath, current + suffix + ".worktrees\n", "utf8");
113
113
  }
114
- async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
115
- const mainRoot = resolve(reposRoot, repositoryKey);
114
+ async function ensureMainClone(git, repo, repositoryKey, directoryName, reposRoot) {
115
+ const mainRoot = resolve(reposRoot, directoryName);
116
116
  if (!isWithin(reposRoot, mainRoot) || await existingAncestorIsSymlink(reposRoot, mainRoot)) {
117
117
  throw new ProjectDependencyError("project_dependency_checkout_conflict", "repository path is unsafe", repositoryKey);
118
118
  }
@@ -123,7 +123,7 @@ async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
123
123
  if (info === null) {
124
124
  await mkdir(dirname(mainRoot), { recursive: true });
125
125
  try {
126
- await git.run(["clone", "--no-checkout", repo, mainRoot]);
126
+ await git.run(["clone", repo, mainRoot]);
127
127
  }
128
128
  catch {
129
129
  await rm(mainRoot, { recursive: true, force: true }).catch(() => { });
@@ -243,7 +243,7 @@ export function createProjectDependencyProvisioner(options = {}) {
243
243
  for (const { identity, projects } of grouped.values()) {
244
244
  try {
245
245
  await withProjectDependencyLock(lockRoot, identity.repositoryKey, async () => {
246
- const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, input.reposRoot);
246
+ const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, identity.directoryName, input.reposRoot);
247
247
  const seenVersions = new Set();
248
248
  for (const project of projects) {
249
249
  const branchSource = project.branch === undefined ? "remote-default" : "manifest";
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
3
3
  const SCP_REPOSITORY = /^git@([^:]+):(.+)$/u;
4
4
  const COMMIT = /^[a-f0-9]{40}$/u;
5
+ const DIRECTORY_HASH_LENGTH = 12;
5
6
  function normalizePath(value) {
6
7
  const path = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
7
8
  if (!path || path.split("/").some((part) => part.length === 0 || part === "." || part === ".." || part.includes("\\"))) {
@@ -31,6 +32,7 @@ export function normalizeRepositoryIdentity(repository) {
31
32
  canonical,
32
33
  repositoryKey: `repo-${hash}`,
33
34
  displayName,
35
+ directoryName: `${displayName}--${hash.slice(0, DIRECTORY_HASH_LENGTH)}`,
34
36
  });
35
37
  }
36
38
  export function worktreeDirectoryName(branch, resolvedCommit) {
package/dist/runner.js CHANGED
@@ -84,6 +84,8 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
84
84
  token: credential.token,
85
85
  agentId: credential.agentId,
86
86
  agentsRoot: config.agentsRoot,
87
+ ...(config.reposRoot === undefined ? {} : { reposRoot: config.reposRoot }),
88
+ ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
87
89
  cliPath: config.cliPath,
88
90
  providerConfig,
89
91
  codexHomeMode: config.codexHomeMode,
@@ -121,6 +121,7 @@ export function storageLayout(input) {
121
121
  agentItemsRoot: paths.join(workspaceRoot, "agent-items"),
122
122
  daemonRoot: paths.join(workspaceRoot, "daemon"),
123
123
  repoRoot: paths.join(workspaceRoot, "repos"),
124
+ qaReportRoot: paths.join(workspaceRoot, "qa-report-server-v2"),
124
125
  suiteToolsRoot: paths.join(workspaceRoot, "suite-tools"),
125
126
  migrationsRoot: paths.join(persistentRoot, "migrations"),
126
127
  runtimeRoot,
@@ -132,7 +133,7 @@ export function agentStorageRoot(layout, agentId) {
132
133
  }
133
134
  const COMPARABLE_FIELDS = [
134
135
  "layoutVersion", "environment", "workspaceId", "persistentRoot", "environmentRoot", "workspaceRoot",
135
- "agentItemsRoot", "daemonRoot", "repoRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
136
+ "agentItemsRoot", "daemonRoot", "repoRoot", "qaReportRoot", "suiteToolsRoot", "migrationsRoot", "runtimeRoot",
136
137
  ];
137
138
  export function compareStorageLayout(current, expected) {
138
139
  if (current === null || current.layoutVersion !== STORAGE_LAYOUT_VERSION)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.77-beta.4",
3
+ "version": "0.6.77-beta.6",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",