@arcships/morula-runtime 0.1.0-alpha.14 → 0.1.0-alpha.16

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.
package/dist/index.js CHANGED
@@ -740,7 +740,7 @@ var BUILD_ID, BUILD_CONTEXTS, BUILD_DEFAULT_CONTEXT;
740
740
  var init_build_env = __esm({
741
741
  "../morula-cli-core/dist/build-env.js"() {
742
742
  "use strict";
743
- BUILD_ID = "20260923140226.31d77ab2";
743
+ BUILD_ID = "20260929103157.019b0b1c";
744
744
  BUILD_CONTEXTS = {
745
745
  "local": "http://localhost:8080/api",
746
746
  "test": "https://morula.basecastle.com/api",
@@ -2588,9 +2588,9 @@ async function setupAutostart(context) {
2588
2588
  await removeLegacyAutostart();
2589
2589
  const { command, args } = resolvePortalCommand();
2590
2590
  const stateDir2 = process.env.MORULA_CONFIG_DIR ?? join3(homedir2(), ".morula-cli");
2591
- const daemonDir2 = join3(stateDir2, "daemon");
2592
- mkdirSync2(daemonDir2, { recursive: true, mode: 448 });
2593
- const logFile = join3(daemonDir2, "portal.log");
2591
+ const daemonDir = join3(stateDir2, "daemon");
2592
+ mkdirSync2(daemonDir, { recursive: true, mode: 448 });
2593
+ const logFile = join3(daemonDir, "portal.log");
2594
2594
  const plist = buildLaunchAgentPlist({ label: LABEL, command, args, logFile });
2595
2595
  const plistPath = launchAgentPath();
2596
2596
  mkdirSync2(join3(plistPath, ".."), { recursive: true });
@@ -2626,7 +2626,13 @@ var init_portal_autostart = __esm({
2626
2626
  // src/daemon/daemon-config.ts
2627
2627
  import { randomUUID } from "node:crypto";
2628
2628
  import { homedir as homedir4 } from "node:os";
2629
- import { join as join5 } from "node:path";
2629
+ import { dirname as dirname4, join as join5 } from "node:path";
2630
+ function configFilePathForEnv(env = process.env) {
2631
+ return env.MORULA_CONFIG ?? join5(env.MORULA_CONFIG_DIR ?? join5(homedir4(), ".morula-cli"), "config.yaml");
2632
+ }
2633
+ function daemonDirForEnv(env = process.env) {
2634
+ return join5(dirname4(configFilePathForEnv(env)), "daemon");
2635
+ }
2630
2636
  function loadDaemonConfig(store) {
2631
2637
  const config = store.load();
2632
2638
  const d = config?.[DAEMON_KEY];
@@ -2666,7 +2672,7 @@ var init_daemon_config = __esm({
2666
2672
  // src/daemon/daemon-lock.ts
2667
2673
  import { execFileSync as execFileSync3 } from "node:child_process";
2668
2674
  import { closeSync as closeSync2, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync3, writeSync } from "node:fs";
2669
- import { dirname as dirname4, join as join6 } from "node:path";
2675
+ import { dirname as dirname5, join as join6 } from "node:path";
2670
2676
  function daemonPidPath(configDir) {
2671
2677
  return join6(configDir, "daemon", DAEMON_PID_FILE);
2672
2678
  }
@@ -2769,7 +2775,7 @@ function acquireDaemonLock(lockPath, input) {
2769
2775
  startedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
2770
2776
  workdir: input.workdir
2771
2777
  };
2772
- mkdirSync4(dirname4(lockPath), { recursive: true, mode: 448 });
2778
+ mkdirSync4(dirname5(lockPath), { recursive: true, mode: 448 });
2773
2779
  try {
2774
2780
  const fd = openSync2(lockPath, "wx", 384);
2775
2781
  try {
@@ -2854,7 +2860,7 @@ __export(portal_instances_exports, {
2854
2860
  });
2855
2861
  import { readdirSync, statSync } from "node:fs";
2856
2862
  import { homedir as homedir5 } from "node:os";
2857
- import { dirname as dirname5, join as join7 } from "node:path";
2863
+ import { dirname as dirname6, join as join7 } from "node:path";
2858
2864
  function parseBaseUrl(raw) {
2859
2865
  if (!raw)
2860
2866
  return null;
@@ -2922,7 +2928,7 @@ function createPortalEnvironmentResolver(options = {}) {
2922
2928
  const env = options.env ?? process.env;
2923
2929
  const log = options.log ?? (() => {
2924
2930
  });
2925
- const defaultConfigDir = dirname5(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2931
+ const defaultConfigDir = dirname6(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2926
2932
  let index = [];
2927
2933
  let signature = "";
2928
2934
  function buildIndex() {
@@ -2939,7 +2945,7 @@ function createPortalEnvironmentResolver(options = {}) {
2939
2945
  const normalized = normalizeBaseUrl2(entry.url);
2940
2946
  if (!normalized)
2941
2947
  continue;
2942
- const configDir = dirname5(file);
2948
+ const configDir = dirname6(file);
2943
2949
  built.push({
2944
2950
  configDir,
2945
2951
  context,
@@ -3482,7 +3488,7 @@ var init_dim_locate = __esm({
3482
3488
  // src/daemon/acp/acp-process-registry.ts
3483
3489
  import { execFileSync as execFileSync4 } from "node:child_process";
3484
3490
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
3485
- import { dirname as dirname6 } from "node:path";
3491
+ import { dirname as dirname7 } from "node:path";
3486
3492
  function parseLstartToMs(tokens) {
3487
3493
  if (tokens.length < 5)
3488
3494
  return null;
@@ -3595,7 +3601,7 @@ function persistRecords() {
3595
3601
  if (!recordFile)
3596
3602
  return;
3597
3603
  try {
3598
- mkdirSync5(dirname6(recordFile), { recursive: true });
3604
+ mkdirSync5(dirname7(recordFile), { recursive: true });
3599
3605
  writeFileSync5(recordFile, JSON.stringify({
3600
3606
  v: RECORD_FILE_VERSION,
3601
3607
  children: [...ownedChildren.values()]
@@ -3838,9 +3844,9 @@ var init_acp_connection = __esm({
3838
3844
  process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
3839
3845
  `);
3840
3846
  try {
3841
- const { existsSync: existsSync19 } = await import("node:fs");
3847
+ const { existsSync: existsSync21 } = await import("node:fs");
3842
3848
  process.stderr.write(
3843
- `[daemon] diag: fileExists=${existsSync19(dimPath)} cwdExists=${this.options.cwd ? existsSync19(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
3849
+ `[daemon] diag: fileExists=${existsSync21(dimPath)} cwdExists=${this.options.cwd ? existsSync21(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
3844
3850
  `
3845
3851
  );
3846
3852
  } catch {
@@ -4424,14 +4430,13 @@ init_dist2();
4424
4430
  init_dist2();
4425
4431
 
4426
4432
  // src/build-env.ts
4427
- var BUILD_VERSION2 = "0.1.0-alpha.14";
4433
+ var BUILD_VERSION2 = "0.1.0-alpha.16";
4428
4434
 
4429
4435
  // src/commands/daemon.ts
4430
4436
  init_dist();
4431
4437
  init_dist2();
4432
- import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync12, rmSync as rmSync10, writeFileSync as writeFileSync13 } from "node:fs";
4433
- import { homedir as homedir13 } from "node:os";
4434
- import { dirname as dirname11, join as join22 } from "node:path";
4438
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync12, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
4439
+ import { dirname as dirname13, join as join23 } from "node:path";
4435
4440
 
4436
4441
  // src/daemon/daemon-autostart.ts
4437
4442
  init_portal_autostart();
@@ -4500,9 +4505,9 @@ init_portal_server();
4500
4505
  // src/daemon/runner.ts
4501
4506
  init_dist2();
4502
4507
  import { execFileSync as execFileSync11 } from "node:child_process";
4503
- import { existsSync as existsSync16, mkdirSync as mkdirSync14, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
4508
+ import { existsSync as existsSync17, mkdirSync as mkdirSync15, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
4504
4509
  import { arch, hostname } from "node:os";
4505
- import { dirname as dirname9, join as join20 } from "node:path";
4510
+ import { dirname as dirname11, join as join21 } from "node:path";
4506
4511
 
4507
4512
  // src/daemon/acp/acp-connection-pool.ts
4508
4513
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
@@ -4637,6 +4642,10 @@ var DaemonApi = class {
4637
4642
  * D1(daemon-server-connection-unstable):心跳是「设备存活」的唯一信号,显式 10s 超时——
4638
4643
  * 慢请求快速失败,避免占住单飞槽位(客户端默认 30s,对心跳过松)。
4639
4644
  * 说明:`client.post()` 的第三参是 query,不支持 RequestOptions,故直接走 `client.request`。
4645
+ *
4646
+ * `upgradeRequested`(runtime-self-upgrade,沿用 stop 的一次性认领模式):服务端置了
4647
+ * upgradeRequestedAt → 本次心跳认领(服务端已清指令)→ daemon 触发自升级;`upgradeVersion`
4648
+ * 为置指令时的目标版本(可空 = daemon 自行解析 latest)。
4640
4649
  */
4641
4650
  heartbeat(runtimeId, runningTaskCount = 0) {
4642
4651
  return this.client.request(
@@ -4647,6 +4656,13 @@ var DaemonApi = class {
4647
4656
  pollTasks(runtimeId, max = 1) {
4648
4657
  return this.client.get("/agent-tasks/poll", { runtimeId, max });
4649
4658
  }
4659
+ /**
4660
+ * runtime-self-upgrade:自升级结果回报(机器协议,设备级凭证)。
4661
+ * best-effort 语义由调用方掌握(失败只 log,不影响本地升级结果)。
4662
+ */
4663
+ reportUpgradeResult(runtimeId, body) {
4664
+ return this.client.post(`/agent-runtimes/${runtimeId}/upgrade-result`, body);
4665
+ }
4650
4666
  claim(taskId, runtimeId) {
4651
4667
  const res = this.client.post(`/agent-tasks/${taskId}/claim`, void 0, { runtimeId });
4652
4668
  res.then((r) => {
@@ -5076,7 +5092,7 @@ ${repoLine}- **\u4E0D\u8981**\u4F7F\u7528 \`gh\`\uFF08GitHub CLI \u5728\u672C\u5
5076
5092
  \`\`\`
5077
5093
  \u5185\u5BB9\u4E3A\u7ED3\u8BBA + \u5173\u952E\u95EE\u9898\uFF08\u6587\u4EF6:\u884C + \u5EFA\u8BAE\uFF09\uFF0C\u672B\u5C3E\u5E26\u7F72\u540D\u884C \`> \u7531\u81EA\u52A8\u5316\u300C<\u81EA\u52A8\u5316\u540D\u79F0>\u300D\u4E8E <\u65E5\u671F> \u751F\u6210\`\uFF08\u4F9B\u4E0B\u4E00\u8F6E\u81EA\u8BC6\u522B\uFF09\uFF1B
5078
5094
  **\u4E0D\u8981\u7528 \`morula review submit\`**\uFF08\u90A3\u662F\u8BC4\u5BA1\u4EFB\u52A1\u7684\u673A\u5668\u534F\u8BAE\uFF0C\u4F1A\u9A71\u52A8\u4E1A\u52A1\u4EFB\u52A1\u72B6\u6001\u56DE\u9000\uFF09\u3002
5079
- - \u5173\u8054/\u5173\u5355\u8BED\u4E49\u4E0E\u4E0A\u6587\u4E00\u81F4\uFF1AMR \u7684 title / body / \u6E90\u5206\u652F\u51FA\u73B0\u4EFB\u52A1\u7F16\u53F7 \u2192 \u81EA\u52A8\u5173\u8054\uFF1B\`Closes <\u4EFB\u52A1\u7F16\u53F7>\` \u2192 merge \u65F6\u81EA\u52A8\u6D41\u8F6C\u4EFB\u52A1\u3002
5095
+ - \u5173\u8054/\u5173\u5355\u8BED\u4E49\u4E0E\u4E0A\u6587\u4E00\u81F4\uFF1APR \u5173\u8054**\u53EA\u7531\u5E73\u53F0\u901A\u9053\u4EA7\u751F**\uFF08\u670D\u52A1\u7AEF\u4E0D\u8BFB PR \u6587\u672C\uFF09\uFF1B\u5173\u8054\u4E0A\u7684 PR merge \u540E\u670D\u52A1\u7AEF\u6309\u65E2\u6709\u6D41\u7A0B\u63A8\u8FDB\u4EFB\u52A1\u72B6\u6001\u3002
5080
5096
  `;
5081
5097
  }
5082
5098
  var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u5951\u7EA6\uFF09
@@ -5130,15 +5146,20 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5130
5146
  > \u7EAF\u95EE\u7B54\u3001\u8C03\u7814\u3001\u5206\u6790\u3001\u672C\u5730\u8D77\u670D\u52A1\u7C7B\u4EFB\u52A1\uFF1A**\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u63D0 PR\u3001\u6700\u7EC8\u8BC4\u8BBA\u4E0D\u51FA\u73B0 PR \u7AE0\u8282**\uFF0C\u76F4\u63A5\u56DE\u7B54\u7528\u6237\u95EE\u9898\u3002
5131
5147
 
5132
5148
  - \u672C\u4EFB\u52A1\u7684\u5BF9\u5916\u7F16\u53F7\uFF08\u5728\u4EFB\u52A1\u770B\u677F URL / \u5217\u8868\u53EF\u89C1\uFF09\u53EF\u80FD\u5F62\u5982 \`STR-101\` \u6216 \`AGENT-463963\`\u3002
5133
- - **PR \u5173\u8054**\uFF1APR \u7684 title / branch / body \u4E2D\u51FA\u73B0\u4EFB\u52A1\u7F16\u53F7 \u2192 \u670D\u52A1\u7AEF\u81EA\u52A8\u628A PR \u5173\u8054\u5230\u4EFB\u52A1\u3002
5134
- - **\u5173\u5355**\uFF1A\u53EA\u6709 \`Closes\` / \`Fixes\` / \`Resolves\` \u540E**\u7D27\u90BB**\u4EFB\u52A1\u7F16\u53F7\uFF08\u5982 \`Closes STR-101\`\uFF09\u2192 \u8BE5 PR merge \u65F6\u670D\u52A1\u7AEF\u81EA\u52A8\u628A\u4EFB\u52A1\u6D41\u8F6C\u5230\u5B8C\u6210\u72B6\u6001\u3002
5135
- - **\u7F16\u53F7\u7528\u5BF9\u5916\u7F16\u53F7\uFF08code\uFF0C\u5982 AGENT-463963 / STR-101\uFF09\uFF0C\u4E0D\u8981\u7528 UUID**\uFF1AMR/\u5206\u652F\u91CC\u7684\u4EFB\u52A1\u7F16\u53F7\u5FC5\u987B\u7528\u5BF9\u5916\u7F16\u53F7\u2014\u2014\u670D\u52A1\u7AEF\u7684\u5173\u5355/\u5173\u8054\u626B\u63CF\uFF08\`Closes\` \u901A\u9053\uFF09\u53EA\u8BC6\u522B\u5BF9\u5916\u7F16\u53F7\uFF0CUUID \u53EA\u7528\u4E8E\u4E1A\u52A1\u5BF9\u8C61\uFF08Object\uFF09\uFF0Cagent \u4EFB\u52A1\uFF08AgentTask\uFF09\u4E0D\u4F1A\u88AB UUID \u5173\u5355\u8BC6\u522B\uFF08\u90A3\u4F1A\u628A MR \u5173\u8054\u5931\u8D25\uFF09\u3002
5136
- - \u82E5\u4EFB\u52A1\u8BE6\u60C5\u53EA\u6709 UUID \u6CA1\u6709\u5BF9\u5916\u7F16\u53F7\uFF0C\u628A\u4EFB\u52A1\u7F16\u53F7\u5199\u8FDB PR \u63CF\u8FF0\u524D\u5148\u786E\u8BA4\u662F\u5426\u5B58\u5728\u5BF9\u5916\u7F16\u53F7\uFF1B\u786E\u65E0\u518D\u7528 UUID \u4F5C\u4E3A\u515C\u5E95\u3002
5149
+ - **PR \u5173\u8054\u53EA\u7531\u5E73\u53F0\u901A\u9053\u4EA7\u751F**\uFF082026-09-24 \u5B9A\u6848\uFF09\uFF1A**\u53EA\u6709\u7ECF\u5E73\u53F0\u901A\u9053\u5EFA\u51FA\u6765\u7684 PR \u4F1A\u81EA\u52A8\u5173\u8054\u5230\u4EFB\u52A1**\u2014\u2014
5150
+ \`morula repo mr create\`\uFF08\u63A8\u8350\uFF09\u6216 \`gh pr create\` \u540E\u8C03 \`report_pr\` \u5DE5\u5177\u4E0A\u62A5\u3002\u5E73\u53F0\u6309\u81EA\u5DF1\u7684\u4E8B\u5B9E\uFF08\u54EA\u4E2A\u4EFB\u52A1\u3001
5151
+ \u54EA\u6761\u5206\u652F\u3001\u54EA\u4E2A workContext\uFF09\u8BA4\u9886\uFF0C**\u4E0D\u8BFB PR \u6587\u672C**\u3002
5152
+ - **\u4E0D\u8981\u5728 PR \u6807\u9898/\u6B63\u6587\u91CC\u9760\u4EFB\u52A1\u7F16\u53F7\u300C\u6807\u8BB0\u5F52\u5C5E\u300D**\uFF1A\u670D\u52A1\u7AEF**\u4E0D\u518D\u626B\u63CF** title/branch/body \u627E\u4EFB\u52A1\u7F16\u53F7\u3002
5153
+ \u4EBA\u5DE5\u5728 GitLab/GitHub \u4E0A\u624B\u5DE5\u5EFA\u7684 PR \u5E73\u53F0**\u4E0D\u5173\u8054**\uFF08\u4E0D\u8FDB\u5173\u8054\u533A\u3001\u4E0D\u89E6\u53D1\u81EA\u52A8\u8BC4\u5BA1\u3001\u4E0D\u9A71\u52A8\u4EFB\u52A1\u72B6\u6001\uFF09\u3002
5154
+ - **\u5173\u5355**\uFF1A\u5173\u8054\u4E0A\u7684 PR \u5408\u5E76\u540E\uFF0C\u670D\u52A1\u7AEF\u6309\u65E2\u6709\u6D41\u7A0B\u81EA\u52A8\u63A8\u8FDB\u4EFB\u52A1\u72B6\u6001\uFF08\u5168\u90E8 MR \u5408\u5E76 \u2192 \u5DF2\u5408\u5E76\uFF1B\u8FD9\u4E9B\u63D0\u4EA4\u8FDB\u5165\u751F\u4EA7\u5206\u652F \u2192 \u5DF2\u53D1\u5E03\uFF09\u3002
5155
+ \u4E0D\u9700\u8981 \`Closes\` \u4E4B\u7C7B\u5173\u952E\u5B57\u2014\u2014**\u5173\u8054\u672C\u8EAB\u5C31\u662F\u6388\u6743**\u3002
5156
+ - \u4ECD\u7136\u5EFA\u8BAE PR \u6807\u9898\u4EE5\u300C\`<\u4EFB\u52A1\u7F16\u53F7>: \u63CF\u8FF0\`\u300D\u5F00\u5934\u3001\u6B63\u6587\u5199\u4E00\u53E5 \`Closes <\u4EFB\u52A1\u7F16\u53F7>\`\uFF1A**\u8FD9\u662F\u7ED9\u4EBA\u770B\u7684\u53EF\u8BFB\u6027**\uFF08\u5E73\u53F0\u4E0D\u518D\u4F9D\u8D56\u5B83\uFF09\uFF0C
5157
+ \u4E14\u662F\u4EFB\u52A1\u4E0E PR \u4E4B\u95F4\u6700\u7701\u4E8B\u7684\u5BF9\u7167\u7EBF\u7D22\u3002
5137
5158
 
5138
5159
  ## 0.3 \u5206\u652F\u547D\u540D\uFF1A\u7531\u4F60\uFF08Agent\uFF09\u53D6\u540D\u5E76\u4E0A\u62A5
5139
5160
 
5140
5161
  - **\u4EFB\u52A1 ID \u83B7\u53D6**\uFF1A\u672C\u4EFB\u52A1\u7684 AgentTask ID\uFF08UUID\uFF09\u5728\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF08\u4E5F\u662F \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u8FD4\u56DE\u7684 \`id\`\uFF0C\`.agent_context/task-context.md\` \u5F00\u5934\u4EA6\u6709\uFF09\u3002\u9700\u8981\u8C03\u7528 \`morula task branch\` \u65F6\u7528\u5B83\u4F5C\u4E3A taskId\u3002
5141
- - **\u91CD\u8981\u533A\u5206**\uFF1A\`morula task branch <taskId>\` \u7684 taskId \u7528**\u672C\u4EFB\u52A1 UUID**\uFF08\u673A\u5668\u534F\u8BAE\uFF09\uFF1B\u4F46 **MR / PR \u91CC\u7684\u4EFB\u52A1\u7F16\u53F7\u7528\u5BF9\u5916\u7F16\u53F7 code**\uFF08\u5982 \`AGENT-958201\`\uFF0C\u7528\u4E8E\u670D\u52A1\u7AEF\u5173\u8054/\u5173\u5355\uFF09\u2014\u2014**\u4E0D\u8981\u7528 UUID \u6807\u8BB0 MR**\uFF08UUID \u5173\u5355\u53EA\u8BA4\u4E1A\u52A1\u5BF9\u8C61\uFF0CAgentTask \u4E0D\u4F1A\u88AB\u8BC6\u522B\uFF0C\u4F1A\u5BFC\u81F4 MR \u5173\u8054\u5931\u8D25\uFF09\u3002
5162
+ - **\u91CD\u8981\u533A\u5206**\uFF1A\`morula task branch <taskId>\` \u7684 taskId \u7528**\u672C\u4EFB\u52A1 UUID**\uFF08\u673A\u5668\u534F\u8BAE\uFF09\uFF1BMR \u6807\u9898\u91CC\u7684\u4EFB\u52A1\u7F16\u53F7\u7528**\u5BF9\u5916\u7F16\u53F7 code**\uFF08\u5982 \`AGENT-958201\`\uFF09\u2014\u2014\u4F46\u6CE8\u610F**\u7F16\u53F7\u53EA\u5F71\u54CD\u53EF\u8BFB\u6027\uFF0C\u4E0D\u518D\u5F71\u54CD\u5173\u8054**\uFF08\u670D\u52A1\u7AEF\u4E0D\u8BFB PR \u6587\u672C\uFF0C\u89C1\u7B2C 0.2 / 2 \u6761\uFF09\u3002
5142
5163
  - **\u53EA\u6709\u9700\u8981\u6539\u4EE3\u7801\u5E76\u63A8\u5206\u652F\u7684\u4EFB\u52A1\u624D\u53D6\u540D**\uFF1B\u7EAF\u95EE\u7B54/\u53EA\u8BFB\u8C03\u7814/\u672C\u5730\u8D77\u670D\u52A1\u4E0D\u5EFA\u5206\u652F\uFF08\u89C1\u7B2C 3 \u6761\u51B3\u7B56\u6811\uFF09\u3002
5143
5164
  - **\u5148\u68C0\u67E5\u5F53\u524D\u5206\u652F**\uFF1A\u82E5\u4ED3\u5E93\u5DF2\u6709 feature \u5206\u652F\uFF08\u540C\u4EFB\u52A1/\u540C\u4E00\u6539\u52A8\u95ED\u73AF\u7684\u540E\u7EED\u8F6E\u6B21\u590D\u7528\uFF09\uFF0C**\u76F4\u63A5\u7EE7\u7EED\u7528\u5F53\u524D\u5206\u652F\uFF0C\u4E0D\u8981\u65B0\u5EFA**\uFF08\u5728\u4ED3\u5E93\u76EE\u5F55\u5185\u6267\u884C\uFF09\uFF1A
5144
5165
  \`\`\`bash
@@ -5200,8 +5221,9 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5200
5221
  - **git / gh \u547D\u4EE4\u4E00\u5F8B\u5728\u5BF9\u5E94\u4ED3\u5E93\u76EE\u5F55\u5185\u6267\u884C**\uFF08\`cd <\u5DE5\u4F5C\u533A\u6839>/<\u4ED3\u5E93\u76EE\u5F55>\`\uFF0C\u6216 \`git -C <\u4ED3\u5E93\u76EE\u5F55> ...\`\uFF09\uFF1A
5201
5222
  \u5DE5\u4F5C\u533A\u6839\u4E0D\u662F git \u4ED3\u5E93\uFF0C\u5728\u6839\u76EE\u5F55\u8DD1 git \u547D\u4EE4\u5FC5\u7136\u5931\u8D25\u3002\`morula\` CLI \u53EF\u5728\u5DE5\u4F5C\u533A\u6839\u6216\u5176\u4EFB\u610F\u5B50\u76EE\u5F55\u6267\u884C\uFF08\u89C1 0.4\uFF09\u3002
5202
5223
  - **\u4E3B\u4ED3\u5E93**\uFF1A\u5E73\u53F0\u5DF2\u89E3\u6790\u51FA\u7684\u4ED3\u5E93\u5728**\u5DE5\u4F5C\u533A\u6839\u4E0B\u5C31\u7EEA**\uFF08\u76EE\u5F55\u540D = \u4ED3\u5E93\u5168\u540D\u628A \`/\` \u6362\u6210 \`-\`\uFF0C\u5982 \`human-resources/xxx\` \u2192 \`human-resources-xxx\`\uFF09\uFF1B\`ls\` \u5DE5\u4F5C\u533A\u6839\u5373\u53EF\u770B\u5230\uFF0C\u76F4\u63A5\u5728\u8BE5\u76EE\u5F55\u5185\u5EFA\u5206\u652F/\u63D0\u4EA4/\u63A8\u5206\u652F/\u63D0 PR\u3002**\u9700\u8981\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u54EA\u4E9B\u4ED3\u5E93\u65F6\u81EA\u5DF1\u67E5**\uFF1A\`morula repo list --space <spaceId>\`\uFF08spaceId \u89C1\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF09\u3002
5203
- - **\u26A0\uFE0F \`morula repo checkout\` \u7684\u8BED\u4E49 = \u300C\u58F0\u660E/\u5207\u6362\u672C\u4EFB\u52A1\u7684**\u4E3B\u4ED3\u5E93**\u300D**\uFF08\u670D\u52A1\u7AEF generation+1 + daemon \u7269\u5316\u5DE5\u4F5C\u533A\uFF09\uFF1A
5204
- **\u5B83\u51B3\u5B9A\u540E\u7EED\u5F00\u53D1\u9636\u6BB5\u5728\u54EA\u4E2A\u4ED3\u5E93\u5E72\u6D3B**\u3002\u56E0\u6B64\u53EA\u5728**\u5224\u5B9A\u51FA\u4E3B\u4ED3\u5E93\u4E4B\u540E**\u8C03\u7528\uFF0C**\u4E00\u6B21\u4EFB\u52A1\u53EA\u58F0\u660E\u4E00\u6B21**\uFF08\u5224\u9519\u8981\u7EA0\u6B63\u65F6\u624D\u518D\u8C03\u4E00\u6B21\uFF09\u3002
5224
+ - **\u26A0\uFE0F \`morula repo checkout\` \u7684\u8BED\u4E49 = \u300C\u58F0\u660E/\u5207\u6362\u672C\u4EFB\u52A1\u7684\u4E3B\u4ED3\u5E93\u300D**\uFF08\u670D\u52A1\u7AEF generation+1 + daemon \u7269\u5316\u5DE5\u4F5C\u533A\uFF09\uFF1A
5225
+ \u58F0\u660E\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u4EFB\u52A1\uFF08\u5F53\u524D\u9636\u6BB5\u7684\u6267\u884C\u8005\uFF09\uFF1B**\u5F00\u53D1\u9636\u6BB5\u4EFB\u52A1\u4E0D\u7EE7\u627F\u5206\u6790\u9636\u6BB5\u7684\u58F0\u660E**\u2014\u2014\u9700\u8981\u6539\u4EE3\u7801\u65F6\u7531\u4F60\u81EA\u5DF1
5226
+ \u6309\u4EFB\u52A1\u63CF\u8FF0/\u5206\u6790\u7ED3\u8BBA checkout\u3002\u56E0\u6B64\u53EA\u5728**\u5224\u5B9A\u51FA\u4E3B\u4ED3\u5E93\u4E4B\u540E**\u8C03\u7528\uFF0C**\u4E00\u6B21\u4EFB\u52A1\u53EA\u58F0\u660E\u4E00\u6B21**\uFF08\u5224\u9519\u8981\u7EA0\u6B63\u65F6\u624D\u518D\u8C03\u4E00\u6B21\uFF09\u3002
5205
5227
  \u4E0D\u8981\u7528\u5B83\u300C\u628A\u7A7A\u95F4\u91CC\u7684\u4ED3\u5E93\u9010\u4E2A\u901B\u4E00\u904D\u300D\u2014\u2014\u6BCF\u8C03\u4E00\u6B21\u4E3B\u4ED3\u5E93\u5C31\u8DDF\u7740\u6362\u4E00\u6B21\uFF08\u771F\u673A\u8E29\u5230\uFF1A\u5206\u6790\u9636\u6BB5\u4E3A\u67E5\u8BC1 checkout \u4E86 3 \u4E2A\u4ED3\u5E93\uFF0C
5206
5228
  \u4E3B\u4ED3\u5E93\u6700\u7EC8\u505C\u5728\u6700\u540E\u90A3\u4E2A\u300C\u53EA\u770B\u4E86\u4E00\u773C\u300D\u7684\u4ED3\u5E93\u4E0A\uFF09\u3002
5207
5229
  - **\u82E5\u4EFB\u52A1\u9700\u8981\u4E86\u89E3\u4ED3\u5E93\u5185\u5BB9/\u4EE3\u7801/\u63D0\u4EA4\u72B6\u6001\uFF08\u8BFB\u573A\u666F\uFF0C\u542B\u7EAF\u95EE\u7B54\uFF09**\uFF1A
@@ -5220,7 +5242,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5220
5242
  - **\u82E5\u4EFB\u52A1\u6D89\u53CA\u591A\u4E2A\u4ED3\u5E93\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\uFF09**\uFF1A\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\u7531**\u4F60**\u7ED3\u5408\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\uFF08\u4EFB\u52A1\u63CF\u8FF0\u91CC\u7684\u300C\u6D89\u53CA\u4ED3\u5E93\u300D\u662F\u7BA1\u5BB6\u7684\u521D\u59CB\u5224\u65AD\uFF1B\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u54EA\u4E9B\u4ED3\u5E93\u7528 \`morula repo list --space <spaceId>\` \u67E5\uFF1B\u6267\u884C\u4E2D\u53D1\u73B0\u8FD8\u9700\u6539\u52A8\u5176\u4ED6\u4ED3\u5E93\u65F6\uFF0C\u81EA\u884C prepare \u62C9\u53D6\uFF09\uFF0C**\u4E0D\u9700\u8981\u5411\u7528\u6237\u9010\u4E2A\u786E\u8BA4**\uFF1A
5221
5243
  1. \u4E3B\u4ED3\u5E93\uFF1A\u5728\u5B83\u7684\u5B50\u76EE\u5F55\u5185\u76F4\u63A5 git \u64CD\u4F5C\uFF08\u5DE5\u4F5C\u533A\u6839\u4E0D\u662F\u4ED3\u5E93\uFF09\uFF1B
5222
5244
  2. \u5176\u4ED6\u4ED3\u5E93\u7528 \`morula repo prepare <taskId> <repoFullName>\` \u7269\u5316\u5230\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\u4E0B\uFF08**\u4E0E\u4E3B\u4ED3\u5E93\u540C\u7EA7\u7684\u5B50\u76EE\u5F55**\uFF0C\u672C\u5730 daemon \u7AEF\u70B9\uFF0C\u672C\u8F6E\u7ACB\u5373\u7269\u5316\u5E76\u8FD4\u56DE\u8DEF\u5F84\uFF1Bgit \u534F\u8BAE\u62C9\u53D6\uFF0C\u4E0D\u8D70\u5E73\u53F0 API\uFF09\uFF1B
5223
- 3. \u5728\u54EA\u4E2A\u4ED3\u5E93\u6539\u52A8\uFF0C\u5C31\u5728\u54EA\u4E2A\u4ED3\u5E93\u5185\u5EFA\u5206\u652F\uFF08\u7B2C 0.3 \u6761\u547D\u540D\u89C4\u8303\uFF09\u3001\u63A8\u5206\u652F\u5E76\u5EFA PR/MR\uFF08title/description \u5E26\u4EFB\u52A1\u7F16\u53F7\uFF09\uFF1B\u6BCF\u4E2A\u4ED3\u5E93\u7684 PR \u670D\u52A1\u7AEF\u90FD\u4F1A\u81EA\u52A8\u5173\u8054\u5230\u672C\u4EFB\u52A1\u3002**\u5206\u652F\u540D\u9010\u4ED3\u4E0A\u62A5**\uFF1A\u4E3B\u4ED3\u5E93\uFF08\u5176\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D>\`\uFF1B\u6B21\u4ED3\u5E93\uFF08\u540C\u7EA7\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D> --repo <owner/name>\` \u2014\u2014 \u670D\u52A1\u7AEF\u636E\u6B64\u628A\u5206\u652F\u540D\u5199\u5230\u8BE5\u4ED3\u5E93\u7684\u5DE5\u4F5C\u4E0A\u4E0B\u6587\uFF08\u5E73\u53F0\u4FA7\u9010\u4ED3\u5EFA MR \u515C\u5E95\u3001\u4EA4\u4ED8\u6E05\u5355\u90FD\u4F9D\u8D56\u5B83\uFF1B\u4E0D\u62A5\u5219\u4E00\u76F4\u505C\u5728\u5360\u4F4D\u5206\u652F\u540D \`morula/tmp-*\`\uFF0C\u5E73\u53F0\u627E\u4E0D\u5230\u4F60\u7684\u5206\u652F\uFF09\uFF1B
5245
+ 3. \u5728\u54EA\u4E2A\u4ED3\u5E93\u6539\u52A8\uFF0C\u5C31\u5728\u54EA\u4E2A\u4ED3\u5E93\u5185\u5EFA\u5206\u652F\uFF08\u7B2C 0.3 \u6761\u547D\u540D\u89C4\u8303\uFF09\u3001\u63A8\u5206\u652F\u5E76**\u7ECF\u5E73\u53F0\u901A\u9053\u5EFA PR/MR**\uFF08\`morula repo mr create\` \u4F18\u5148\uFF1B\u6807\u9898\u5E26\u4EFB\u52A1\u7F16\u53F7\u4EC5\u4E3A\u53EF\u8BFB\u6027\uFF09\u3002**\u53EA\u6709\u7ECF\u5E73\u53F0\u901A\u9053\u5EFA\u7684 PR \u624D\u4F1A\u5173\u8054\u5230\u672C\u4EFB\u52A1**\uFF08\u670D\u52A1\u7AEF\u4E0D\u8BFB PR \u6587\u672C\uFF0C\u89C1\u7B2C 2 \u6761\uFF09\u3002**\u5206\u652F\u540D\u9010\u4ED3\u4E0A\u62A5**\uFF1A\u4E3B\u4ED3\u5E93\uFF08\u5176\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D>\`\uFF1B\u6B21\u4ED3\u5E93\uFF08\u540C\u7EA7\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D> --repo <owner/name>\` \u2014\u2014 \u670D\u52A1\u7AEF\u636E\u6B64\u628A\u5206\u652F\u540D\u5199\u5230\u8BE5\u4ED3\u5E93\u7684\u5DE5\u4F5C\u4E0A\u4E0B\u6587\uFF08\u5E73\u53F0\u4FA7\u9010\u4ED3\u5EFA MR \u515C\u5E95\u3001\u4EA4\u4ED8\u6E05\u5355\u90FD\u4F9D\u8D56\u5B83\uFF1B\u4E0D\u62A5\u5219\u4E00\u76F4\u505C\u5728\u5360\u4F4D\u5206\u652F\u540D \`morula/tmp-*\`\uFF0C\u5E73\u53F0\u627E\u4E0D\u5230\u4F60\u7684\u5206\u652F\uFF09\uFF1B
5224
5246
  4. prepare \u76EE\u5F55\u4E8C\u6B21\u8C03\u7528\u4F1A fetch \u66F4\u65B0\u4F46**\u4E0D\u52A8\u5DE5\u4F5C\u533A**\u2014\u2014\u8DE8\u8F6E\u590D\u7528\u65F6\u5148 \`git status\` \u786E\u8BA4\u73B0\u573A\u3002
5225
5247
  - **\u5B89\u5168\u8FB9\u754C**\uFF1A\u53EA\u5728\u672C\u4EFB\u52A1\u7684\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\uFF08\u53CA\u5176\u4ED3\u5E93\u5B50\u76EE\u5F55\uFF09\u5185\u64CD\u4F5C\uFF1B\u7981\u6B62\u63A2\u7D22/\u4FEE\u6539\u5DE5\u4F5C\u533A\u6839\u4E4B\u5916\u7684\u8DEF\u5F84\uFF08\u5C24\u5176 daemon \u6839\u76EE\u5F55\u4E0B\u5176\u4ED6\u4EFB\u52A1\u7684 \`repos/\`\u3001\`worktrees/\`\u3001\`scratch/\`\uFF09\u3002
5226
5248
  - **\u672C\u5730\u9A8C\u8BC1\u670D\u52A1\u7AEF\u53E3\u9694\u79BB**\uFF08issues/local-backend-watch-instability.md\uFF09\uFF1A\u82E5\u4EFB\u52A1\u9700\u8981\u542F\u52A8\u672C\u5730\u670D\u52A1\u9A8C\u8BC1\uFF08\u5982 \`npm run dev\` / \`nest start\` \u7B49\uFF09\uFF0C**\u5FC5\u987B\u4F7F\u7528\u975E\u5E73\u53F0\u5360\u7528\u7AEF\u53E3**\u2014\u2014\u7ED1\u5B9A \`127.0.0.1\` \u7684\u9AD8\u4F4D\u968F\u673A\u7AEF\u53E3\uFF08\u5982 127.0.0.1:18080+\uFF09\uFF0C**\u7981\u6B62**\u4F7F\u7528 \`8080\`\uFF08\u5E73\u53F0\u540E\u7AEF\uFF09\u53CA \`15173\`\uFF08\u5E73\u53F0\u524D\u7AEF\uFF09\u7B49\u5E73\u53F0\u5360\u7528\u7AEF\u53E3\uFF0C\u4E5F\u4E0D\u8981\u4F9D\u8D56\u7EC8\u7AEF\u4F1A\u8BDD\u5B58\u6D3B\uFF08\u540E\u53F0\u8FDB\u7A0B\u65B9\u5F0F\u542F\u52A8\uFF0C\u7528 \`nohup\`/\`disown\` \u6216 \`&!\` \u8131\u79BB\uFF09\uFF1B\u542F\u52A8\u540E\u81EA\u6D4B\u53EF\u7528\u5373\u53EF\uFF0C\u4E0D\u9700\u8981\uFF08\u4E5F\u4E0D\u5E94\uFF09\u8BA9\u5E73\u53F0\u4FA7\u8BBF\u95EE\u3002\u4E91\u7AEF\u6C99\u7BB1\u5316\u540E\u7531\u6C99\u7BB1\u7F51\u7EDC\u7B56\u7565\u515C\u5E95\u3002
@@ -5320,13 +5342,15 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
5320
5342
 
5321
5343
  > \u4E3A\u4EC0\u4E48\u662F\u8FD9\u56DB\u4E2A\u65F6\u673A\uFF1A\u68C0\u67E5\u662F**\u5DE5\u5177\u8C03\u7528**\uFF0C\u6709 token \u4E0E\u5EF6\u8FDF\u5F00\u9500\uFF1B\u300C\u65E0\u6D88\u606F\u7ACB\u5373\u7EE7\u7EED\u300D\u662F\u4E3A\u4E86\u4E0D\u8BA9\u68C0\u67E5\u70B9\u628A\u4E3B\u7EBF\u5E26\u504F\u3002
5322
5344
 
5323
- ## 2. PR \u94FE\u63A5\u4E0E\u5173\u5355\u610F\u56FE\u662F\u4E24\u5957\u4E0D\u540C\u7684\u5951\u7EA6
5345
+ ## 2. PR \u5173\u8054\uFF1A\u53EA\u8D70\u5E73\u53F0\u901A\u9053
5324
5346
 
5325
- - **\u94FE\u63A5\u901A\u9053**\uFF1APR \u7684 title / body \u4E2D\u51FA\u73B0\u4EFB\u52A1\u7F16\u53F7 \u2192 \u670D\u52A1\u7AEF\u81EA\u52A8\u5173\u8054 PR \u5230\u4EFB\u52A1\u3002
5326
- \u63A8\u8350\u5199\u6CD5\uFF1Atitle \u4EE5\u300CSTR-101: \u63CF\u8FF0\u300D\u5F00\u5934\uFF0C\u6216 body \u5199\u300CCloses STR-101\u300D\uFF08\u5206\u652F\u540D\u4E0D\u542B\u4EFB\u52A1\u7F16\u53F7\uFF0C\u89C1 0.3\uFF1B\u5E73\u53F0\u901A\u9053\u5EFA\u7684 MR \u4F1A\u81EA\u52A8\u628A\u7F16\u53F7\u5199\u8FDB title/body\uFF09\u3002
5327
- - **\u5173\u5355\u901A\u9053**\uFF1A\u53EA\u6709 \`Closes\` / \`Fixes\` / \`Resolves\` \u540E**\u7D27\u90BB**\u4EFB\u52A1\u7F16\u53F7\uFF08\u5982\u300CCloses STR-101\u300D\uFF09\u2192 \u8BE5 PR
5328
- merge \u65F6\u670D\u52A1\u7AEF\u81EA\u52A8\u628A\u4EFB\u52A1\u6D41\u8F6C\u5230\u5B8C\u6210\u72B6\u6001\u3002title \u524D\u7F00\u6216 branch \u5F15\u7528\u53EA\u4F1A\u5173\u8054 PR\uFF0C\u4E0D\u4F1A\u5728 merge \u65F6\u5173\u5355\u3002
5329
- - body \u4E2D\u677E\u6563\u7684\u63D0\u53CA\uFF08\u5982\u300CRelated to STR-101\u300D\uFF09\u4F1A\u88AB\u6807\u8BB0\u4E3A\u7EAF\u5F15\u7528\uFF0C\u4E0D\u5C55\u793A\u5728\u4EFB\u52A1\u7684 PR \u5217\u8868\u4E2D\u3002
5347
+ - **\u5173\u8054\u7684\u552F\u4E00\u6765\u6E90\u662F\u5E73\u53F0\u901A\u9053**\uFF082026-09-24 \u5B9A\u6848\uFF09\uFF1A\`morula repo mr create\`\uFF08\u63A8\u8350\uFF09\u5EFA PR \u65F6\u670D\u52A1\u7AEF\u5F53\u573A\u843D\u5173\u8054\uFF1B
5348
+ \u6216 \`gh pr create\` \u4E4B\u540E\u8C03 \`report_pr\` \u5DE5\u5177\u4E0A\u62A5\u3002**\u670D\u52A1\u7AEF\u4E0D\u8BFB PR \u6587\u672C**\u2014\u2014title/branch/body \u91CC\u5199\u4EC0\u4E48\u4EFB\u52A1\u7F16\u53F7\u90FD\u4E0D\u4F1A\u5173\u8054\u3002
5349
+ - **\u4EBA\u5DE5\u5728 GitLab/GitHub \u4E0A\u624B\u5DE5\u5EFA\u7684 PR\uFF0C\u5E73\u53F0\u4E0D\u5173\u8054**\uFF1A\u4E0D\u8FDB\u4EFB\u52A1\u5173\u8054\u533A\u3001\u4E0D\u89E6\u53D1\u81EA\u52A8\u8BC4\u5BA1\u3001\u4E0D\u9A71\u52A8\u4EFB\u52A1\u72B6\u6001\u3002
5350
+ \u5982\u679C\u4F60\u5E0C\u671B\u4E00\u6761 PR \u5173\u8054\u5230\u672C\u4EFB\u52A1\uFF0C\u5C31\u5FC5\u987B\u8D70\u4E0A\u9762\u4E24\u6761\u5E73\u53F0\u901A\u9053\u4E4B\u4E00\u3002
5351
+ - **\u5173\u5355\u4E0D\u9700\u8981\u5173\u952E\u5B57**\uFF1A\u5173\u8054\u4E0A\u7684 PR \u5408\u5E76\u540E\uFF0C\u670D\u52A1\u7AEF\u6309\u65E2\u6709\u6D41\u7A0B\u63A8\u8FDB\u4EFB\u52A1\u72B6\u6001
5352
+ \uFF08\u5168\u90E8 MR \u5408\u5E76 \u2192 \u5DF2\u5408\u5E76\uFF1B\u8FD9\u4E9B\u63D0\u4EA4\u8FDB\u5165\u751F\u4EA7\u5206\u652F \u2192 \u5DF2\u53D1\u5E03\uFF09\u3002**\u5173\u8054\u672C\u8EAB\u5C31\u662F\u6388\u6743**\u3002
5353
+ - \u63A8\u8350\uFF08\u53EF\u8BFB\u6027\uFF0C\u4E0D\u662F\u5173\u8054\u624B\u6BB5\uFF09\uFF1Atitle \u4EE5\u300CSTR-101: \u63CF\u8FF0\u300D\u5F00\u5934\u3001body \u5199\u4E00\u53E5 \`Closes STR-101\`\u2014\u2014\u4FBF\u4E8E\u4EBA\u548C\u5E73\u53F0\u65E5\u5FD7\u5BF9\u7167\u3002
5330
5354
 
5331
5355
  ## 3. \u4EC0\u4E48\u65F6\u5019\u63A8\u5206\u652F\u3001\u63D0 PR\uFF08\u51B3\u7B56\u6811\uFF09
5332
5356
 
@@ -5469,10 +5493,10 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
5469
5493
 
5470
5494
  | \u5951\u7EA6 | \u670D\u52A1\u7AEF\u5B9E\u73B0 | \u4F4D\u7F6E |
5471
5495
  |------|-----------|------|
5472
- | \u4EFB\u52A1 key \u5339\u914D\uFF08\`SPW299-30\`\uFF09 | \`VcsLinkService.TASK_KEY_RE\`\uFF08\u626B\u63CF title/branch/body\uFF1B\u6B63\u5219 \`[A-Z][A-Z0-9]{1,9}-\u6570\u5B57\`\uFF0C\u5927\u5C0F\u5199\u654F\u611F \u2192 **MR title/body \u4E2D\u7684\u7F16\u53F7\u987B\u539F\u6837\u5927\u5199**\uFF1B\u5206\u652F\u540D\u5DF2\u4E0D\u542B\u7F16\u53F7\uFF08\u5951\u7EA6 0.3 \u5B9A\u6848\uFF09\uFF0C\u7F16\u53F7\u7531\u5E73\u53F0\u901A\u9053\u81EA\u52A8\u5199\u5165 title/body\uFF09 | \`src/modules/vcs-integration/link/vcs-link.service.ts:30,435\` |
5473
- | Close intent \u5355\u72EC\u626B\u63CF\uFF08title/body\uFF0C\`Closes|Fixes|Resolves\` \u7D27\u8DDF key\uFF09 | \`CLOSE_INTENT_RE\` | \`vcs-link.service.ts:29\` |
5474
- | reference-only \u9690\u85CF\uFF08\u677E\u6563 body \u63D0\u53CA\u4E0D\u5165 PR \u5217\u8868\uFF09 | \`resolveTargets\` \u4E0E closeIntent \u5206\u652F | \`vcs-link.service.ts:60-80, 184-186\` |
5475
- | merge \u5173\u5355\u5E42\u7B49 | merged \u2192 closeIntent \u94FE\u63A5\u5BF9\u5E94\u4EFB\u52A1 | \`vcs-link.service.ts:19-20\`\uFF08\u6587\u4EF6\u5934\u6CE8\u91CA\uFF09 |
5496
+ | PR \u5173\u8054**\u53EA\u7531\u5E73\u53F0\u901A\u9053\u4EA7\u751F**\uFF08\u670D\u52A1\u7AEF\u4E0D\u8BFB PR \u6587\u672C\uFF09 | \u5E73\u53F0\u901A\u9053\u5EFA\u94FE\uFF08\`reportPrLinkByAgent\`\uFF09+ webhook \u6309 \`(\u79DF\u6237, \u4ED3\u5E93, PR \u53F7)\` \u5B9A\u4F4D**\u5DF2\u5B58\u5728**\u7684\u94FE\u63A5 \u2192 \u53EA\u66F4\u65B0\u72B6\u6001 | \`src/modules/vcs-integration/link/vcs-link.service.ts\`\uFF08\`findLinkedRows\` / \`handlePullRequest\`\uFF09 |
5497
+ | \u672A\u5173\u8054\u7684 PR\uFF08\u4EBA\u5DE5\u624B\u5DE5\u5EFA\uFF09\u2192 \u4E0D\u5EFA\u94FE\u3001\u4E0D\u6D41\u8F6C\u3001\u7559\u75D5 | \u540C\u4E0A\uFF08\u5B9A\u4F4D\u4E0D\u5230\u5373\u8FD4\u56DE\uFF0C\u5199\u300C\u672A\u5173\u8054\u4EFB\u4F55\u4EFB\u52A1\u300D\u65E5\u5FD7\uFF09 | \u540C\u4E0A |
5498
+ | merge \u5173\u5355\uFF1A**\u94FE\u63A5\u5B58\u5728\u5373\u6388\u6743**\uFF08\u65E0\u9700 \`Closes\` \u5173\u952E\u5B57\uFF09 | merged/closed \u2192 \u591A\u4ED3\u5E93\u6C47\u603B \u2192 \`closeObject\` | \`vcs-link.service.ts\`\uFF08\`closeObject\`\uFF09 |
5499
+ | \u7EC8\u6001\u4E0D\u53EF\u88AB\u964D\u7EA7\uFF08\`released\` \u4E0D\u88AB \`closed\`/\`merged\` \u8986\u76D6\uFF1B\`merged \u2192 released\` \u653E\u884C\uFF09 | \`isTerminalDowngrade\`\uFF08\u9886\u57DF\u5C42\u552F\u4E00\u5B9A\u4E49\uFF09+ \`ObjectService.update\` \u4E0D\u53D8\u91CF | \`domain/lifecycle-state.ts\`\u3001\`object.service.ts\` |
5476
5500
 
5477
5501
  ## 2. \u4E0E\u5E73\u53F0\u4EA4\u4E92\u53EA\u80FD\u8D70 morula CLI
5478
5502
 
@@ -5502,7 +5526,7 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
5502
5526
 
5503
5527
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
5504
5528
  |------|------|------|
5505
- | merge \u81EA\u52A8\u5173\u5355\uFF08\u670D\u52A1\u7AEF\u9A71\u52A8\u72B6\u6001\uFF09 | webhook merged \u2192 closeIntent \u2192 Object \u5B8C\u6210 | \`src/modules/vcs-integration/webhook/vcs-event.service.ts\` |
5529
+ | merge \u81EA\u52A8\u5173\u5355\uFF08\u670D\u52A1\u7AEF\u9A71\u52A8\u72B6\u6001\uFF09 | webhook merged \u2192 \u6309\u5E73\u53F0\u4E8B\u5B9E\u5B9A\u4F4D\u5DF2\u5173\u8054\u94FE\u63A5 \u2192 \u6C47\u603B \u2192 Object \u5B8C\u6210 | \`src/modules/vcs-integration/webhook/vcs-event.service.ts\`\u3001\`link/vcs-link.service.ts\` |
5506
5530
  | agent \u4E0D\u8C03 CLI \u6539\u72B6\u6001 | \u534F\u4F5C\u5951\u7EA6\u7B2C 4 \u6761\u3002**\u6CE8\u610F\uFF1ACLI \u6709 \`morula task state <id> --state <code>\`**\uFF08\u673A\u5668\u534F\u8BAE \`POST /agent-tasks/:id/state\`\uFF0C\u4F9B\u5E73\u53F0/\u81EA\u6D41\u8F6C\u4F7F\u7528\uFF09\u2014\u2014\u662F**\u5951\u7EA6\u8981\u6C42 agent \u4E0D\u8C03\u7528**\u5B83\uFF0C\u4E0D\u662F\u300CCLI \u6CA1\u6709\u8FD9\u4E2A\u547D\u4EE4\u300D | \`packages/morula-cli/src/commands/task.ts\`\uFF08action \`state\`\uFF09\u3001\`src/modules/agent-task/agent-task.controller.ts\`\uFF08\u7AEF\u70B9\uFF09 |
5507
5531
 
5508
5532
  ## 4.1 \u8BC4\u5BA1\u4EFB\u52A1\u7ED3\u8BBA\u56DE\u4F20\uFF08morula review submit\uFF09
@@ -5771,7 +5795,7 @@ function outboxAge(entry) {
5771
5795
  // src/daemon/plugin-register.ts
5772
5796
  import { chmodSync as chmodSync3, cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, rmSync as rmSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
5773
5797
  import { homedir as homedir10 } from "node:os";
5774
- import { dirname as dirname7, join as join13 } from "node:path";
5798
+ import { dirname as dirname8, join as join13 } from "node:path";
5775
5799
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5776
5800
  var PLUGIN_NAME = "morula-cli";
5777
5801
  var MANIFEST_DIR_NAME = ".codex-plugin";
@@ -5786,9 +5810,9 @@ function defaultPluginsRoot(env = process.env, home = homedir10()) {
5786
5810
  return join13(resolveDimcodeHome(env, home), "plugins");
5787
5811
  }
5788
5812
  function resolvePackagePaths(entryUrl) {
5789
- const distDir = dirname7(fileURLToPath2(entryUrl));
5813
+ const distDir = dirname8(fileURLToPath2(entryUrl));
5790
5814
  return {
5791
- packageRoot: dirname7(distDir),
5815
+ packageRoot: dirname8(distDir),
5792
5816
  distDir,
5793
5817
  assetsDir: join13(distDir, "plugin")
5794
5818
  };
@@ -5804,7 +5828,25 @@ if [ -z "$NODE" ]; then
5804
5828
  echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
5805
5829
  exit 127
5806
5830
  fi
5807
- exec "$NODE" ${JSON.stringify(entry)} "$@"
5831
+ ENTRY=${JSON.stringify(entry)}
5832
+ # \u81EA\u6108\u56DE\u9000\uFF1A\u70D8\u7119\u7684\u5B89\u88C5\u76EE\u5F55\u88AB\u6E05\u7406/\u632A\u52A8\u65F6\uFF0C\u6309 daemon \u6CE8\u5165\u7684\u540C\u6E90\u5165\u53E3\u6216\u5F53\u524D morula-runtime \u91CD\u65B0\u5B9A\u4F4D
5833
+ if [ ! -f "$ENTRY" ] && [ -n "$MORULA_CLI" ] && [ -f "$MORULA_CLI" ]; then
5834
+ ENTRY="$MORULA_CLI"
5835
+ fi
5836
+ if [ ! -f "$ENTRY" ]; then
5837
+ RT="$(command -v morula-runtime 2>/dev/null || true)"
5838
+ if [ -n "$RT" ]; then
5839
+ ALT="$("$NODE" -e 'const{realpathSync}=require("fs");const{dirname,join}=require("path");try{const p=realpathSync(process.argv[1]);process.stdout.write(join(dirname(dirname(p)),"dist","morula.js"))}catch(e){}' "$RT" 2>/dev/null || true)"
5840
+ if [ -n "$ALT" ] && [ -f "$ALT" ]; then
5841
+ ENTRY="$ALT"
5842
+ fi
5843
+ fi
5844
+ fi
5845
+ if [ ! -f "$ENTRY" ]; then
5846
+ echo '{"code":"ERROR","message":"morula.js not found: the install dir was removed or moved; fix: morula-runtime setup (or npm i -g @arcships/morula-runtime)"}' >&2
5847
+ exit 127
5848
+ fi
5849
+ exec "$NODE" "$ENTRY" "$@"
5808
5850
  `;
5809
5851
  }
5810
5852
  function assetsReady(paths) {
@@ -6061,7 +6103,7 @@ function isAcpInitRetryableError(error) {
6061
6103
  async function withAcpInitRetry(setupOnce, deps = {}) {
6062
6104
  const delays = deps.delaysMs ?? ACP_INIT_RETRY_DELAYS_MS;
6063
6105
  const isRetryable = deps.isRetryable ?? isAcpInitRetryableError;
6064
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6106
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6065
6107
  const maxAttempts = delays.length + 1;
6066
6108
  let lastError;
6067
6109
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -6076,7 +6118,7 @@ async function withAcpInitRetry(setupOnce, deps = {}) {
6076
6118
  error: lastError instanceof Error ? lastError.message : String(lastError ?? "")
6077
6119
  });
6078
6120
  if (delayMs > 0)
6079
- await sleep2(delayMs);
6121
+ await sleep(delayMs);
6080
6122
  if (deps.isAborted?.())
6081
6123
  throw new Error("\u4EFB\u52A1\u88AB\u53D6\u6D88");
6082
6124
  }
@@ -6828,8 +6870,8 @@ var DimAgentAdapter = class {
6828
6870
  }
6829
6871
  }
6830
6872
  async probe() {
6831
- const { existsSync: existsSync19 } = await import("node:fs");
6832
- const installed = existsSync19(this.dimPath) || this.dimPath === "dim";
6873
+ const { existsSync: existsSync21 } = await import("node:fs");
6874
+ const installed = existsSync21(this.dimPath) || this.dimPath === "dim";
6833
6875
  let authStatus = installed ? "unknown" : "error";
6834
6876
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
6835
6877
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -7122,7 +7164,7 @@ function pickLegacyDimModels(inventory) {
7122
7164
  // src/daemon/repo-cache.ts
7123
7165
  import { execFileSync as execFileSync8 } from "node:child_process";
7124
7166
  import { existsSync as existsSync12, mkdirSync as mkdirSync10 } from "node:fs";
7125
- import { dirname as dirname8, join as join15 } from "node:path";
7167
+ import { dirname as dirname9, join as join15 } from "node:path";
7126
7168
  function ownerOf(repoFullName) {
7127
7169
  const idx = repoFullName.indexOf("/");
7128
7170
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
@@ -7157,7 +7199,7 @@ function openRepoCache(workdir, tenantId, runtimeId, repoFullName, env, agentRem
7157
7199
  syncRemoteUrl(repoCachePath, originUrl(), env);
7158
7200
  return;
7159
7201
  }
7160
- mkdirSync10(dirname8(repoCachePath), { recursive: true });
7202
+ mkdirSync10(dirname9(repoCachePath), { recursive: true });
7161
7203
  const cloneUrl = fetchUrl();
7162
7204
  try {
7163
7205
  execFileSync8("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
@@ -8275,11 +8317,333 @@ function materializeAdditionalRepo(input) {
8275
8317
  return input.targetPath;
8276
8318
  }
8277
8319
 
8320
+ // src/daemon/self-upgrade.ts
8321
+ import { spawn as spawn4 } from "node:child_process";
8322
+ import { closeSync as closeSync4, existsSync as existsSync16, mkdirSync as mkdirSync13, openSync as openSync4, writeFileSync as writeFileSync11 } from "node:fs";
8323
+ import { dirname as dirname10, join as join19 } from "node:path";
8324
+ var RUNTIME_PACKAGE = "@arcships/morula-runtime";
8325
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
8326
+ var REGISTRY_TIMEOUT_MS = 1e4;
8327
+ var NPM_INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
8328
+ var NPM_ROLLBACK_TIMEOUT_MS = 10 * 60 * 1e3;
8329
+ var RESTART_LOCK_WAIT_MS = 6e4;
8330
+ var RESTART_LIVENESS_CHECK_MS = 5e3;
8331
+ function resolveRegistry(env = process.env) {
8332
+ return (env.MORULA_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY).replace(/\/+$/, "");
8333
+ }
8334
+ function parseVersion(input) {
8335
+ if (typeof input !== "string")
8336
+ return null;
8337
+ const match = input.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
8338
+ if (!match)
8339
+ return null;
8340
+ const prerelease = match[4] ? match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part) : [];
8341
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease };
8342
+ }
8343
+ function compareVersions(a, b) {
8344
+ const pa = parseVersion(a);
8345
+ const pb = parseVersion(b);
8346
+ if (!pa || !pb)
8347
+ return a === b ? 0 : a < b ? -1 : 1;
8348
+ for (const key of ["major", "minor", "patch"]) {
8349
+ if (pa[key] !== pb[key])
8350
+ return pa[key] < pb[key] ? -1 : 1;
8351
+ }
8352
+ if (pa.prerelease.length === 0 && pb.prerelease.length === 0)
8353
+ return 0;
8354
+ if (pa.prerelease.length === 0)
8355
+ return 1;
8356
+ if (pb.prerelease.length === 0)
8357
+ return -1;
8358
+ for (let i = 0; i < Math.max(pa.prerelease.length, pb.prerelease.length); i++) {
8359
+ const sa = pa.prerelease[i];
8360
+ const sb = pb.prerelease[i];
8361
+ if (sa === void 0)
8362
+ return -1;
8363
+ if (sb === void 0)
8364
+ return 1;
8365
+ if (sa === sb)
8366
+ continue;
8367
+ if (typeof sa === "number" && typeof sb === "number")
8368
+ return sa < sb ? -1 : 1;
8369
+ if (typeof sa === "number")
8370
+ return -1;
8371
+ if (typeof sb === "number")
8372
+ return 1;
8373
+ return sa < sb ? -1 : 1;
8374
+ }
8375
+ return 0;
8376
+ }
8377
+ function isUpgradeAvailable(current, latest) {
8378
+ if (!parseVersion(current) || !parseVersion(latest))
8379
+ return false;
8380
+ return compareVersions(current, latest) < 0;
8381
+ }
8382
+ function run2(command, args, opts) {
8383
+ return new Promise((resolve2) => {
8384
+ let child;
8385
+ try {
8386
+ child = spawn4(command, args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
8387
+ } catch (error) {
8388
+ resolve2({ ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) });
8389
+ return;
8390
+ }
8391
+ let stdout = "";
8392
+ let stderr = "";
8393
+ let settled = false;
8394
+ const timer = setTimeout(() => {
8395
+ if (!settled) {
8396
+ settled = true;
8397
+ try {
8398
+ child.kill("SIGKILL");
8399
+ } catch {
8400
+ }
8401
+ resolve2({ ok: false, code: null, stdout, stderr, error: `timeout after ${Math.round(opts.timeoutMs / 1e3)}s` });
8402
+ }
8403
+ }, opts.timeoutMs);
8404
+ timer.unref?.();
8405
+ child.stdout?.on("data", (d) => {
8406
+ stdout += d.toString();
8407
+ });
8408
+ child.stderr?.on("data", (d) => {
8409
+ stderr += d.toString();
8410
+ });
8411
+ child.on("error", (error) => {
8412
+ if (settled)
8413
+ return;
8414
+ settled = true;
8415
+ clearTimeout(timer);
8416
+ resolve2({ ok: false, code: null, stdout, stderr, error: error.message });
8417
+ });
8418
+ child.on("close", (code) => {
8419
+ if (settled)
8420
+ return;
8421
+ settled = true;
8422
+ clearTimeout(timer);
8423
+ resolve2({ ok: code === 0, code, stdout, stderr });
8424
+ });
8425
+ });
8426
+ }
8427
+ async function resolveNpmCommand() {
8428
+ const nodeDir = dirname10(process.execPath);
8429
+ const candidates = [
8430
+ { command: "npm", args: [] },
8431
+ { command: join19(nodeDir, "npm"), args: [] },
8432
+ { command: process.execPath, args: [join19(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")] }
8433
+ ];
8434
+ for (const candidate of candidates) {
8435
+ const res = await run2(candidate.command, [...candidate.args, "--version"], { timeoutMs: 15e3 });
8436
+ if (res.ok && /^\d+\.\d+\.\d+/.test(res.stdout.trim()))
8437
+ return candidate;
8438
+ }
8439
+ return null;
8440
+ }
8441
+ async function resolveNpmGlobalLayout() {
8442
+ const npm = await resolveNpmCommand();
8443
+ if (!npm)
8444
+ throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 npm\uFF08\u5347\u7EA7\u9700\u8981 npm install -g\uFF1B\u8BF7\u786E\u8BA4 node/npm \u5B89\u88C5\u5B8C\u6574\uFF09");
8445
+ const prefixRes = await run2(npm.command, [...npm.args, "prefix", "-g"], { timeoutMs: 3e4 });
8446
+ const rootRes = await run2(npm.command, [...npm.args, "root", "-g"], { timeoutMs: 3e4 });
8447
+ const prefix = prefixRes.ok ? prefixRes.stdout.trim() : "";
8448
+ const nodeModulesRoot = rootRes.ok ? rootRes.stdout.trim() : "";
8449
+ if (!prefix || !nodeModulesRoot)
8450
+ throw new Error(`\u89E3\u6790 npm \u5168\u5C40\u5E03\u5C40\u5931\u8D25\uFF08prefix=${prefix || "?"} root=${nodeModulesRoot || "?"}\uFF09`);
8451
+ return {
8452
+ prefix,
8453
+ nodeModulesRoot,
8454
+ runtimePackageRoot: join19(nodeModulesRoot, RUNTIME_PACKAGE),
8455
+ runtimeBinPath: process.platform === "win32" ? join19(prefix, `${RUNTIME_PACKAGE.split("/")[1] ?? "morula-runtime"}.cmd`) : join19(prefix, "bin", "morula-runtime")
8456
+ };
8457
+ }
8458
+ async function fetchLatestRuntimeVersion(opts = {}) {
8459
+ const registry2 = opts.registry ?? resolveRegistry();
8460
+ const url = `${registry2}/${encodeURIComponent(RUNTIME_PACKAGE)}/latest`;
8461
+ const controller = new AbortController();
8462
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? REGISTRY_TIMEOUT_MS);
8463
+ timer.unref?.();
8464
+ try {
8465
+ const res = await fetch(url, { signal: controller.signal, headers: { Accept: "application/json" } });
8466
+ if (!res.ok)
8467
+ throw new Error(`registry ${res.status}`);
8468
+ const body = await res.json();
8469
+ const version = typeof body.version === "string" ? body.version.trim() : "";
8470
+ if (!version)
8471
+ throw new Error("registry \u54CD\u5E94\u7F3A\u5C11 version");
8472
+ return version;
8473
+ } catch (error) {
8474
+ const message = error instanceof Error ? error.message : String(error);
8475
+ throw new Error(`\u67E5\u8BE2 npm registry \u5931\u8D25\uFF08${url}\uFF09: ${message}`);
8476
+ } finally {
8477
+ clearTimeout(timer);
8478
+ }
8479
+ }
8480
+ async function npmContext(registry2) {
8481
+ const npm = await resolveNpmCommand();
8482
+ if (!npm)
8483
+ throw new Error("\u672A\u627E\u5230\u53EF\u7528\u7684 npm\uFF08\u5347\u7EA7\u9700\u8981 npm install -g\uFF1B\u8BF7\u786E\u8BA4 node/npm \u5B89\u88C5\u5B8C\u6574\uFF09");
8484
+ const layout = await resolveNpmGlobalLayout();
8485
+ return { npm, layout };
8486
+ }
8487
+ async function npmInstallGlobal(ctx, version, registry2, timeoutMs) {
8488
+ const args = [...ctx.npm.args, "install", "-g", `${RUNTIME_PACKAGE}@${version}`];
8489
+ if (registry2 && registry2 !== DEFAULT_NPM_REGISTRY)
8490
+ args.push("--registry", registry2);
8491
+ return run2(ctx.npm.command, args, { timeoutMs });
8492
+ }
8493
+ async function verifyRuntimeInstall(binPath, expected) {
8494
+ if (!existsSync16(binPath))
8495
+ return { ok: false, version: null, reason: `bin \u4E0D\u5B58\u5728\uFF1A${binPath}` };
8496
+ const res = await run2(binPath, ["--version"], { timeoutMs: 6e4 });
8497
+ if (!res.ok)
8498
+ return { ok: false, version: null, reason: `--version \u6267\u884C\u5931\u8D25\uFF08${res.error ?? `exit ${res.code}`}\uFF09\uFF1A${res.stderr.trim().slice(0, 300) || "\u65E0\u8F93\u51FA"}` };
8499
+ try {
8500
+ const parsed = JSON.parse(res.stdout.trim());
8501
+ const version = typeof parsed.version === "string" ? parsed.version : null;
8502
+ if (!version)
8503
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u7F3A\u5C11 version\uFF1A${res.stdout.trim().slice(0, 200)}` };
8504
+ if (expected && version !== expected)
8505
+ return { ok: false, version, reason: `\u5B89\u88C5\u540E\u7248\u672C\u4E0D\u7B26\uFF1A\u671F\u671B ${expected}\uFF0C\u5B9E\u9645 ${version}` };
8506
+ return { ok: true, version };
8507
+ } catch {
8508
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u975E JSON\uFF1A${res.stdout.trim().slice(0, 200)}` };
8509
+ }
8510
+ }
8511
+ async function performRuntimeUpgrade(opts) {
8512
+ const log = opts.log ?? (() => {
8513
+ });
8514
+ const registry2 = opts.registry ?? resolveRegistry();
8515
+ const steps = [];
8516
+ const from = opts.currentVersion;
8517
+ let target;
8518
+ if (opts.targetVersion) {
8519
+ target = opts.targetVersion;
8520
+ } else {
8521
+ target = await fetchLatestRuntimeVersion({ registry: registry2 });
8522
+ steps.push(`registry latest: ${target}`);
8523
+ }
8524
+ if (!isUpgradeAvailable(from, target)) {
8525
+ steps.push(`\u5F53\u524D ${from} \u5DF2\u4E0D\u4F4E\u4E8E ${target}\uFF0C\u65E0\u9700\u5347\u7EA7`);
8526
+ return { status: "up_to_date", fromVersion: from, targetVersion: target, steps };
8527
+ }
8528
+ log(`[upgrade] ${from} \u2192 ${target}\uFF08${registry2}\uFF09`);
8529
+ const ctx = await npmContext(registry2);
8530
+ steps.push(`npm install -g ${RUNTIME_PACKAGE}@${target}`);
8531
+ const install = await npmInstallGlobal(ctx, target, registry2, NPM_INSTALL_TIMEOUT_MS);
8532
+ if (!install.ok) {
8533
+ const error = `\u5B89\u88C5\u5931\u8D25\uFF1A${install.error ?? `exit ${install.code}`} ${install.stderr.trim().slice(0, 300)}`.trim();
8534
+ steps.push(error);
8535
+ log(`[upgrade] ${error}`);
8536
+ const still = await verifyRuntimeInstall(ctx.layout.runtimeBinPath);
8537
+ if (still.ok) {
8538
+ steps.push(`\u539F\u7248\u672C ${still.version ?? from} \u5B89\u88C5\u5B8C\u597D\uFF0C\u4FDD\u6301\u4E0D\u53D8`);
8539
+ return { status: "failed", fromVersion: from, toVersion: null, error, steps };
8540
+ }
8541
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8542
+ if (rollback.ok) {
8543
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from}`);
8544
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8545
+ }
8546
+ steps.push(`\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF1A${rollback.error ?? `exit ${rollback.code}`}`);
8547
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u4E14\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF0C\u5F53\u524D\u5168\u5C40\u5B89\u88C5\u53EF\u80FD\u635F\u574F\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`, steps };
8548
+ }
8549
+ const verified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, target);
8550
+ if (!verified.ok) {
8551
+ const error = `\u65B0\u5B89\u88C5\u9A8C\u8BC1\u5931\u8D25\uFF1A${verified.reason ?? "\u672A\u77E5\u539F\u56E0"}`;
8552
+ steps.push(error);
8553
+ log(`[upgrade] ${error}`);
8554
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8555
+ if (rollback.ok) {
8556
+ const rollbackVerified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, from);
8557
+ if (rollbackVerified.ok) {
8558
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from} \u5E76\u9A8C\u8BC1\u53EF\u7528`);
8559
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8560
+ }
8561
+ steps.push(`\u56DE\u88C5\u540E\u9A8C\u8BC1\u5931\u8D25\uFF1A${rollbackVerified.reason ?? "\u672A\u77E5"}\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`);
8562
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u56DE\u88C5 ${from} \u540E\u9A8C\u8BC1\u4ECD\u5931\u8D25`, steps };
8563
+ }
8564
+ steps.push(`\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF1A${rollback.error ?? `exit ${rollback.code}`}\uFF08\u8BF7\u624B\u52A8\u6267\u884C npm i -g ${RUNTIME_PACKAGE}@${from}\uFF09`);
8565
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u4E14\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25`, steps };
8566
+ }
8567
+ steps.push(`\u65B0\u7248\u672C ${verified.version} \u9A8C\u8BC1\u901A\u8FC7`);
8568
+ const warnings = [];
8569
+ const setup = await run2(ctx.layout.runtimeBinPath, ["setup"], { timeoutMs: 6e4 });
8570
+ if (setup.ok) {
8571
+ steps.push("\u63D2\u4EF6\u76EE\u5F55\u5DF2\u91CD\u70D8\u7119\uFF08morula-runtime setup\uFF09");
8572
+ } else {
8573
+ const warning = `\u63D2\u4EF6\u76EE\u5F55\u91CD\u70D8\u7119\u5931\u8D25\uFF08${setup.error ?? `exit ${setup.code}`}\uFF09\u2014\u2014 \u4E0D\u5F71\u54CD\u672C\u6B21\u5347\u7EA7\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C morula-runtime setup`;
8574
+ warnings.push(warning);
8575
+ steps.push(warning);
8576
+ log(`[upgrade] ${warning}`);
8577
+ }
8578
+ log(`[upgrade] \u5347\u7EA7\u5B8C\u6210\uFF1A${from} \u2192 ${verified.version}`);
8579
+ return { status: "succeeded", fromVersion: from, toVersion: verified.version ?? target, warnings, steps };
8580
+ }
8581
+ async function spawnUpgradedDaemon(opts) {
8582
+ let packageRoot = opts.runtimePackageRoot;
8583
+ if (!packageRoot || !existsSync16(join19(packageRoot, "dist", "index.js"))) {
8584
+ const layout = await resolveNpmGlobalLayout();
8585
+ packageRoot = layout.runtimePackageRoot;
8586
+ }
8587
+ const entry = join19(packageRoot, "dist", "index.js");
8588
+ if (!existsSync16(entry))
8589
+ return { pid: 0, entry, error: `\u65B0\u5305\u5165\u53E3\u4E0D\u5B58\u5728\uFF1A${entry}`, waitExit: async () => ({ exited: true, code: null }) };
8590
+ mkdirSync13(dirname10(opts.logFile), { recursive: true, mode: 448 });
8591
+ let logFd;
8592
+ try {
8593
+ logFd = openSync4(opts.logFile, "a", 384);
8594
+ } catch (error) {
8595
+ return { pid: 0, entry, error: `\u65E0\u6CD5\u6253\u5F00\u65E5\u5FD7\u6587\u4EF6 ${opts.logFile}: ${error instanceof Error ? error.message : String(error)}`, waitExit: async () => ({ exited: true, code: null }) };
8596
+ }
8597
+ try {
8598
+ const child = spawn4(process.execPath, [entry, "daemon", "run"], {
8599
+ cwd: process.cwd(),
8600
+ detached: true,
8601
+ stdio: ["ignore", logFd, logFd],
8602
+ env: {
8603
+ ...opts.env,
8604
+ MORULA_DAEMON_NAME: opts.daemonConfig?.name ?? "",
8605
+ MORULA_DAEMON_WORKDIR: opts.daemonConfig?.workdir ?? "",
8606
+ MORULA_DAEMON_MAX_CONCURRENT: String(opts.daemonConfig?.maxConcurrent ?? ""),
8607
+ MORULA_DAEMON_RESTART_WAIT_MS: String(RESTART_LOCK_WAIT_MS)
8608
+ }
8609
+ });
8610
+ child.unref();
8611
+ if (!child.pid)
8612
+ return { pid: 0, entry, error: "spawn \u672A\u8FD4\u56DE pid", waitExit: async () => ({ exited: true, code: null }) };
8613
+ const exitPromise = new Promise((resolve2) => {
8614
+ child.on("exit", (code) => {
8615
+ resolve2(code ?? null);
8616
+ });
8617
+ });
8618
+ const waitExit = async (ms) => {
8619
+ return await Promise.race([
8620
+ exitPromise.then((code) => ({ exited: true, code })),
8621
+ new Promise((resolve2) => {
8622
+ const timer = setTimeout(() => {
8623
+ resolve2({ exited: false, code: null });
8624
+ }, ms);
8625
+ timer.unref?.();
8626
+ })
8627
+ ]);
8628
+ };
8629
+ mkdirSync13(dirname10(opts.pidFile), { recursive: true, mode: 448 });
8630
+ writeFileSync11(opts.pidFile, String(child.pid), { mode: 384 });
8631
+ return { pid: child.pid, entry, waitExit };
8632
+ } catch (error) {
8633
+ return { pid: 0, entry, error: error instanceof Error ? error.message : String(error), waitExit: async () => ({ exited: true, code: null }) };
8634
+ } finally {
8635
+ try {
8636
+ closeSync4(logFd);
8637
+ } catch {
8638
+ }
8639
+ }
8640
+ }
8641
+
8278
8642
  // src/daemon/session-host.ts
8279
8643
  init_dist2();
8280
8644
  init_acp_connection();
8281
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "node:fs";
8282
- import { join as join19 } from "node:path";
8645
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync12 } from "node:fs";
8646
+ import { join as join20 } from "node:path";
8283
8647
 
8284
8648
  // src/daemon/steward-instructions.ts
8285
8649
  var STEWARD_SYSTEM_INSTRUCTIONS = `\u4F60\u662F\u300C\u7A7A\u95F4\u7BA1\u5BB6\u667A\u80FD\u4F53\u300D\uFF08Project Steward\uFF09\u3002
@@ -8533,8 +8897,8 @@ var SessionHost = class {
8533
8897
  async open(cmd) {
8534
8898
  if (!cmd?.sessionId || !cmd?.tenantId || !cmd?.projectId)
8535
8899
  return { ok: false, error: "SESSION_OPEN_INVALID_ARGS" };
8536
- const workspaceDir = join19(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8537
- mkdirSync13(workspaceDir, { recursive: true });
8900
+ const workspaceDir = join20(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8901
+ mkdirSync14(workspaceDir, { recursive: true });
8538
8902
  this.writeInjections(workspaceDir, cmd);
8539
8903
  const existing = this.sessions.get(cmd.sessionId);
8540
8904
  if (existing?.conn.isAlive) {
@@ -8820,7 +9184,7 @@ var SessionHost = class {
8820
9184
  ---
8821
9185
 
8822
9186
  ${cmd.prompt}`;
8823
- const run3 = (async () => {
9187
+ const run5 = (async () => {
8824
9188
  try {
8825
9189
  const { blocks, fileRefs } = await this.materializeAttachments(managed.workspaceDir, cmd.attachments ?? []);
8826
9190
  const promptBlocks = [{ type: "text", text: fileRefs.length > 0 ? `${text}
@@ -8867,8 +9231,8 @@ ${fileRefs.join("\n")}` : text }];
8867
9231
  this.touch(managed);
8868
9232
  }
8869
9233
  })();
8870
- managed.promptInFlight = run3;
8871
- return run3;
9234
+ managed.promptInFlight = run5;
9235
+ return run5;
8872
9236
  }
8873
9237
  /**
8874
9238
  * 附件物化(§五 P2-3):
@@ -8883,7 +9247,7 @@ ${fileRefs.join("\n")}` : text }];
8883
9247
  const fileRefs = [];
8884
9248
  if (attachments.length === 0)
8885
9249
  return { blocks, fileRefs };
8886
- const dir = join19(workspaceDir, "attachments");
9250
+ const dir = join20(workspaceDir, "attachments");
8887
9251
  for (const attachment of attachments) {
8888
9252
  try {
8889
9253
  const res = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
@@ -8893,9 +9257,9 @@ ${fileRefs.join("\n")}` : text }];
8893
9257
  if (attachment.isImage) {
8894
9258
  blocks.push({ type: "image", data: bytes.toString("base64"), mimeType: attachment.fileType || "image/png" });
8895
9259
  } else {
8896
- mkdirSync13(dir, { recursive: true });
8897
- const target = join19(dir, safeAttachmentName(attachment.fileName));
8898
- writeFileSync11(target, bytes);
9260
+ mkdirSync14(dir, { recursive: true });
9261
+ const target = join20(dir, safeAttachmentName(attachment.fileName));
9262
+ writeFileSync12(target, bytes);
8899
9263
  fileRefs.push(`- ${attachment.fileName} \u2192 ${target}`);
8900
9264
  }
8901
9265
  } catch (err) {
@@ -8991,13 +9355,13 @@ ${fileRefs.join("\n")}` : text }];
8991
9355
  /** 工作区注入物(幂等覆盖;daemon 侧组装,P1-6) */
8992
9356
  writeInjections(workspaceDir, cmd) {
8993
9357
  writeProjectAgentsMd(workspaceDir, cmd.agentsMd, this.log);
8994
- const skillDir = join19(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
8995
- mkdirSync13(skillDir, { recursive: true });
8996
- writeFileSync11(join19(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
8997
- const contextDir = join19(workspaceDir, ".agent_context");
8998
- mkdirSync13(contextDir, { recursive: true });
8999
- writeFileSync11(
9000
- join19(contextDir, "steward-context.md"),
9358
+ const skillDir = join20(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
9359
+ mkdirSync14(skillDir, { recursive: true });
9360
+ writeFileSync12(join20(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
9361
+ const contextDir = join20(workspaceDir, ".agent_context");
9362
+ mkdirSync14(contextDir, { recursive: true });
9363
+ writeFileSync12(
9364
+ join20(contextDir, "steward-context.md"),
9001
9365
  buildStewardContextMd(cmd.facts ?? { projectId: cmd.projectId, tenantId: cmd.tenantId }),
9002
9366
  "utf8"
9003
9367
  );
@@ -9541,6 +9905,12 @@ var DaemonRunner = class {
9541
9905
  outboxConsecutiveFailures = 0;
9542
9906
  outboxPausedUntil = 0;
9543
9907
  processing = /* @__PURE__ */ new Set();
9908
+ /**
9909
+ * runtime-self-upgrade:远程升级执行中标志。
9910
+ * 置位期间 tick / WS 派发**不领新任务**——升级完成后 daemon 会重启,中途接的任务会被打断
9911
+ * (心跳超时 → 服务端回收重派),违背「升级不破坏正在执行的任务」的约束。
9912
+ */
9913
+ upgradeInProgress = false;
9544
9914
  /** M1-A1:任务级取消信号(WS cancelled 事件 / 状态轮询触发 abort) */
9545
9915
  cancels = /* @__PURE__ */ new Map();
9546
9916
  /** Phase 2:审批决策等待表(approvalId → resolver;WS 事件 resolve,轮询兜底) */
@@ -9682,7 +10052,10 @@ var DaemonRunner = class {
9682
10052
  models: dimModels,
9683
10053
  // LOCAL-AGENT-COLLAB-001 Phase 1:宿主协议 + Provider inventory
9684
10054
  hostProtocolVersion: 1,
9685
- hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false },
10055
+ // selfUpgrade(runtime-self-upgrade 评审 P2):本 daemon 认识心跳响应里的 upgradeRequested
10056
+ // 指令并具备自升级能力。管理页据此门控远程升级按钮——旧版 daemon 会静默吞掉指令,
10057
+ // 对它显示按钮只会误导用户(能力自报比服务端维护最低版本号更准确,register 即刷新)
10058
+ hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false, selfUpgrade: true },
9686
10059
  inventoryRevision: `daemon-v${BUILD_VERSION2}`,
9687
10060
  providers: inventory
9688
10061
  });
@@ -9700,10 +10073,33 @@ var DaemonRunner = class {
9700
10073
  await this.tick();
9701
10074
  if (!this.running)
9702
10075
  break;
9703
- await sleep(POLL_INTERVAL_MS);
10076
+ await this.waitForPollInterval();
9704
10077
  }
9705
10078
  this.stop();
9706
10079
  }
10080
+ /** 下一拍等待(POLL_INTERVAL_MS,可被 wakeMainLoop 提前唤醒) */
10081
+ pollWake = null;
10082
+ waitForPollInterval() {
10083
+ return new Promise((resolve2) => {
10084
+ const timer = setTimeout(() => {
10085
+ this.pollWake = null;
10086
+ resolve2();
10087
+ }, POLL_INTERVAL_MS);
10088
+ timer.unref?.();
10089
+ this.pollWake = () => {
10090
+ clearTimeout(timer);
10091
+ this.pollWake = null;
10092
+ resolve2();
10093
+ };
10094
+ });
10095
+ }
10096
+ /**
10097
+ * 唤醒主循环的 poll 等待(评审 P4 配套):远程升级完成(running=false)后立即生效,
10098
+ * 不必最多干等 15s 的 poll 间隔——旧进程越快退出释放锁,新进程越快接管,自重启窗口越小。
10099
+ */
10100
+ wakeMainLoop() {
10101
+ this.pollWake?.();
10102
+ }
9707
10103
  stop() {
9708
10104
  this.running = false;
9709
10105
  this.stopHeartbeatTimer();
@@ -9768,6 +10164,10 @@ var DaemonRunner = class {
9768
10164
  this.running = false;
9769
10165
  return;
9770
10166
  }
10167
+ if (res?.upgradeRequested) {
10168
+ this.log(`[daemon] \u6536\u5230\u670D\u52A1\u7AEF\u8FDC\u7A0B\u5347\u7EA7\u6307\u4EE4${res.upgradeVersion ? `\uFF08\u76EE\u6807 ${res.upgradeVersion}\uFF09` : ""}`);
10169
+ void this.handleRemoteUpgrade(res.upgradeVersion ?? null);
10170
+ }
9771
10171
  if (!res.ok && res.status === "unknown") {
9772
10172
  this.log("[daemon] \u672C\u8BBE\u5907\u5DF2\u5728\u670D\u52A1\u7AEF\u6CE8\u9500\uFF08runtime \u8BB0\u5F55\u4E0D\u5B58\u5728\uFF09\uFF0C\u81EA\u52A8\u9000\u51FA");
9773
10173
  this.running = false;
@@ -9786,6 +10186,95 @@ var DaemonRunner = class {
9786
10186
  this.heartbeatInFlight = false;
9787
10187
  }
9788
10188
  }
10189
+ /**
10190
+ * runtime-self-upgrade:远程升级指令执行(心跳认领后异步触发,见 sendHeartbeat)。
10191
+ *
10192
+ * 边界:
10193
+ * - 有任务在跑(processing 非空)→ 回报 busy 拒绝(指令已消耗,页面可重新下发);
10194
+ * - 空闲 → performRuntimeUpgrade(安装/验证/失败回装/重烘焙插件目录);期间心跳照常、不领新任务;
10195
+ * - 成功 → 用**新包入口** detached 拉起新 daemon(等旧进程释放单实例锁)→ 回报 → 本实例退出;
10196
+ * - 失败/回退 → 回报后**继续用原版本运行**(升级失败不中断服务)。
10197
+ */
10198
+ async handleRemoteUpgrade(targetVersion) {
10199
+ if (!this.runtimeId)
10200
+ return;
10201
+ if (this.upgradeInProgress) {
10202
+ this.log("[daemon] \u5347\u7EA7\u5DF2\u5728\u8FDB\u884C\uFF0C\u5FFD\u7565\u91CD\u590D\u6307\u4EE4");
10203
+ return;
10204
+ }
10205
+ this.upgradeInProgress = true;
10206
+ const fromVersion = BUILD_VERSION2;
10207
+ try {
10208
+ if (this.processing.size > 0) {
10209
+ const error = `${this.processing.size} \u4E2A\u4EFB\u52A1\u6267\u884C\u4E2D`;
10210
+ this.log(`[daemon] \u5347\u7EA7\u6307\u4EE4\u88AB\u62D2\u7EDD\uFF1A${error}`);
10211
+ await this.reportUpgradeResultSafe({ status: "busy", fromVersion, error });
10212
+ return;
10213
+ }
10214
+ const report = await performRuntimeUpgrade({
10215
+ currentVersion: fromVersion,
10216
+ targetVersion,
10217
+ log: (line) => this.log(line)
10218
+ });
10219
+ if (report.status === "up_to_date") {
10220
+ this.log(`[daemon] \u5F53\u524D ${fromVersion} \u5DF2\u4E0D\u4F4E\u4E8E ${report.targetVersion}\uFF0C\u65E0\u9700\u5347\u7EA7`);
10221
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: fromVersion, restart: false });
10222
+ return;
10223
+ }
10224
+ if (report.status !== "succeeded") {
10225
+ this.log(`[daemon] \u5347\u7EA7\u5931\u8D25\uFF08${report.status}\uFF09\uFF1A${report.error}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10226
+ await this.reportUpgradeResultSafe({
10227
+ status: report.status,
10228
+ fromVersion,
10229
+ toVersion: report.toVersion ?? void 0,
10230
+ error: report.error
10231
+ });
10232
+ return;
10233
+ }
10234
+ const cfg = loadDaemonConfig(this.store);
10235
+ const dir = daemonDirForEnv(process.env);
10236
+ const spawned = await spawnUpgradedDaemon({
10237
+ env: process.env,
10238
+ logFile: join21(dir, "daemon.log"),
10239
+ pidFile: join21(dir, "daemon.pid"),
10240
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
10241
+ });
10242
+ if (spawned.pid <= 0) {
10243
+ const error = `\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4F46\u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${spawned.error ?? "\u672A\u77E5\u539F\u56E0"}\uFF08\u672C\u5B9E\u4F8B\u7EE7\u7EED\u8FD0\u884C\u65E7\u4EE3\u7801\uFF0C\u8BF7\u624B\u52A8\u6267\u884C morula-runtime daemon start \u91CD\u542F\uFF09`;
10244
+ this.log(`[daemon] ${error}`);
10245
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10246
+ return;
10247
+ }
10248
+ this.log(`[daemon] \u65B0 daemon \u5DF2\u62C9\u8D77\uFF08pid=${spawned.pid}\uFF09\uFF0C\u89C2\u5BDF ${Math.round(RESTART_LIVENESS_CHECK_MS / 1e3)}s \u786E\u8BA4\u63A5\u7BA1\u2026`);
10249
+ const liveness = await spawned.waitExit(RESTART_LIVENESS_CHECK_MS);
10250
+ if (liveness.exited) {
10251
+ const error = `\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4F46\u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF0C\u8BE6\u89C1 ${join21(dir, "daemon.log")}\uFF09\u2014\u2014\u672C\u5B9E\u4F8B\u7EE7\u7EED\u8FD0\u884C\u65E7\u4EE3\u7801\uFF0C\u8BF7\u68C0\u67E5\u65E5\u5FD7\u540E\u624B\u52A8 morula-runtime daemon start`;
10252
+ this.log(`[daemon] ${error}`);
10253
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10254
+ return;
10255
+ }
10256
+ this.log("[daemon] \u65B0\u8FDB\u7A0B\u8FD0\u884C\u4E2D\uFF08\u7B49\u672C\u5B9E\u4F8B\u9000\u51FA\u91CA\u653E\u9501\u540E\u63A5\u7BA1\uFF09\uFF0C\u56DE\u62A5\u6210\u529F\u5E76\u9000\u51FA");
10257
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: report.toVersion, restart: true });
10258
+ this.running = false;
10259
+ this.wakeMainLoop();
10260
+ } catch (error) {
10261
+ const message = error instanceof Error ? error.message : String(error);
10262
+ this.log(`[daemon] \u81EA\u5347\u7EA7\u5F02\u5E38\uFF1A${message}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10263
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, error: message });
10264
+ } finally {
10265
+ this.upgradeInProgress = false;
10266
+ }
10267
+ }
10268
+ /** 升级结果回报(best-effort:服务端不可达不影响本地升级流程) */
10269
+ async reportUpgradeResultSafe(body) {
10270
+ if (!this.runtimeId)
10271
+ return;
10272
+ try {
10273
+ await this.api.reportUpgradeResult(this.runtimeId, body);
10274
+ } catch (error) {
10275
+ this.log(`[daemon] \u5347\u7EA7\u7ED3\u679C\u56DE\u62A5\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u5730\u6D41\u7A0B\uFF09: ${error instanceof Error ? error.message : String(error)}`);
10276
+ }
10277
+ }
9789
10278
  /**
9790
10279
  * tick:poll 兜底 + 取消轮询 + cleanup 补拉(心跳与 outbox 补投均已独立于本循环)。
9791
10280
  * D1:整轮加时间预算——超预算记 WARN 并丢弃本轮等待下一拍,避免单个慢请求把 tick 拖死。
@@ -9814,6 +10303,8 @@ var DaemonRunner = class {
9814
10303
  async tickOnce() {
9815
10304
  if (!this.runtimeId)
9816
10305
  return;
10306
+ if (this.upgradeInProgress)
10307
+ return;
9817
10308
  try {
9818
10309
  const tasks = await this.api.pollTasks(this.runtimeId);
9819
10310
  for (const task of tasks) {
@@ -10213,6 +10704,10 @@ var DaemonRunner = class {
10213
10704
  async handleTask(taskId) {
10214
10705
  if (this.processing.has(taskId))
10215
10706
  return;
10707
+ if (this.upgradeInProgress) {
10708
+ this.log(`[daemon] \u5347\u7EA7\u8FDB\u884C\u4E2D\uFF0C\u5FFD\u7565\u4EFB\u52A1\u6D3E\u53D1 task=${taskId}`);
10709
+ return;
10710
+ }
10216
10711
  this.processing.add(taskId);
10217
10712
  const ctrl = new AbortController();
10218
10713
  this.cancels.set(taskId, ctrl);
@@ -10482,12 +10977,12 @@ var DaemonRunner = class {
10482
10977
  });
10483
10978
  if (spaceToken?.token)
10484
10979
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
10485
- repoDir = join20(cfg.workdir, "scratch", taskId);
10980
+ repoDir = join21(cfg.workdir, "scratch", taskId);
10486
10981
  taskRoot = repoDir;
10487
- mkdirSync14(repoDir, { recursive: true });
10488
- const readme = join20(repoDir, "README.md");
10489
- if (!existsSync16(readme)) {
10490
- writeFileSync12(
10982
+ mkdirSync15(repoDir, { recursive: true });
10983
+ const readme = join21(repoDir, "README.md");
10984
+ if (!existsSync17(readme)) {
10985
+ writeFileSync13(
10491
10986
  readme,
10492
10987
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
10493
10988
 
@@ -10785,7 +11280,7 @@ var DaemonRunner = class {
10785
11280
  }, { log: (line) => this.log(`[daemon] ${line}`) });
10786
11281
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
10787
11282
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
10788
- const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync16(resumeWorkDir);
11283
+ const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync17(resumeWorkDir);
10789
11284
  const input = {
10790
11285
  executionId: `${taskId}:1`,
10791
11286
  // task-workspace-layout:新会话 cwd = 任务工作区根(AGENTS.md/.agents 在根下,仓库在子目录)
@@ -11046,7 +11541,7 @@ var DaemonRunner = class {
11046
11541
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
11047
11542
  return;
11048
11543
  }
11049
- const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
11544
+ const managedRoot = join21(cfg.workdir, "worktrees", this.tenantId);
11050
11545
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
11051
11546
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
11052
11547
  ok: false,
@@ -11085,47 +11580,44 @@ var DaemonRunner = class {
11085
11580
  }
11086
11581
  }
11087
11582
  };
11088
- function sleep(ms) {
11089
- return new Promise((r) => setTimeout(r, ms));
11090
- }
11091
11583
  function isDebugLogEnabled() {
11092
11584
  return isFlagEnabled("MORULA_DAEMON_DEBUG_LOG", false);
11093
11585
  }
11094
11586
  var spaceTokenFailureLogged = false;
11095
11587
  function writeCustomSkills(targetDir, skills, log) {
11096
11588
  const NAME_RE = /^[\w-]{1,64}$/;
11097
- const base = join20(targetDir, ".agents", "skills");
11589
+ const base = join21(targetDir, ".agents", "skills");
11098
11590
  for (const skill of skills) {
11099
11591
  const name = skill.name.trim();
11100
11592
  if (!NAME_RE.test(name)) {
11101
11593
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
11102
11594
  continue;
11103
11595
  }
11104
- const dir = join20(base, name);
11105
- if (dirname9(dir) !== base) {
11596
+ const dir = join21(base, name);
11597
+ if (dirname11(dir) !== base) {
11106
11598
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
11107
11599
  continue;
11108
11600
  }
11109
- mkdirSync14(dir, { recursive: true });
11601
+ mkdirSync15(dir, { recursive: true });
11110
11602
  const meta = ["---", `name: ${name}`];
11111
11603
  if (skill.description?.trim()) {
11112
11604
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
11113
11605
  }
11114
11606
  meta.push("---", "");
11115
- writeFileSync12(join20(dir, "SKILL.md"), `${meta.join("\n")}
11607
+ writeFileSync13(join21(dir, "SKILL.md"), `${meta.join("\n")}
11116
11608
  ${skill.content ?? ""}`, "utf8");
11117
11609
  }
11118
11610
  }
11119
11611
  function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
11120
- const dir = join20(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11121
- const refsDir = join20(dir, "references");
11122
- mkdirSync14(refsDir, { recursive: true });
11612
+ const dir = join21(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11613
+ const refsDir = join21(dir, "references");
11614
+ mkdirSync15(refsDir, { recursive: true });
11123
11615
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
11124
11616
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
11125
11617
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
11126
- writeFileSync12(join20(dir, "SKILL.md"), `${front}
11618
+ writeFileSync13(join21(dir, "SKILL.md"), `${front}
11127
11619
  ${body}`, "utf8");
11128
- writeFileSync12(join20(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11620
+ writeFileSync13(join21(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11129
11621
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u534F\u4F5C\u5951\u7EA6 skill ${COLLABORATION_SKILL_NAME}\uFF08${isGitlab ? "GitLab" : "GitHub"} \u7248\uFF0C\u4ED3\u5E93=${repo ?? "\u65E0"}\uFF09\u5230 .agents/skills/`);
11130
11622
  }
11131
11623
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -11156,8 +11648,8 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
11156
11648
  };
11157
11649
  }
11158
11650
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11159
- const briefDir = join20(targetDir, ".agent_context");
11160
- mkdirSync14(briefDir, { recursive: true });
11651
+ const briefDir = join21(targetDir, ".agent_context");
11652
+ mkdirSync15(briefDir, { recursive: true });
11161
11653
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
11162
11654
 
11163
11655
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
@@ -11167,7 +11659,7 @@ function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11167
11659
  ---
11168
11660
 
11169
11661
  ${buildTaskPromptWithContract(detail, void 0)}`;
11170
- writeFileSync12(join20(briefDir, "task-context.md"), brief, "utf8");
11662
+ writeFileSync13(join21(briefDir, "task-context.md"), brief, "utf8");
11171
11663
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
11172
11664
  }
11173
11665
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -11181,8 +11673,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
11181
11673
  log?.(`[daemon] \u7A7A\u95F4 AGENTS.md \u5185\u5BB9\u8D85\u957F\uFF08${content.length} \u5B57\u7B26 > ${PROJECT_AGENTS_MD_MAX_CHARS}\uFF09\uFF0C\u5DF2\u622A\u65AD\u5199\u5165`);
11182
11674
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
11183
11675
  }
11184
- mkdirSync14(targetDir, { recursive: true });
11185
- writeFileSync12(join20(targetDir, "AGENTS.md"), `${content}
11676
+ mkdirSync15(targetDir, { recursive: true });
11677
+ writeFileSync13(join21(targetDir, "AGENTS.md"), `${content}
11186
11678
  `, "utf8");
11187
11679
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u7A7A\u95F4\u9879\u76EE\u8BF4\u660E\u5230 AGENTS.md\uFF08${content.length} \u5B57\u7B26\uFF0C\u4F5C\u4E3A project.instructions \u81EA\u52A8\u52A0\u8F7D\uFF09`);
11188
11680
  return true;
@@ -11241,7 +11733,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
11241
11733
  }
11242
11734
  async function reportOneRepoTarget(taskId, target, env, ops) {
11243
11735
  const execEnv = mergeExecEnv(env);
11244
- if (!existsSync16(target.dir) || !existsSync16(join20(target.dir, ".git"))) {
11736
+ if (!existsSync17(target.dir) || !existsSync17(join21(target.dir, ".git"))) {
11245
11737
  ops.log(`[daemon] \u4EFB\u52A1 ${taskId} \u4ED3\u5E93 ${target.repoFullName} \u4EA4\u4ED8\u8DF3\u8FC7\uFF1A\u5DE5\u4F5C\u76EE\u5F55\u5DF2\u4E0D\u5B58\u5728\uFF08cleanup/\u5206\u652F\u6539\u540D\u65F6\u5E8F\uFF09`);
11246
11738
  return;
11247
11739
  }
@@ -11409,9 +11901,9 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
11409
11901
 
11410
11902
  // src/commands/doctor.ts
11411
11903
  init_dist2();
11412
- import { existsSync as existsSync17, readFileSync as readFileSync11, realpathSync } from "node:fs";
11904
+ import { existsSync as existsSync18, readFileSync as readFileSync11, realpathSync } from "node:fs";
11413
11905
  import { homedir as homedir12 } from "node:os";
11414
- import { dirname as dirname10, join as join21 } from "node:path";
11906
+ import { dirname as dirname12, join as join22 } from "node:path";
11415
11907
  init_daemon_config();
11416
11908
  init_daemon_lock();
11417
11909
  init_dim_locate();
@@ -11431,16 +11923,16 @@ async function doctorCommand(context) {
11431
11923
  async function collectDoctor(context, probe) {
11432
11924
  const checks = [];
11433
11925
  const env = context.env;
11434
- const configDir = env.MORULA_CONFIG_DIR ?? join21(homedir12(), ".morula-cli");
11435
- const configPath = env.MORULA_CONFIG ?? join21(configDir, "config.yaml");
11436
- const daemonPath = join21(dirname10(configPath), "daemon");
11437
- const logFile = join21(daemonPath, "daemon.log");
11926
+ const configDir = env.MORULA_CONFIG_DIR ?? join22(homedir12(), ".morula-cli");
11927
+ const configPath = env.MORULA_CONFIG ?? join22(configDir, "config.yaml");
11928
+ const daemonPath = join22(dirname12(configPath), "daemon");
11929
+ const logFile = join22(daemonPath, "daemon.log");
11438
11930
  const contextName = context.runtime.context ?? null;
11439
11931
  const baseUrl = context.runtime.baseUrl ?? null;
11440
11932
  const tokenPresent = Boolean(context.runtime.token);
11441
11933
  const tokenSource = context.runtime.tokenSource ?? null;
11442
11934
  let configReadable = false;
11443
- if (existsSync17(configPath)) {
11935
+ if (existsSync18(configPath)) {
11444
11936
  try {
11445
11937
  readFileSync11(configPath, "utf8");
11446
11938
  configReadable = true;
@@ -11448,9 +11940,9 @@ async function collectDoctor(context, probe) {
11448
11940
  configReadable = false;
11449
11941
  }
11450
11942
  }
11451
- const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
11943
+ const lockPath = join22(daemonPath, DAEMON_LOCK_FILE);
11452
11944
  const lock = probeDaemonLock(lockPath);
11453
- const pidFile = join21(daemonPath, DAEMON_PID_FILE);
11945
+ const pidFile = join22(daemonPath, DAEMON_PID_FILE);
11454
11946
  const filePid = readPidFile(pidFile);
11455
11947
  const daemonPid = probeDaemonInstance(configDir).pid;
11456
11948
  const cfg = loadDaemonConfig(context.store);
@@ -11502,8 +11994,8 @@ async function collectDoctor(context, probe) {
11502
11994
  hint: "morula-runtime auth login"
11503
11995
  });
11504
11996
  } else if (probe) {
11505
- const check = await probeAuth(context);
11506
- checks.push(check);
11997
+ const check2 = await probeAuth(context);
11998
+ checks.push(check2);
11507
11999
  } else {
11508
12000
  checks.push({
11509
12001
  id: "auth.token",
@@ -11512,7 +12004,7 @@ async function collectDoctor(context, probe) {
11512
12004
  hint: "morula-runtime daemon doctor --probe\uFF08\u8054\u7F51\u786E\u8BA4\u767B\u5F55\u6001\uFF09"
11513
12005
  });
11514
12006
  }
11515
- checks.push(configReadable ? { id: "config.readable", status: "ok", message: `\u914D\u7F6E\u53EF\u8BFB\uFF1A${configPath}` } : existsSync17(configPath) ? { id: "config.readable", status: "fail", message: `\u914D\u7F6E\u5B58\u5728\u4F46\u4E0D\u53EF\u8BFB\uFF08\u6743\u9650\uFF1F\uFF09\uFF1A${configPath}`, hint: `ls -l ${configPath}` } : { id: "config.readable", status: "warn", message: `\u914D\u7F6E\u6587\u4EF6\u5C1A\u4E0D\u5B58\u5728\uFF1A${configPath}`, hint: "morula-runtime auth login\uFF08\u767B\u5F55\u540E\u751F\u6210\uFF09" });
12007
+ checks.push(configReadable ? { id: "config.readable", status: "ok", message: `\u914D\u7F6E\u53EF\u8BFB\uFF1A${configPath}` } : existsSync18(configPath) ? { id: "config.readable", status: "fail", message: `\u914D\u7F6E\u5B58\u5728\u4F46\u4E0D\u53EF\u8BFB\uFF08\u6743\u9650\uFF1F\uFF09\uFF1A${configPath}`, hint: `ls -l ${configPath}` } : { id: "config.readable", status: "warn", message: `\u914D\u7F6E\u6587\u4EF6\u5C1A\u4E0D\u5B58\u5728\uFF1A${configPath}`, hint: "morula-runtime auth login\uFF08\u767B\u5F55\u540E\u751F\u6210\uFF09" });
11516
12008
  if (!dimFound) {
11517
12009
  checks.push({
11518
12010
  id: "dim.present",
@@ -11619,19 +12111,13 @@ function detectInstallMode() {
11619
12111
  }
11620
12112
 
11621
12113
  // src/commands/daemon.ts
11622
- function configFilePath(env) {
11623
- return env.MORULA_CONFIG ?? join22(env.MORULA_CONFIG_DIR ?? join22(homedir13(), ".morula-cli"), "config.yaml");
11624
- }
11625
- function daemonDir(configPath) {
11626
- return join22(dirname11(configPath), "daemon");
11627
- }
11628
12114
  var PID_FILE = DAEMON_PID_FILE;
11629
12115
  var LOG_FILE = "daemon.log";
11630
12116
  var DAEMON_EXIT_FALLBACK_MS = 3e3;
11631
12117
  async function daemonCommand(context) {
11632
12118
  const { action } = context.parsed;
11633
12119
  if (action === "run")
11634
- return run2(context);
12120
+ return run3(context);
11635
12121
  if (action === "start")
11636
12122
  return start(context);
11637
12123
  if (action === "stop")
@@ -11808,7 +12294,7 @@ async function models(context) {
11808
12294
  contextWindow: m.contextWindow
11809
12295
  }));
11810
12296
  }
11811
- async function run2(context) {
12297
+ async function run3(context) {
11812
12298
  rejectUnknownFlags(context.parsed.flags, ["name", "workdir", "max-concurrent"]);
11813
12299
  const armBoundedExit = () => {
11814
12300
  setTimeout(() => process.exit(0), DAEMON_EXIT_FALLBACK_MS).unref();
@@ -11821,9 +12307,18 @@ async function run2(context) {
11821
12307
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
11822
12308
  }
11823
12309
  const resolvedWorkdir = workdir ?? loadDaemonConfig(context.store)?.workdir ?? DEFAULT_WORKDIR;
11824
- mkdirSync15(resolvedWorkdir, { recursive: true });
11825
- const lockPath = join22(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
11826
- const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12310
+ mkdirSync16(resolvedWorkdir, { recursive: true });
12311
+ const lockPath = join23(daemonDirForEnv(context.env), DAEMON_LOCK_FILE);
12312
+ let acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12313
+ const restartWaitMs = Number(context.env.MORULA_DAEMON_RESTART_WAIT_MS ?? 0) || 0;
12314
+ if (!acquired.ok && restartWaitMs > 0) {
12315
+ const deadline = Date.now() + restartWaitMs;
12316
+ context.err(`[daemon] \u81EA\u5347\u7EA7\u91CD\u542F\uFF1A\u7B49\u5F85\u65E7\u5B9E\u4F8B\u91CA\u653E\u5355\u5B9E\u4F8B\u9501\uFF08\u6700\u591A ${Math.round(restartWaitMs / 1e3)}s\uFF09`);
12317
+ while (!acquired.ok && Date.now() < deadline) {
12318
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
12319
+ acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12320
+ }
12321
+ }
11827
12322
  if (!acquired.ok) {
11828
12323
  const pid = acquired.existing?.pid;
11829
12324
  if (context.env.MORULA_DAEMON_SUPERVISED === "1") {
@@ -11849,7 +12344,7 @@ async function run2(context) {
11849
12344
  log: (line) => context.err(line),
11850
12345
  // dim acp 子进程治理(issues/dim-acp-process-leak-shared-space-contention.md):
11851
12346
  // spawn 记录落盘(供 daemon 重启后的启动自愈认领自己的孤儿)+ 无归属子进程周期巡检
11852
- acpChildRecordFile: join22(daemonDir(configFilePath(context.env)), "dim-acp-children.json"),
12347
+ acpChildRecordFile: join23(daemonDirForEnv(context.env), "dim-acp-children.json"),
11853
12348
  // D6:认证失败时重新读 config 换票(不传则退回「启动即快照」旧行为)。
11854
12349
  // 只重算凭证,不重建 client——WS 用返回值更新 socket.auth 后重连。
11855
12350
  resolveToken: () => {
@@ -11900,12 +12395,12 @@ async function start(context) {
11900
12395
  throw error;
11901
12396
  identityError = error instanceof Error ? error.message : String(error);
11902
12397
  }
11903
- const dir = daemonDir(configFilePath(context.env));
12398
+ const dir = daemonDirForEnv(context.env);
11904
12399
  if (!dryRun)
11905
- mkdirSync15(dir, { recursive: true, mode: 448 });
11906
- const pidFile = join22(dir, PID_FILE);
11907
- const logFile = join22(dir, LOG_FILE);
11908
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12400
+ mkdirSync16(dir, { recursive: true, mode: 448 });
12401
+ const pidFile = join23(dir, PID_FILE);
12402
+ const logFile = join23(dir, LOG_FILE);
12403
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11909
12404
  const lock = probeDaemonLock(lockPath);
11910
12405
  if (lock.state === "active") {
11911
12406
  throw new CliError(
@@ -11916,7 +12411,7 @@ async function start(context) {
11916
12411
  { hint: "morula-runtime daemon status" }
11917
12412
  );
11918
12413
  }
11919
- if (existsSync18(pidFile)) {
12414
+ if (existsSync19(pidFile)) {
11920
12415
  const pid = Number(readFileSync12(pidFile, "utf8").trim());
11921
12416
  if (isProcessAlive(pid)) {
11922
12417
  throw new CliError("ALREADY_RUNNING", EXIT.CONFLICT, `daemon \u5DF2\u5728\u8FD0\u884C\uFF08pid=${pid}\uFF09\uFF1B\u5982\u9700\u5F3A\u5236\u91CD\u542F\u8BF7\u5148\u6267\u884C kill -9 ${pid}`, false, {
@@ -11967,19 +12462,19 @@ async function start(context) {
11967
12462
  hint: `tail -n 50 ${logFile}`
11968
12463
  });
11969
12464
  }
11970
- writeFileSync13(pidFile, String(result.pid), { mode: 384 });
12465
+ writeFileSync14(pidFile, String(result.pid), { mode: 384 });
11971
12466
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
11972
12467
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
11973
12468
  }
11974
12469
  async function stop(context) {
11975
12470
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
11976
12471
  const dryRun = isDryRun(context);
11977
- const dir = daemonDir(configFilePath(context.env));
11978
- const pidFile = join22(dir, PID_FILE);
11979
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12472
+ const dir = daemonDirForEnv(context.env);
12473
+ const pidFile = join23(dir, PID_FILE);
12474
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11980
12475
  const lock = probeDaemonLock(lockPath);
11981
12476
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
11982
- const filePid = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
12477
+ const filePid = existsSync19(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
11983
12478
  const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
11984
12479
  if (dryRun) {
11985
12480
  return {
@@ -12006,9 +12501,9 @@ async function stop(context) {
12006
12501
  async function status2(context) {
12007
12502
  rejectUnknownFlags(context.parsed.flags, []);
12008
12503
  const cfg = loadDaemonConfig(context.store);
12009
- const dir = daemonDir(configFilePath(context.env));
12010
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12011
- const probe = probeDaemonInstance(dirname11(configFilePath(context.env)));
12504
+ const dir = daemonDirForEnv(context.env);
12505
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
12506
+ const probe = probeDaemonInstance(dirname13(configFilePathForEnv(context.env)));
12012
12507
  return {
12013
12508
  running: probe.running,
12014
12509
  pid: probe.pid,
@@ -12017,7 +12512,7 @@ async function status2(context) {
12017
12512
  name: cfg?.name ?? null,
12018
12513
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
12019
12514
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
12020
- logFile: join22(dir, LOG_FILE),
12515
+ logFile: join23(dir, LOG_FILE),
12021
12516
  lockFile: lockPath
12022
12517
  };
12023
12518
  }
@@ -12074,11 +12569,228 @@ async function setupCommand(context) {
12074
12569
  };
12075
12570
  }
12076
12571
 
12572
+ // src/commands/upgrade.ts
12573
+ init_dist2();
12574
+ import { existsSync as existsSync20, mkdirSync as mkdirSync17, readFileSync as readFileSync13, rmSync as rmSync11 } from "node:fs";
12575
+ import { join as join24 } from "node:path";
12576
+ init_daemon_config();
12577
+ init_daemon_lock();
12578
+ var PID_FILE2 = DAEMON_PID_FILE;
12579
+ var LOG_FILE2 = "daemon.log";
12580
+ async function upgradeCommand(context) {
12581
+ const { action } = context.parsed;
12582
+ if (action === "check")
12583
+ return check(context);
12584
+ if (action === "run")
12585
+ return run4(context);
12586
+ throw usage("usage: morula-runtime upgrade <check|run>", {
12587
+ hint: "morula-runtime upgrade --help"
12588
+ });
12589
+ }
12590
+ async function check(context) {
12591
+ rejectUnknownFlags(context.parsed.flags, ["registry"]);
12592
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12593
+ let latest = null;
12594
+ let error = null;
12595
+ try {
12596
+ latest = await fetchLatestRuntimeVersion({ registry: registry2 });
12597
+ } catch (e) {
12598
+ error = e instanceof Error ? e.message : String(e);
12599
+ }
12600
+ if (latest === null) {
12601
+ context.setExitCode(EXIT.UPSTREAM);
12602
+ return {
12603
+ ok: false,
12604
+ current: BUILD_VERSION2,
12605
+ latest: null,
12606
+ updateAvailable: null,
12607
+ registry: registry2,
12608
+ error,
12609
+ hint: "\u7F51\u7EDC\u4E0D\u53EF\u8FBE\u6216 registry \u4E0D\u53EF\u7528\uFF1B\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u76F4\u63A5\u6267\u884C morula-runtime upgrade run --yes\uFF08run \u4F1A\u81EA\u884C\u89E3\u6790\u76EE\u6807\u7248\u672C\uFF09"
12610
+ };
12611
+ }
12612
+ const updateAvailable = isUpgradeAvailable(BUILD_VERSION2, latest);
12613
+ return {
12614
+ ok: true,
12615
+ current: BUILD_VERSION2,
12616
+ latest,
12617
+ updateAvailable,
12618
+ registry: registry2,
12619
+ ...updateAvailable ? { hint: "morula-runtime upgrade run --yes\uFF08\u5347\u7EA7\u4F1A\u5148\u786E\u8BA4\u65E0\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u88C5\u539F\u7248\u672C\uFF09" } : {}
12620
+ };
12621
+ }
12622
+ function probeDaemonPid(env) {
12623
+ const dir = daemonDirForEnv(env);
12624
+ const pidFile = join24(dir, PID_FILE2);
12625
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12626
+ const lock = probeDaemonLock(lockPath);
12627
+ const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
12628
+ const filePid = existsSync20(pidFile) ? Number(readFileSync13(pidFile, "utf8").trim()) : null;
12629
+ const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
12630
+ return { pid, pidFile, lockPath, lockState: lock.state };
12631
+ }
12632
+ async function reportUpgradeResult(context, body) {
12633
+ const cfg = loadDaemonConfig(context.store);
12634
+ if (!context.runtime.token || !cfg?.runtimeId)
12635
+ return;
12636
+ try {
12637
+ await context.client.http.post(`/agent-runtimes/${cfg.runtimeId}/upgrade-result`, body);
12638
+ } catch (error) {
12639
+ context.err(`[upgrade] \u5347\u7EA7\u7ED3\u679C\u56DE\u62A5\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u672C\u5730\u7ED3\u679C\uFF09: ${error instanceof Error ? error.message : String(error)}`);
12640
+ }
12641
+ }
12642
+ async function countActiveTasks(context, runtimeId) {
12643
+ const res = await context.client.http.get(
12644
+ "/agent-tasks",
12645
+ { runtimeId, statusGroup: "active", page: 1, pageSize: 100 }
12646
+ );
12647
+ return (res?.data ?? []).filter((t) => ["dispatched", "running", "awaiting_approval"].includes(t.status)).length;
12648
+ }
12649
+ async function run4(context) {
12650
+ rejectUnknownFlags(context.parsed.flags, ["yes", "version", "registry"]);
12651
+ const yes = flagBoolean(context.parsed.flags, "yes");
12652
+ const targetVersion = flagString(context.parsed.flags, "version") ?? null;
12653
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12654
+ if (!yes) {
12655
+ throw usage("upgrade run \u9700\u8981 --yes \u663E\u5F0F\u786E\u8BA4\uFF08\u5148\u8DD1 morula-runtime upgrade check \u67E5\u770B\u5DEE\u8DDD\uFF09", {
12656
+ hint: "morula-runtime upgrade run --yes"
12657
+ });
12658
+ }
12659
+ const dir = daemonDirForEnv(context.env);
12660
+ const pidFile = join24(dir, PID_FILE2);
12661
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12662
+ const probe = probeDaemonPid(context.env);
12663
+ const wasRunning = probe.pid !== null;
12664
+ const cfgForSafety = loadDaemonConfig(context.store);
12665
+ let activeTasks = 0;
12666
+ if (wasRunning && cfgForSafety?.runtimeId) {
12667
+ if (!context.runtime.token) {
12668
+ throw new CliError(
12669
+ "TASKS_UNKNOWN",
12670
+ EXIT.UNAUTHENTICATED,
12671
+ "daemon \u8FD0\u884C\u4E2D\u4F46\u5F53\u524D\u672A\u767B\u5F55\uFF0C\u65E0\u6CD5\u786E\u8BA4\u662F\u5426\u6709\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u5DF2\u62D2\u7EDD\u5347\u7EA7",
12672
+ false,
12673
+ { hint: "morula-runtime auth login \u540E\u91CD\u8BD5\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12674
+ );
12675
+ }
12676
+ try {
12677
+ activeTasks = await countActiveTasks(context, cfgForSafety.runtimeId);
12678
+ } catch (error) {
12679
+ throw new CliError(
12680
+ "TASKS_UNKNOWN",
12681
+ EXIT.UPSTREAM,
12682
+ `daemon \u8FD0\u884C\u4E2D\u4E14\u65E0\u6CD5\u786E\u8BA4\u4EFB\u52A1\u72B6\u6001\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5347\u7EA7`,
12683
+ false,
12684
+ { hint: "\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u5148 morula-runtime daemon stop \u518D\u5347\u7EA7\uFF08\u5347\u7EA7\u540E daemon \u4E0D\u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09" }
12685
+ );
12686
+ }
12687
+ }
12688
+ if (activeTasks > 0) {
12689
+ throw new CliError(
12690
+ "TASKS_RUNNING",
12691
+ EXIT.CONFLICT,
12692
+ `daemon \u6709 ${activeTasks} \u4E2A\u4EFB\u52A1\u6267\u884C\u4E2D\uFF0C\u62D2\u7EDD\u5347\u7EA7\uFF08\u5347\u7EA7\u4F1A\u7834\u574F\u6B63\u5728\u6267\u884C\u7684\u4EFB\u52A1\uFF09`,
12693
+ false,
12694
+ { hint: "\u7B49\u4EFB\u52A1\u5B8C\u6210\u540E\u518D\u5347\u7EA7\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12695
+ );
12696
+ }
12697
+ const report = await performRuntimeUpgrade({
12698
+ currentVersion: BUILD_VERSION2,
12699
+ targetVersion,
12700
+ registry: registry2,
12701
+ log: (line) => context.err(line)
12702
+ });
12703
+ if (report.status === "up_to_date") {
12704
+ await reportUpgradeResult(context, { status: "succeeded", fromVersion: BUILD_VERSION2, toVersion: BUILD_VERSION2, restart: false });
12705
+ return { ok: true, upToDate: true, current: BUILD_VERSION2, target: report.targetVersion, steps: report.steps };
12706
+ }
12707
+ if (report.status !== "succeeded") {
12708
+ await reportUpgradeResult(context, { status: report.status, fromVersion: report.fromVersion, toVersion: report.toVersion ?? void 0, error: report.error });
12709
+ context.setExitCode(EXIT.OTHER);
12710
+ return {
12711
+ ok: false,
12712
+ status: report.status,
12713
+ from: report.fromVersion,
12714
+ to: report.toVersion,
12715
+ error: report.error,
12716
+ steps: report.steps,
12717
+ hint: report.status === "rolled_back" ? "\u5DF2\u56DE\u88C5\u539F\u7248\u672C\uFF1B\u53EF\u67E5\u770B steps \u5B9A\u4F4D\u5931\u8D25\u539F\u56E0\u540E\u91CD\u8BD5" : "\u539F\u7248\u672C\u4FDD\u6301\u4E0D\u53D8\uFF1B\u53EF\u67E5\u770B steps \u5B9A\u4F4D\u5931\u8D25\u539F\u56E0\u540E\u91CD\u8BD5"
12718
+ };
12719
+ }
12720
+ const cfg = loadDaemonConfig(context.store);
12721
+ let restart = null;
12722
+ let deferredRestart = false;
12723
+ if (wasRunning) {
12724
+ if (cfgForSafety?.runtimeId && context.runtime.token) {
12725
+ const stillBusy = await countActiveTasks(context, cfgForSafety.runtimeId);
12726
+ if (stillBusy > 0) {
12727
+ deferredRestart = true;
12728
+ context.err(`[upgrade] daemon \u6709 ${stillBusy} \u4E2A\u4EFB\u52A1\u5728\u5347\u7EA7\u671F\u95F4\u88AB\u6D3E\u53D1\uFF0C\u4FDD\u7559\u5176\u8FD0\u884C\u2014\u2014\u65B0\u7248\u672C ${report.toVersion} \u5DF2\u5B89\u88C5\uFF0C\u4E0B\u6B21 daemon \u91CD\u542F\u65F6\u751F\u6548\uFF08morula-runtime daemon stop && morula-runtime daemon start\uFF09`);
12729
+ }
12730
+ }
12731
+ if (!deferredRestart) {
12732
+ if (probe.pid !== null) {
12733
+ const stopped = await stopDaemonByPid(probe.pid);
12734
+ context.err(`[upgrade] \u65E7 daemon\uFF08pid=${probe.pid}\uFF09\u5DF2\u505C\u6B62\uFF08${stopped.outcome}\uFF09`);
12735
+ }
12736
+ if (probe.lockState === "stale")
12737
+ releaseDaemonLock(lockPath);
12738
+ else if (probe.pid !== null)
12739
+ releaseDaemonLock(lockPath, probe.pid);
12740
+ rmSync11(pidFile, { force: true });
12741
+ mkdirSync17(dir, { recursive: true, mode: 448 });
12742
+ restart = await spawnUpgradedDaemon({
12743
+ env: context.env,
12744
+ logFile: join24(dir, LOG_FILE2),
12745
+ pidFile,
12746
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
12747
+ });
12748
+ if (restart.pid > 0) {
12749
+ const liveness = await restart.waitExit(RESTART_LIVENESS_CHECK_MS);
12750
+ if (liveness.exited) {
12751
+ context.err(`[upgrade] \u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF09\uFF0C\u8BE6\u89C1 ${join24(dir, LOG_FILE2)}\u2014\u2014\u8BF7\u68C0\u67E5\u65E5\u5FD7\u540E\u624B\u52A8 morula-runtime daemon start`);
12752
+ restart = { pid: 0, entry: restart.entry, error: `\u65B0 daemon \u8FDB\u7A0B\u7ACB\u5373\u9000\u51FA\uFF08code=${liveness.code ?? "\u672A\u77E5"}\uFF09`, waitExit: async () => ({ exited: true, code: liveness.code ?? null }) };
12753
+ } else {
12754
+ context.err(`[upgrade] \u65B0 daemon \u5DF2\u62C9\u8D77\uFF08pid=${restart.pid}\uFF09\uFF0C\u65E5\u5FD7: ${join24(dir, LOG_FILE2)}`);
12755
+ }
12756
+ } else {
12757
+ context.err(`[upgrade] \u65B0 daemon \u62C9\u8D77\u5931\u8D25\uFF1A${restart.error ?? "\u672A\u77E5\u539F\u56E0"}\u2014\u2014\u8BF7\u624B\u52A8\u6267\u884C morula-runtime daemon start`);
12758
+ }
12759
+ }
12760
+ } else {
12761
+ context.err("[upgrade] daemon \u672A\u5728\u8FD0\u884C\uFF0C\u4FDD\u6301\u505C\u6B62\u72B6\u6001\uFF08\u9700\u8981\u65F6\u624B\u52A8 morula-runtime daemon start\uFF09");
12762
+ }
12763
+ const spawnFailed = wasRunning && !deferredRestart && restart !== null && restart.pid <= 0;
12764
+ await reportUpgradeResult(context, {
12765
+ status: spawnFailed ? "failed" : "succeeded",
12766
+ fromVersion: report.fromVersion,
12767
+ toVersion: report.toVersion,
12768
+ restart: !spawnFailed && wasRunning && !deferredRestart,
12769
+ ...spawnFailed ? { error: `\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\u4F46\u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${restart?.error ?? "\u672A\u77E5\u539F\u56E0"}` } : {}
12770
+ });
12771
+ if (spawnFailed) {
12772
+ context.setExitCode(EXIT.OTHER);
12773
+ }
12774
+ return {
12775
+ ok: !spawnFailed,
12776
+ status: spawnFailed ? "restart_failed" : deferredRestart ? "installed_pending_restart" : "succeeded",
12777
+ from: report.fromVersion,
12778
+ to: report.toVersion,
12779
+ // deferredRestart(评审 P3):安装期有任务被派发 → 不打断 daemon,新版本下次重启生效
12780
+ restarted: !spawnFailed && wasRunning && !deferredRestart,
12781
+ pid: !spawnFailed && wasRunning && !deferredRestart ? restart?.pid ?? null : null,
12782
+ ...deferredRestart ? { note: "\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\uFF1Bdaemon \u6709\u4EFB\u52A1\u5728\u6267\u884C\u672A\u91CD\u542F\uFF0C\u4E0B\u6B21 daemon \u91CD\u542F\u65F6\u751F\u6548" } : {},
12783
+ warnings: report.warnings,
12784
+ steps: report.steps
12785
+ };
12786
+ }
12787
+
12077
12788
  // src/commands/registry-runtime.ts
12078
12789
  var COMMANDS = {
12079
12790
  auth: authCommand,
12080
12791
  daemon: daemonCommand,
12081
- setup: setupCommand
12792
+ setup: setupCommand,
12793
+ upgrade: upgradeCommand
12082
12794
  };
12083
12795
  var EXIT_CODES = {
12084
12796
  0: "success",
@@ -12100,7 +12812,7 @@ var GLOBAL_FLAGS2 = {
12100
12812
  };
12101
12813
  var SPEC = {
12102
12814
  name: "morula-runtime",
12103
- summary: "Device-level CLI for the Morula local agent runtime: authentication, daemon lifecycle (run/start/stop/status), local assistant portal, model inventory, and device diagnostics. Success output is JSON on stdout; errors are JSON on stderr.",
12815
+ summary: "Device-level CLI for the Morula local agent runtime: authentication, daemon lifecycle (run/start/stop/status), local assistant portal, model inventory, device diagnostics, and runtime self-upgrade (check + confirmed run). Success output is JSON on stdout; errors are JSON on stderr.",
12104
12816
  resources: [
12105
12817
  {
12106
12818
  resource: "auth",
@@ -12271,6 +12983,44 @@ var SPEC = {
12271
12983
  "canonical form is the bare `morula-runtime setup`; postinstall runs the same registration automatically",
12272
12984
  "only writes <root>/morula-cli; never deletes other plugins or the legacy ~/.agents/plugins/morula-cli (reported as legacyRoot)"
12273
12985
  ]
12986
+ },
12987
+ {
12988
+ resource: "upgrade",
12989
+ summary: "runtime self-upgrade: compare the installed version against the latest npm release, then upgrade this device after explicit confirmation (installs, verifies, rolls back on failure, and re-bakes the plugin launcher)",
12990
+ actions: [
12991
+ {
12992
+ action: "check",
12993
+ summary: "read-only: compare current version with the latest published version on the npm registry (one network call)",
12994
+ flags: [
12995
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to query (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
12996
+ ],
12997
+ examples: ["morula-runtime upgrade check"],
12998
+ writes: "none",
12999
+ interactive: false,
13000
+ idempotent: true,
13001
+ emits: ["ok", "current", "latest", "updateAvailable", "registry"]
13002
+ },
13003
+ {
13004
+ action: "run",
13005
+ summary: "upgrade this device to the target version (default: registry latest). REQUIRES --yes (no silent auto-upgrade). Refuses while the daemon has tasks running; stops an idle daemon, upgrades, verifies (rolls back on failure), re-runs setup, then restarts the daemon",
13006
+ flags: [
13007
+ { name: "yes", type: "boolean", description: "REQUIRED: explicit confirmation; without it the command refuses (exit 2). No silent auto-upgrade" },
13008
+ { name: "version", type: "string", valueHint: "<semver>", description: "target version (default: latest from the registry)" },
13009
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to install from (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
13010
+ ],
13011
+ examples: ["morula-runtime upgrade run --yes", "morula-runtime upgrade run --yes --version 0.1.0-alpha.16"],
13012
+ writes: "local",
13013
+ interactive: false,
13014
+ idempotent: false,
13015
+ emits: ["ok", "status", "from", "to", "restarted", "pid", "steps"]
13016
+ }
13017
+ ],
13018
+ notes: [
13019
+ "check: exit 0 = compared (see updateAvailable); 7 = registry unreachable (see error/hint)",
13020
+ "run: exit 0 = upgraded or already up to date; 6 (conflict) = daemon has running tasks (upgrade refused); 1 = install/verify failed (previous version kept or re-installed, see steps/error)",
13021
+ "the daemon restart only happens when the daemon was running before the upgrade; a stopped daemon stays stopped",
13022
+ "a stopped daemon is upgraded but not started \u2014 start it manually with morula-runtime daemon start"
13023
+ ]
12274
13024
  }
12275
13025
  ]
12276
13026
  };