@arcships/morula-runtime 0.1.0-alpha.15 → 0.1.0-alpha.17

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 = "20260924065153.0fc1a372";
743
+ BUILD_ID = "20260930065011.80ac5142";
744
744
  BUILD_CONTEXTS = {
745
745
  "local": "http://localhost:8080/api",
746
746
  "test": "https://morula.basecastle.com/api",
@@ -841,13 +841,6 @@ var init_attachment = __esm({
841
841
  getDownloadUrl(id) {
842
842
  return this.client.get(`/attachments/${encodeURIComponent(id)}/download-url`);
843
843
  }
844
- /**
845
- * 读取附件的解析产物全文(MinerU markdown)。
846
- * 未解析/解析中/解析失败由服务端在返回体里说明,不当作 HTTP 错误。
847
- */
848
- content(id, maxChars) {
849
- return this.client.get(`/attachments/${encodeURIComponent(id)}/content`, { maxChars });
850
- }
851
844
  /** 更新附件关联的资源 ID */
852
845
  updateResourceId(id, resourceId) {
853
846
  return this.client.patch(`/attachments/${encodeURIComponent(id)}`, { resourceId });
@@ -2588,9 +2581,9 @@ async function setupAutostart(context) {
2588
2581
  await removeLegacyAutostart();
2589
2582
  const { command, args } = resolvePortalCommand();
2590
2583
  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");
2584
+ const daemonDir = join3(stateDir2, "daemon");
2585
+ mkdirSync2(daemonDir, { recursive: true, mode: 448 });
2586
+ const logFile = join3(daemonDir, "portal.log");
2594
2587
  const plist = buildLaunchAgentPlist({ label: LABEL, command, args, logFile });
2595
2588
  const plistPath = launchAgentPath();
2596
2589
  mkdirSync2(join3(plistPath, ".."), { recursive: true });
@@ -2626,7 +2619,13 @@ var init_portal_autostart = __esm({
2626
2619
  // src/daemon/daemon-config.ts
2627
2620
  import { randomUUID } from "node:crypto";
2628
2621
  import { homedir as homedir4 } from "node:os";
2629
- import { join as join5 } from "node:path";
2622
+ import { dirname as dirname4, join as join5 } from "node:path";
2623
+ function configFilePathForEnv(env = process.env) {
2624
+ return env.MORULA_CONFIG ?? join5(env.MORULA_CONFIG_DIR ?? join5(homedir4(), ".morula-cli"), "config.yaml");
2625
+ }
2626
+ function daemonDirForEnv(env = process.env) {
2627
+ return join5(dirname4(configFilePathForEnv(env)), "daemon");
2628
+ }
2630
2629
  function loadDaemonConfig(store) {
2631
2630
  const config = store.load();
2632
2631
  const d = config?.[DAEMON_KEY];
@@ -2666,7 +2665,7 @@ var init_daemon_config = __esm({
2666
2665
  // src/daemon/daemon-lock.ts
2667
2666
  import { execFileSync as execFileSync3 } from "node:child_process";
2668
2667
  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";
2668
+ import { dirname as dirname5, join as join6 } from "node:path";
2670
2669
  function daemonPidPath(configDir) {
2671
2670
  return join6(configDir, "daemon", DAEMON_PID_FILE);
2672
2671
  }
@@ -2769,7 +2768,7 @@ function acquireDaemonLock(lockPath, input) {
2769
2768
  startedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
2770
2769
  workdir: input.workdir
2771
2770
  };
2772
- mkdirSync4(dirname4(lockPath), { recursive: true, mode: 448 });
2771
+ mkdirSync4(dirname5(lockPath), { recursive: true, mode: 448 });
2773
2772
  try {
2774
2773
  const fd = openSync2(lockPath, "wx", 384);
2775
2774
  try {
@@ -2854,7 +2853,7 @@ __export(portal_instances_exports, {
2854
2853
  });
2855
2854
  import { readdirSync, statSync } from "node:fs";
2856
2855
  import { homedir as homedir5 } from "node:os";
2857
- import { dirname as dirname5, join as join7 } from "node:path";
2856
+ import { dirname as dirname6, join as join7 } from "node:path";
2858
2857
  function parseBaseUrl(raw) {
2859
2858
  if (!raw)
2860
2859
  return null;
@@ -2922,7 +2921,7 @@ function createPortalEnvironmentResolver(options = {}) {
2922
2921
  const env = options.env ?? process.env;
2923
2922
  const log = options.log ?? (() => {
2924
2923
  });
2925
- const defaultConfigDir = dirname5(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2924
+ const defaultConfigDir = dirname6(env.MORULA_CONFIG ?? join7(env.MORULA_CONFIG_DIR ?? join7(options.homeDir ?? homedir5(), ".morula-cli"), "config.yaml"));
2926
2925
  let index = [];
2927
2926
  let signature = "";
2928
2927
  function buildIndex() {
@@ -2939,7 +2938,7 @@ function createPortalEnvironmentResolver(options = {}) {
2939
2938
  const normalized = normalizeBaseUrl2(entry.url);
2940
2939
  if (!normalized)
2941
2940
  continue;
2942
- const configDir = dirname5(file);
2941
+ const configDir = dirname6(file);
2943
2942
  built.push({
2944
2943
  configDir,
2945
2944
  context,
@@ -3482,7 +3481,7 @@ var init_dim_locate = __esm({
3482
3481
  // src/daemon/acp/acp-process-registry.ts
3483
3482
  import { execFileSync as execFileSync4 } from "node:child_process";
3484
3483
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
3485
- import { dirname as dirname6 } from "node:path";
3484
+ import { dirname as dirname7 } from "node:path";
3486
3485
  function parseLstartToMs(tokens) {
3487
3486
  if (tokens.length < 5)
3488
3487
  return null;
@@ -3595,7 +3594,7 @@ function persistRecords() {
3595
3594
  if (!recordFile)
3596
3595
  return;
3597
3596
  try {
3598
- mkdirSync5(dirname6(recordFile), { recursive: true });
3597
+ mkdirSync5(dirname7(recordFile), { recursive: true });
3599
3598
  writeFileSync5(recordFile, JSON.stringify({
3600
3599
  v: RECORD_FILE_VERSION,
3601
3600
  children: [...ownedChildren.values()]
@@ -3838,9 +3837,9 @@ var init_acp_connection = __esm({
3838
3837
  process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
3839
3838
  `);
3840
3839
  try {
3841
- const { existsSync: existsSync19 } = await import("node:fs");
3840
+ const { existsSync: existsSync21 } = await import("node:fs");
3842
3841
  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 ?? "-"}
3842
+ `[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
3843
  `
3845
3844
  );
3846
3845
  } catch {
@@ -4424,14 +4423,13 @@ init_dist2();
4424
4423
  init_dist2();
4425
4424
 
4426
4425
  // src/build-env.ts
4427
- var BUILD_VERSION2 = "0.1.0-alpha.15";
4426
+ var BUILD_VERSION2 = "0.1.0-alpha.17";
4428
4427
 
4429
4428
  // src/commands/daemon.ts
4430
4429
  init_dist();
4431
4430
  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";
4431
+ import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync12, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
4432
+ import { dirname as dirname13, join as join23 } from "node:path";
4435
4433
 
4436
4434
  // src/daemon/daemon-autostart.ts
4437
4435
  init_portal_autostart();
@@ -4500,9 +4498,9 @@ init_portal_server();
4500
4498
  // src/daemon/runner.ts
4501
4499
  init_dist2();
4502
4500
  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";
4501
+ import { existsSync as existsSync17, mkdirSync as mkdirSync15, statSync as statSync4, writeFileSync as writeFileSync13 } from "node:fs";
4504
4502
  import { arch, hostname } from "node:os";
4505
- import { dirname as dirname9, join as join20 } from "node:path";
4503
+ import { dirname as dirname11, join as join21 } from "node:path";
4506
4504
 
4507
4505
  // src/daemon/acp/acp-connection-pool.ts
4508
4506
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
@@ -4637,6 +4635,10 @@ var DaemonApi = class {
4637
4635
  * D1(daemon-server-connection-unstable):心跳是「设备存活」的唯一信号,显式 10s 超时——
4638
4636
  * 慢请求快速失败,避免占住单飞槽位(客户端默认 30s,对心跳过松)。
4639
4637
  * 说明:`client.post()` 的第三参是 query,不支持 RequestOptions,故直接走 `client.request`。
4638
+ *
4639
+ * `upgradeRequested`(runtime-self-upgrade,沿用 stop 的一次性认领模式):服务端置了
4640
+ * upgradeRequestedAt → 本次心跳认领(服务端已清指令)→ daemon 触发自升级;`upgradeVersion`
4641
+ * 为置指令时的目标版本(可空 = daemon 自行解析 latest)。
4640
4642
  */
4641
4643
  heartbeat(runtimeId, runningTaskCount = 0) {
4642
4644
  return this.client.request(
@@ -4647,6 +4649,13 @@ var DaemonApi = class {
4647
4649
  pollTasks(runtimeId, max = 1) {
4648
4650
  return this.client.get("/agent-tasks/poll", { runtimeId, max });
4649
4651
  }
4652
+ /**
4653
+ * runtime-self-upgrade:自升级结果回报(机器协议,设备级凭证)。
4654
+ * best-effort 语义由调用方掌握(失败只 log,不影响本地升级结果)。
4655
+ */
4656
+ reportUpgradeResult(runtimeId, body) {
4657
+ return this.client.post(`/agent-runtimes/${runtimeId}/upgrade-result`, body);
4658
+ }
4650
4659
  claim(taskId, runtimeId) {
4651
4660
  const res = this.client.post(`/agent-tasks/${taskId}/claim`, void 0, { runtimeId });
4652
4661
  res.then((r) => {
@@ -5205,8 +5214,9 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5205
5214
  - **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
5206
5215
  \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
5207
5216
  - **\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
5208
- - **\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
5209
- **\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
5217
+ - **\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
5218
+ \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
5219
+ \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
5210
5220
  \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
5211
5221
  \u4E3B\u4ED3\u5E93\u6700\u7EC8\u505C\u5728\u6700\u540E\u90A3\u4E2A\u300C\u53EA\u770B\u4E86\u4E00\u773C\u300D\u7684\u4ED3\u5E93\u4E0A\uFF09\u3002
5212
5222
  - **\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
@@ -5241,8 +5251,9 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
5241
5251
  morula repo checkout <taskId> <repoFullName> [--ref <branch|sha>] # \u62C9\u53D6/\u5207\u6362\u4ED3\u5E93\uFF08\u8FD4\u56DE\u7269\u5316\u8DEF\u5F84\uFF0C\u89C1 0.5\uFF09
5242
5252
  morula repo prepare <taskId> <repoFullName> # \u591A\u4ED3\u5E93\u4EFB\u52A1\uFF1A\u7269\u5316\u5176\u4ED6\u4ED3\u5E93\u5230\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\u4E0B\uFF08\u89C1 0.5\uFF09
5243
5253
  morula comment list --object <objectId> # \u8BFB\u8BC4\u8BBA\uFF08\u4EFB\u52A1=objectId\uFF09
5254
+ morula comment add --object <objectId> --content <\u6587\u672C> # \u53D1\u8BC4\u8BBA\uFF08\u6700\u7EC8\u603B\u7ED3\u52A0 --final\uFF0C\u89C1\u7B2C 5 \u6761\uFF09
5244
5255
  morula task inbox [--peek] # \u8FD0\u884C\u4E2D\u68C0\u67E5\u70B9\uFF1A\u53D6\u672C\u7EBF\u7A0B\u672A\u6D88\u8D39\u6D88\u606F\uFF0C\u7EB3\u5165\u672C\u56DE\u5408\uFF08\u89C1 1.3\uFF09
5245
- # \u6CE8\uFF1A\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\uFF08\u4F5C\u8005=\u667A\u80FD\u4F53\uFF09\uFF0C\u4E0D\u8981\u8C03\u7528 comment add \u81EA\u884C\u7F6E\u8BC4\u3002
5256
+ # \u6CE8\uFF1A\u4EFB\u52A1\u6536\u5C3E\u5148\u53D1 final \u603B\u7ED3\u8BC4\u8BBA\uFF08morula comment add --final\uFF09\uFF0C\u518D\u4E0A\u62A5\u7ED3\u679C\uFF1B\u670D\u52A1\u7AEF\u53EA\u5728\u4F60\u6CA1\u53D1\u65F6\u515C\u5E95\u4EE3\u53D1\u4E00\u6761\u3002
5246
5257
  \`\`\`
5247
5258
  - **\u53EF\u7528\u4ED3\u5E93\u6E05\u5355**\uFF1A\u7CFB\u7EDF\u63D0\u793A**\u4E0D\u518D\u5217\u51FA**\u4ED3\u5E93\u2014\u2014\u9700\u8981\u679A\u4E3E\u7A7A\u95F4\u7ED1\u5B9A\u7684\u4ED3\u5E93\u65F6\u7528
5248
5259
  \`morula repo list --space <spaceId>\`\uFF08\`<spaceId>\` \u89C1\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF0C\u6216 \`morula task get\` \u8FD4\u56DE\u7684 \`contextSnapshot.spaceId\`\uFF09\u3002
@@ -5372,26 +5383,34 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
5372
5383
  - PR \u521B\u5EFA\u88AB\u963B\u585E\uFF08\u6743\u9650/\u6D4B\u8BD5\u5931\u8D25/\u8FDC\u7AEF\u72B6\u6001\u5F02\u5E38\uFF09\u2192 \u5728\u6700\u7EC8\u603B\u7ED3\u4E2D**\u62A5\u544A\u963B\u585E\u539F\u56E0**\uFF0C\u4E0D\u8981\u5047\u88C5\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF1B
5373
5384
  - **\u4E0D\u8981**\u5207\u56DE \`main\` \u6216 \`master\` \u76F4\u63A5\u63D0\u4EA4\u3002
5374
5385
 
5375
- ## 4. \u72B6\u6001\u53D8\u66F4\u7531\u670D\u52A1\u7AEF\u9A71\u52A8\uFF0C\u4E0D\u8981\u624B\u52A8\u7FFB\u72B6\u6001
5386
+ ## 4. \u72B6\u6001\u4E0E\u4EFB\u52A1\u64CD\u4F5C\uFF1A\u7ED3\u8BBA\u4F18\u5148\uFF0C\u52A8\u4F5C\u4E3A\u8F85
5376
5387
 
5377
- - \u4EFB\u52A1\u72B6\u6001\u63A8\u8FDB\uFF08\u5982\u5B8C\u6210\uFF09\u7531 PR merge \u4E8B\u4EF6\u81EA\u52A8\u89E6\u53D1\uFF08\u914D\u5408\u7B2C 2 \u6761\u7684\u5173\u5355\u5199\u6CD5\uFF09\u3002
5378
- - \u4E0D\u8981\u76F4\u63A5\u8C03 CLI \u6539\u4EFB\u52A1\u72B6\u6001\uFF1B\u628A\u72B6\u6001\u6D41\u8F6C\u4EA4\u7ED9\u670D\u52A1\u7AEF\u3002
5388
+ - **\u72B6\u6001\u63A8\u8FDB\u7531\u7ED3\u8BBA\u534F\u8BAE\u4E0E\u4E8B\u5B9E\u4E8B\u4EF6\u9A71\u52A8**\uFF1A\u5206\u6790\u4EFB\u52A1\u4E0A\u62A5 \`analysis-verdict\`\u3001\u8BC4\u5BA1\u4EFB\u52A1\u7528 \`morula review submit\`\u3001\u5408\u5E76/\u53D1\u5E03\u7531 PR \u4E8B\u4EF6\u9A71\u52A8\u2014\u2014**\u4E0D\u8981**\u7528\u72B6\u6001\u52A8\u4F5C\u66FF\u4EE3\u7ED3\u8BBA\u534F\u8BAE\u3002
5389
+ - **\u4F60\u53EF\u4EE5\u5728\u4EFB\u52A1\u4E0A\u505A\u57FA\u7840\u64CD\u4F5C**\uFF08\u50CF\u4EBA\u4E00\u6837\uFF0C\u4F5C\u7528\u57DF\u4EC5\u9650\u672C\u4EFB\u52A1\uFF09\uFF1A
5390
+ - \`morula task state <taskId> --state <in_progress|review> [--reason <\u6587\u672C>]\`\uFF1A\u5728\u300C\u5F00\u53D1\u4E2D / \u5F85\u8BC4\u5BA1\u300D\u8303\u56F4\u5185\u81EA\u63A8\u72B6\u6001\uFF1B**\u5F85\u8BC4\u5BA1 \u2192 \u5F00\u53D1\u4E2D = \u9000\u56DE\u5F00\u53D1**\u2014\u2014\u5E73\u53F0\u4F1A\u81EA\u52A8\u56DE\u9000\u72B6\u6001\u3001\u8BB0\u8BC4\u5BA1\u8F6E\u6B21\u5E76\u6D3E\u53D1\u4FEE\u590D\u4EFB\u52A1\uFF08\u540C\u4E00 PR \u8FED\u4EE3\uFF09\uFF0C**\u4E0D\u9700\u8981\u3001\u4E5F\u4E0D\u5E94\u8BE5**\u518D\u81EA\u5DF1 @ \u5F00\u53D1\u6267\u884C\u5668\uFF1B\`--reason\` \u5199\u660E\u9000\u56DE\u539F\u56E0\uFF08\u4F1A\u8FDB\u4FEE\u590D\u4EFB\u52A1 prompt\uFF09\u3002
5391
+ - \`morula comment add\`\uFF1A\u89C1\u7B2C 5 \u6761\u8BC4\u8BBA\u7EAA\u5F8B\u3002
5392
+ - \`morula task assign <taskId> --to <principalId>\`\uFF1A\u6307\u6D3E\u672C\u4EFB\u52A1\u8D1F\u8D23\u4EBA\uFF08\u672C\u7A7A\u95F4\u6210\u5458\u6216\u9636\u6BB5\u6267\u884C\u5668\uFF1B\u76EE\u6807\u4E0D\u5B58\u5728\u4F1A\u663E\u5F0F\u62A5\u9519\uFF09\u3002
5393
+ - **\u7EC8\u6001\uFF08\u5DF2\u5408\u5E76/\u5DF2\u53D1\u5E03/\u5DF2\u5173\u95ED\uFF09\u4E0E\u9700\u6C42\u6C60\u72B6\u6001\u4E0D\u7531\u4F60\u58F0\u660E**\uFF1B\u540C\u6001\u91CD\u590D\u63A8\u9001\u662F\u5E42\u7B49\u7684\uFF08\u65E0\u5BB3\uFF09\u3002
5379
5394
 
5380
5395
  ## 4.1 \u5206\u6790\u4EFB\u52A1\uFF1A\u5FC5\u987B\u4E0A\u62A5\u5206\u6790\u7ED3\u8BBA\uFF08verdict\uFF09
5381
5396
 
5382
5397
  > **\u4EC5\u5F53\u672C\u4EFB\u52A1\u662F\u300C\u5F85\u5206\u6790\u300D\u4EFB\u52A1\u65F6\u9002\u7528**\uFF08\u4EFB\u52A1\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\u9636\u6BB5\u3001\u8981\u6C42\u505A\u9700\u6C42\u5206\u6790\u4E0E\u65B9\u6848\u62C6\u89E3\u3001\u4E0D\u5199\u4EE3\u7801\uFF09\u3002
5383
5398
  > \u5176\u4F59\u4EFB\u52A1\uFF08\u5F00\u53D1 / \u8BC4\u5BA1 / \u8C03\u7814 / \u95EE\u7B54\uFF09\u8DF3\u8FC7\u672C\u8282\uFF0C\u4E0D\u8981\u8C03\u7528\u8BE5\u547D\u4EE4\u3002
5384
5399
 
5385
- - \u5206\u6790\u7ED3\u8BBA\u5206\u4E24\u90E8\u5206\uFF0C**\u90FD\u8981\u7ED9**\uFF1A
5386
- 1. **\u4EBA\u770B\u7684**\uFF1A\u5206\u6790\u7ED3\u8BBA\u5199\u8FDB\u6700\u7EC8\u8F93\u51FA\uFF08\u9700\u6C42\u662F\u5426\u6E05\u6670\u3001\u600E\u4E48\u505A\u3001\u8303\u56F4\u4E0E\u98CE\u9669\u3001\u9A8C\u6536\u70B9\uFF09\u2014\u2014\u670D\u52A1\u7AEF\u4F5C\u4E3A\u4EFB\u52A1\u8BC4\u8BBA\u53D1\u5E03\uFF1B
5387
- 2. **\u673A\u5668\u5224\u5B9A\u7684**\uFF1A\u6267\u884C \`morula task analysis-verdict <taskId> --verdict ready|blocked [--reason "\u2026"]\` \u4E0A\u62A5\u7ED3\u8BBA\u3002
5400
+ - \u5206\u6790\u7ED3\u8BBA\u5206\u4E24\u90E8\u5206\uFF0C**\u90FD\u8981\u7ED9**\uFF08\u987A\u5E8F\uFF1A\u5148\u53D1\u8BC4\u8BBA\uFF0C\u518D\u4E0A\u62A5 verdict\uFF09\uFF1A
5401
+ 1. **\u4EBA\u770B\u7684**\uFF1A\u7528**\u4F60\u81EA\u5DF1\u7684\u8BDD**\u628A\u5206\u6790\u7ED3\u8BBA\u53D1\u6210\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u2014\u2014\u9700\u6C42\u662F\u5426\u6E05\u6670\u3001\u600E\u4E48\u505A\u3001\u8303\u56F4\u4E0E\u98CE\u9669\u3001\u9A8C\u6536\u70B9\uFF1A
5402
+ \`\`\`bash
5403
+ morula comment add --object <objectId> --final --content <\u4F60\u7684\u5206\u6790\u7ED3\u8BBA>
5404
+ \`\`\`
5405
+ \u8FD9\u662F\u7528\u6237\u770B\u5230\u7684\u5206\u6790\u4EA4\u4ED8\uFF1A\u7528\u81EA\u5DF1\u7684\u8BDD\u5199\uFF0C\u4E0D\u8981\u5957\u56FA\u5B9A\u683C\u5F0F\uFF0C\u66F4\u4E0D\u8981\u590D\u8FF0\u4EFB\u52A1\u63CF\u8FF0\uFF1B\u7ED3\u8BBA\u5148\u8BF4\u3001\u7406\u7531\u8DDF\u4E0A\u3002
5406
+ 2. **\u673A\u5668\u5224\u5B9A\u7684**\uFF1A\u6267\u884C \`morula task analysis-verdict <taskId> --verdict ready|blocked [--reason "\u2026"]\` \u4E0A\u62A5\u7ED3\u8BBA\uFF08\`--reason\` \u53EA\u9700\u4E00\u53E5\u8BDD\uFF0C\u8BE6\u7EC6\u5185\u5BB9\u5728\u8BC4\u8BBA\u91CC\uFF09\u3002
5388
5407
  - \`<taskId>\` = \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u8FD4\u56DE\u7684 \`id\`\uFF08\u672C\u4EFB\u52A1 AgentTask UUID\uFF09\u3002
5389
5408
  - **\`ready\`** = \u9700\u6C42\u6E05\u6670\u3001\u53EF\u76F4\u63A5\u5F00\u53D1\uFF08\`--reason\` \u53EF\u5199\u4E00\u53E5\u8BDD\u8303\u56F4\u6458\u8981\uFF09\uFF1B
5390
5409
  **\`blocked\`** = \u4E0D\u53EF\u505A / \u9700\u6F84\u6E05\uFF08**\u5FC5\u987B**\u5728 \`--reason\` \u6216\u6700\u7EC8\u8F93\u51FA\u91CC\u5199\u6E05\u963B\u585E\u70B9\u4E0E\u9700\u8981\u6F84\u6E05\u7684\u95EE\u9898\uFF09\u3002
5391
5410
  - **\u670D\u52A1\u7AEF\u636E\u6B64\u81EA\u52A8\u63A8\u8FDB\uFF0C\u4E0D\u9700\u8981\u4EFB\u4F55\u4EBA\u70B9\u786E\u8BA4**\uFF1A\`ready\` \u2192 \u4EFB\u52A1\u81EA\u52A8\u8FDB\u5165\u300C\u5F00\u53D1\u4E2D\u300D\u5E76\u6D3E\u53D1\u5F00\u53D1\u4EFB\u52A1\uFF1B
5392
5411
  \`blocked\` / \u672A\u4E0A\u62A5 \u2192 \u4EFB\u52A1**\u505C\u5728\u300C\u5F85\u5206\u6790\u300D**\uFF0C\u7531\u7528\u6237\u8865\u5145\u9700\u6C42\u540E\u91CD\u6D3E\u5206\u6790\u3002
5393
5412
  - **\u5FC5\u987B\u4E0A\u62A5**\uFF1A\u6F0F\u62A5\u4F1A\u8BA9\u4EFB\u52A1\u9759\u9ED8\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\uFF1B\u540C\u4E00\u4EFB\u52A1\u53EA\u9700\u4E0A\u62A5\u4E00\u6B21\uFF08\u91CD\u590D\u4E0A\u62A5\u4EE5\u9996\u6B21\u4E3A\u51C6\uFF0C\u4E0D\u4F1A\u6539\u5199\uFF09\u3002
5394
- - **\u4E0D\u8981\u624B\u52A8\u6D41\u8F6C\u672C\u4EFB\u52A1\u72B6\u6001**\uFF08\u4E0D\u8981\u8C03 \`morula task state\`\uFF09\uFF1A\u5206\u6790\u4EFB\u52A1\u7684\u63A8\u8FDB\u7531\u670D\u52A1\u7AEF\u6309 verdict \u51B3\u5B9A\u3002
5413
+ - \u5206\u6790\u4EFB\u52A1\u7684\u63A8\u8FDB\u7531\u670D\u52A1\u7AEF\u6309 verdict \u51B3\u5B9A\u2014\u2014**\u4E0D\u8981\u7528\u72B6\u6001\u52A8\u4F5C\u66FF\u4EE3\u5206\u6790\u7ED3\u8BBA**\uFF08\u4E0D\u8981\u8C03 \`morula task state\`\uFF09\u3002
5395
5414
 
5396
5415
  ## 4.2 \u8BC4\u5BA1\u4EFB\u52A1\uFF1A\u53EA\u8BFB\u8BC4\u5BA1 + \u5FC5\u987B\u56DE\u4F20\u7ED3\u8BBA\uFF08verdict\uFF09
5397
5416
 
@@ -5412,9 +5431,11 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
5412
5431
  - \`<objectId>\` = \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u8FD4\u56DE\u7684 \`objectId\`\uFF1B
5413
5432
  - \`--verdict\`\uFF1A\`approved\`\uFF08\u901A\u8FC7\uFF09/ \`changes_requested\`\uFF08\u4E0D\u901A\u8FC7\uFF0C**\u5FC5\u987B**\u5728 \`--details\` \u5199\u660E\u5177\u4F53\u610F\u89C1\uFF1A\u6587\u4EF6/\u884C/\u95EE\u9898/\u5EFA\u8BAE\uFF0C\u5426\u5219\u5F00\u53D1\u65E0\u6CD5\u4FEE\u590D\uFF09/ \`commented\`\uFF08\u6709\u610F\u89C1\u4F46\u4E0D\u963B\u585E\uFF09\uFF1B
5414
5433
  - \`--pr-number\`\uFF1A\u8BC4\u5BA1\u5BF9\u8C61\u7684 PR \u7F16\u53F7\uFF08\u6709\u5219\u586B\uFF09\u3002
5434
+ - **\u8BC4\u5BA1\u4EFB\u52A1\u7684\u4EA4\u4ED8\u5C31\u662F\u8FD9\u4EFD\u7ED3\u8BBA**\uFF1A\u5E73\u53F0\u4F1A\u6309\u7ED3\u8BBA\u5199\u6743\u5A01\u8BC4\u8BBA\uFF08\u2705/\u{1F4AC}/\u{1F501}\uFF09\u2014\u2014**\u4E0D\u8981**\u518D\u53D1\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\uFF08\`--final\` \u4F1A\u88AB\u670D\u52A1\u7AEF\u62D2\u7EDD\uFF09\uFF0C\u4E5F\u4E0D\u8981\u5728\u8BC4\u8BBA\u533A\u590D\u8FF0\u7ED3\u8BBA\u3002
5435
+ - **\u7ED3\u8BBA\u65B0\u9C9C\u5EA6\uFF08\u65E0\u65B0\u63D0\u4EA4\u4E0D\u91CD\u590D\u6295 \`approved\`\uFF09**\uFF1A\u63D0\u4EA4\u7ED3\u8BBA\u524D\u5148\u6838\u5BF9\u88AB\u8BC4\u5BA1 MR \u7684 head commit \u4E0E\u4E0A\u4E00\u8F6E\u7ED3\u8BBA\u65F6\u662F\u5426\u76F8\u540C\uFF08\`morula repo mr list\` / \`mr-detail\`\uFF1B\u8BC4\u8BBA\u533A\u4E5F\u6709\u4E0A\u4E00\u8F6E\u7ED3\u8BBA\uFF09\u3002**\u76F8\u540C\u4E14\u65E0\u65B0\u63D0\u4EA4\u65F6\u4E0D\u8981\u91CD\u590D\u6295 \`approved\`**\uFF1A\u672C\u8F6E\u4EFB\u52A1\u4E0A\u4E0B\u6587\u91CC\u6709\u4EBA\u7684\u65B0\u8981\u6C42\uFF08\u5982\u8981\u6C42\u4FEE\u590D\u4E0A\u4E00\u8F6E\u610F\u89C1\uFF09\u2192 \u6309\u4EBA\u7684\u8981\u6C42\u5904\u7406\uFF08\u610F\u89C1\u672A\u4FEE\u590D\u5C31\u6295 \`changes_requested\`\uFF09\uFF1B\u786E\u5B9E\u65E0\u53EF\u8BC4\u5BA1\u5185\u5BB9 \u2192 \u6295 \`commented\` \u8BF4\u660E\u300C\u65E0\u65B0\u63D0\u4EA4\u300D\u5373\u53EF\u3002\u590D\u8FF0\u4E0A\u4E00\u8F6E\u7ED3\u8BBA\u5BF9\u4EFB\u52A1\u6CA1\u6709\u4EFB\u4F55\u63A8\u8FDB\u3002
5415
5436
  - **\u7981\u6B62\u8BC4\u5BA1\u81EA\u5DF1\u5F00\u53D1\u7684\u4EFB\u52A1**\uFF1A\u82E5\u5F85\u8BC4\u5BA1\u6539\u52A8\u7531\u4F60\u81EA\u5DF1\u63D0\u4EA4\uFF08\u540C\u4E00\u667A\u80FD\u4F53\u65E2\u5F00\u53D1\u53C8\u8BC4\u5BA1\uFF09\uFF0C\u4E0D\u4E0B\u7ED3\u8BBA\uFF0C\u76F4\u63A5\u62A5\u544A\u8BE5\u914D\u7F6E\u95EE\u9898\u3002
5416
- - **\u4E0D\u8981\u624B\u52A8\u6D41\u8F6C\u672C\u4EFB\u52A1\u72B6\u6001**\uFF08\u4E0D\u8981\u8C03 \`morula task state\`\uFF09\uFF1A\u8BC4\u5BA1\u7ED3\u8BBA\u7531\u670D\u52A1\u7AEF\u636E\u6B64\u63A8\u8FDB\u3002
5417
5437
  - **\u5FC5\u987B\u4E0A\u62A5**\uFF1A\u6F0F\u62A5\u4F1A\u8BA9\u4EFB\u52A1\u505C\u5728\u300C\u5F85\u8BC4\u5BA1\u300D\uFF0C\u5F00\u53D1\u65E0\u6CD5\u8FDB\u5165\u4E0B\u4E00\u8F6E\u3002
5438
+ - \u9000\u56DE\u5F00\u53D1**\u9996\u9009\u7ED3\u8BBA\u534F\u8BAE**\uFF1A\`--verdict changes_requested\`\uFF08\u5E26\u5177\u4F53\u610F\u89C1\uFF09\u4E0E\u670D\u52A1\u7AEF\u7ED3\u8BBA\u94FE\u8DEF\u5B8C\u5168\u7B49\u4EF7\uFF1B\`morula task state --state in_progress\` \u662F\u7B49\u4EF7\u7684\u66FF\u4EE3\u5165\u53E3\uFF08\u89C1\u7B2C 4 \u6761\uFF09\uFF0C\u4E0D\u662F\u7ED5\u8FC7\u3002
5418
5439
 
5419
5440
  ## 4.3 \u5728 MR/PR \u4E0A\u7559\u8BC4\u5BA1\u610F\u89C1\uFF08\u5DE1\u68C0/\u5BA1\u67E5\u7C7B\u4EFB\u52A1\uFF0C\u975E\u8BC4\u5BA1\u4EFB\u52A1\u65F6\uFF09
5420
5441
 
@@ -5430,36 +5451,30 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
5430
5451
  - \u82E5\u672C\u81EA\u52A8\u5316\u5DF2\u5C31\u540C\u4E00\u95EE\u9898\u7559\u8FC7\u8BC4\u8BBA\u3001\u7ED3\u8BBA\u4E0E\u8FD9\u6B21\u4E00\u81F4 \u2192 **\u4E0D\u8981\u91CD\u590D\u8BC4\u8BBA**\uFF1B
5431
5452
  - \u26D4 **\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
5432
5453
 
5433
- ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B
5434
-
5435
- - **\u4F55\u65F6\u53D1\u8BC4\u8BBA**\uFF1A**\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u4EE5\u667A\u80FD\u4F53\u8EAB\u4EFD\u53D1\u9001**\uFF08\u4EFB\u52A1\u5B8C\u6210\u65F6\u81EA\u52A8\u53D1\u5E03\u5230\u4EFB\u52A1\u5BF9\u8C61\uFF0C\u4F5C\u8005=\u5F53\u524D\u667A\u80FD\u4F53\uFF09\u3002**\u4F60\u4E0D\u9700\u8981\u3001\u4E5F\u4E0D\u5E94\u8C03\u7528 \`morula comment add\`**\u2014\u2014\u53EA\u8981\u5728\u6700\u7EC8\u8F93\u51FA\u91CC\u7ED9\u51FA\u7528\u6237\u5173\u5FC3\u7684\u7ED3\u8BBA\u5373\u53EF\uFF0C\u670D\u52A1\u7AEF\u4F1A\u4F5C\u4E3A\u8BE5\u667A\u80FD\u4F53\u7684\u5B8C\u6210\u8BC4\u8BBA\u53D1\u5E03\u3002chat \u4F1A\u8BDD\u65E0 objectId \u7684\u4EFB\u52A1\uFF08\u7EAF\u5BF9\u8BDD\u6D3E\u53D1\uFF09\u4E0D\u56DE\u8BC4\u8BBA\u2014\u2014\u6700\u7EC8\u4EA4\u4ED8\u7531 daemon \u56DE\u586B assistant \u6D88\u606F\u3002
5436
- - **\u7EC8\u7AEF\u8F93\u51FA\u3001\u601D\u8003\u8FC7\u7A0B\u3001\u5DE5\u5177\u65E5\u5FD7\u7528\u6237\u90FD\u770B\u4E0D\u5230**\u2014\u2014\u7528\u6237\u770B\u5230\u7684\u53EA\u6709\u670D\u52A1\u7AEF\u6574\u5408\u540E\u53D1\u5E03\u7684\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u3002
5437
- - **\u8BC4\u8BBA @\u89E6\u53D1\u4EFB\u52A1\uFF08\u672C\u4EFB\u52A1\u7531\u8BC4\u8BBA\u89E6\u53D1\uFF09**\uFF1A\u670D\u52A1\u7AEF\u4F1A\u628A\u6700\u7EC8\u603B\u7ED3\u81EA\u52A8\u4F5C\u4E3A**\u8BE5\u89E6\u53D1\u8BC4\u8BBA\u7684\u56DE\u590D**\u53D1\u51FA\uFF08\u6302\u5728\u88AB @ \u7684\u8BC4\u8BBA\u4E0B\uFF09\u3002\u4F60\u65E0\u9700\u3001\u4E5F\u4E0D\u8981\u81EA\u5DF1\u8C03\u7528 \`morula comment add\`/add_comment\u3002
5438
- - **\u9632\u91CD\u590D\uFF08\u670D\u52A1\u7AEF\u4FDD\u8BC1\uFF09**\uFF1A\u670D\u52A1\u7AEF\u5BF9\u300C\u540C\u4E00\u4E2A\u4EFB\u52A1\u53EA\u53D1\u4E00\u6761\u5B8C\u6210\u8BC4\u8BBA\u300D\u505A\u5E42\u7B49\uFF0C\u4E0D\u4F1A\u91CD\u590D\u5237\u5C4F\u3002\u4F60\u4E0D\u9700\u8981\u3001\u4E5F\u4E0D\u8981\u53BB\u67E5/\u53D1\u591A\u6761\u3002
5439
- - **\u6700\u7EC8\u8F93\u51FA = \u53EA\u7559\u7ED3\u8BBA**\uFF08\u4E0D\u8BBE\u5B57\u6570\u4E0A\u9650\u3001\u4E0D\u8981\u6C42\u56FA\u5B9A\u683C\u5F0F\uFF0C\u81EA\u7136\u7EC4\u7EC7\u5373\u53EF\uFF09\uFF1A
5440
- - \u5FC5\u7B54\uFF1A**\u505A\u4E86\u4EC0\u4E48\u3001\u7ED3\u679C\u5982\u4F55\u3001\u7528\u6237\u9700\u8981\u77E5\u9053\u7684\u540E\u7EED**\u3002
5441
- - **PR \u6C47\u62A5\u4EC5\u5F53\u300C\u4EFB\u52A1\u6D89\u53CA\u4EE3\u7801\u6539\u52A8\u300D\u6216\u300C\u7528\u6237\u660E\u786E\u95EE\u5230 PR/\u5408\u5E76\u300D\u65F6\u7ED9\u51FA**\uFF1B\u7EAF\u95EE\u7B54/\u8C03\u7814\u7C7B\u4EFB\u52A1\u4E0D\u8981\u51FA\u73B0 PR/\u5206\u652F\u7AE0\u8282\uFF0C\u76F4\u63A5\u56DE\u7B54\u95EE\u9898\u3002
5442
- - **\u4E0D\u8981\u5199**\uFF1A\u601D\u8003\u8FC7\u7A0B\u3001\u63A2\u7D22\u6B65\u9AA4\u3001\u5DE5\u5177\u65E5\u5FD7\u3001\u7EC8\u7AEF\u8F93\u51FA\u3001\u82F1\u6587\u8FC7\u7A0B\u6027\u53D9\u8FF0\u2014\u2014\u7528\u6237\u53EA\u5173\u5FC3\u300C\u505A\u4E86\u4EC0\u4E48\u3001\u7ED3\u679C\u600E\u6837\u300D\uFF0C\u4E0D\u5173\u5FC3\u300C\u600E\u4E48\u505A\u7684\u300D\u3002
5443
- - \u5185\u5BB9\u8F83\u591A\u65F6\u7528\u5C0F\u6807\u9898/\u5217\u8868\u7EC4\u7EC7\uFF0C\u8BA9\u7ED3\u8BBA\u5148\u7F6E\u9876\u3002
5444
- - **\u8FC7\u7A0B\u6587\u672C\u5FC5\u987B\u52A0\u5206\u9694\u7B26**\uFF1A\u670D\u52A1\u7AEF\u53D6\u4F60\u7684\u6700\u7EC8\u8F93\u51FA\u65F6\u6309\u4F18\u5148\u7EA7\u622A\u53D6\u300C\u603B\u7ED3\u6BB5\u300D\uFF0C**\u6700\u9AD8\u4F18\u5148\u7EA7\u662F\u5206\u9694\u7B26 \`---FINAL---\`**\uFF08\u72EC\u5360\u4E00\u884C\u3001\u524D\u540E\u5404\u7A7A\u4E00\u884C\uFF09\u3002\u82E5\u4F60\u7684\u8F93\u51FA\u91CC\u6DF7\u5165\u4E86\u4E2D\u95F4\u8FC7\u7A0B\uFF08\u82F1\u6587\u63A2\u7D22\u53D9\u8FF0\u3001\u5DE5\u5177\u5C1D\u8BD5\u8BB0\u5F55\u3001\u601D\u8003\u7247\u6BB5\uFF09\uFF0C**\u5FC5\u987B**\u5728\u771F\u6B63\u7684\u603B\u7ED3\u4E4B\u524D\u63D2\u5165\u8BE5\u5206\u9694\u7B26\u2014\u2014\u670D\u52A1\u7AEF\u53EA\u4FDD\u7559\u5B83\u4E4B\u540E\u7684\u5185\u5BB9\uFF1A
5454
+ ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B\uFF08\u6700\u7EC8\u603B\u7ED3\u7531\u4F60\u81EA\u5DF1\u53D1\u5E03\uFF09
5445
5455
 
5456
+ - **\u6700\u7EC8\u603B\u7ED3\u8BC4\u8BBA\u7531\u4F60\u81EA\u5DF1\u53D1\u5E03**\uFF1A\u4EFB\u52A1\u6536\u5C3E\u65F6\u6267\u884C
5457
+ \`\`\`bash
5458
+ morula comment add --object <objectId> --final --content <\u7ED3\u8BBA>
5446
5459
  \`\`\`
5447
- ...\uFF08\u524D\u9762\u7684\u8FC7\u7A0B\u6587\u672C\uFF0C\u4F1A\u88AB\u670D\u52A1\u7AEF\u81EA\u52A8\u622A\u6389\uFF09...
5448
-
5449
- ---FINAL---
5450
-
5451
- ## \u603B\u7ED3
5452
-
5453
- ...\uFF08\u771F\u6B63\u7684\u603B\u7ED3\u5185\u5BB9\uFF0C\u4F1A\u5C55\u793A\u7ED9\u7528\u6237\uFF09...
5460
+ \u4F5C\u8005=\u4F60\uFF08\u667A\u80FD\u4F53\u8EAB\u4EFD\uFF09\uFF1B\u8BC4\u8BBA\u89E6\u53D1\u4EFB\u52A1\u65F6\u670D\u52A1\u7AEF\u4F1A**\u81EA\u52A8\u628A\u5B83\u6302\u5230\u89E6\u53D1\u8BC4\u8BBA\u4E0B**\u4F5C\u4E3A\u56DE\u590D\uFF08\u65E0\u9700\u81EA\u5DF1\u67E5\u8BC4\u8BBA ID\uFF09\u3002
5461
+ **\u5148\u53D1 final \u8BC4\u8BBA\u3001\u518D\u4E0A\u62A5\u4EFB\u52A1\u7ED3\u679C**\u2014\u2014\u670D\u52A1\u7AEF\u5728\u4F60\u5B8C\u6210\u65F6\u68C0\u67E5\uFF1A\u6CA1\u53D1\u8FC7 final \u624D\u4F1A\u515C\u5E95\u4EE3\u53D1\u4E00\u6761\uFF08\u540C\u4E00\u4EFB\u52A1\u53EA\u4F1A\u4FDD\u7559\u4E00\u6761\u6700\u7EC8\u603B\u7ED3\uFF0C\u91CD\u590D\u53D1\u9001\u4F1A\u88AB\u670D\u52A1\u7AEF\u62D2\u7EDD\uFF0C\u8FD9\u662F\u9884\u671F\u5185\u7684\u5E42\u7B49\u4FDD\u62A4\uFF09\u3002
5462
+ - **\u4F8B\u5916\uFF1A\u8BC4\u5BA1\u4EFB\u52A1\u4E0D\u9700\u8981\uFF08\u4E5F\u4E0D\u5E94\uFF09\u53D1 final**\u2014\u2014\u4F60\u7684\u7ED3\u8BBA\u5DF2\u7ECF\u901A\u8FC7 \`morula review submit\` \u673A\u5668\u534F\u8BAE\u4E0A\u62A5\uFF0C\u5E73\u53F0\u4F1A\u636E\u6B64\u5199\u6743\u5A01\u7ED3\u8BBA\u8BC4\u8BBA\uFF08\u2705 \u901A\u8FC7 / \u{1F4AC} \u975E\u963B\u585E / \u{1F501} \u672A\u901A\u8FC7\uFF09\uFF1B\u518D\u53D1\u4E00\u6761 final \u603B\u7ED3\u4F1A\u8BA9\u540C\u4E00\u7ED3\u8BBA\u5728\u8BC4\u8BBA\u533A**\u8BF4\u4E24\u904D**\uFF08\u670D\u52A1\u7AEF\u4E5F\u4F1A\u62D2\u7EDD\u8BC4\u5BA1\u4EFB\u52A1\u7684 final\uFF09\u3002\u9700\u8981\u53D1 final \u7684\u662F\u5F00\u53D1 / \u4FEE\u590D / \u5206\u6790 / \u95EE\u7B54 / \u8BC4\u8BBA\u89E6\u53D1\u7C7B\u4EFB\u52A1\u2014\u2014\u5185\u5BB9**\u7528\u4F60\u81EA\u5DF1\u7684\u8BDD\u5199**\uFF0C\u4E0D\u8981\u5957\u6A21\u677F\u3002
5463
+ - **\u666E\u901A\u8BC4\u8BBA**\uFF08\u8BF4\u660E\u8FDB\u5C55\u3001@ \u5176\u4ED6\u4EBA\u6216\u8FD0\u884C\u65F6\uFF09\u968F\u65F6\u53EF\u53D1\uFF1A
5464
+ \`\`\`bash
5465
+ morula comment add --object <objectId> --content <\u6587\u672C>
5466
+ morula comment add --object <objectId> --content <\u6587\u672C> --mention-agent <stageExecutorId> # @ \u9636\u6BB5\u8FD0\u884C\u65F6\uFF08\u89E6\u53D1\u8BE5\u6267\u884C\u5668\u5728\u672C\u4EFB\u52A1\u4E0A\u5E72\u6D3B\uFF09
5454
5467
  \`\`\`
5455
-
5456
- \u670D\u52A1\u7AEF\u540C\u65F6\u517C\u5BB9 \`---SUMMARY---\`\u3002**\u4E0D\u5199\u5206\u9694\u7B26\u65F6**\uFF0C\u670D\u52A1\u7AEF\u53EA\u80FD\u9000\u56DE\u300C\u6309\u603B\u7ED3\u7C7B\u6807\u9898 / \u6C34\u5E73\u7EBF\u731C\u6D4B\u300D\uFF0C\u731C\u4E0D\u4E2D\u5C31\u4F1A\u628A\u4F60\u7684\u8FC7\u7A0B\u6587\u672C\u4E00\u8D77\u53D1\u7ED9\u7528\u6237\u2014\u2014\u6240\u4EE5\u6DF7\u4E86\u8FC7\u7A0B\u5C31\u4E00\u5B9A\u8981\u52A0\u3002
5457
- - \u4E0D\u8981\u5305\u542B\u5DE5\u5177\u8C03\u8BD5\u8FC7\u7A0B\u3001\u5185\u90E8\u65E5\u5FD7\u3001\u4EFB\u4F55\u51ED\u636E\u6216 token \u4FE1\u606F\u3002
5468
+ chat \u4F1A\u8BDD\u65E0 objectId \u7684\u4EFB\u52A1\uFF08\u7EAF\u5BF9\u8BDD\u6D3E\u53D1\uFF09\u4E0D\u53D1\u8BC4\u8BBA\u2014\u2014\u6700\u7EC8\u4EA4\u4ED8\u7531 daemon \u56DE\u586B assistant \u6D88\u606F\u3002
5469
+ - **\u8BC4\u8BBA\u7EAA\u5F8B\uFF1A\u53EA\u5199\u7ED3\u8BBA\u4E0E\u4E8B\u5B9E**\u2014\u2014\u505A\u4E86\u4EC0\u4E48\u3001\u7ED3\u679C\u5982\u4F55\u3001\u7528\u6237\u9700\u8981\u77E5\u9053\u7684\u540E\u7EED\uFF1B**\u4E0D\u5199**\u601D\u8003\u8FC7\u7A0B\u3001\u63A2\u7D22\u6B65\u9AA4\u3001\u5DE5\u5177\u65E5\u5FD7\u3001\u7EC8\u7AEF\u8F93\u51FA\u3001\u547D\u4EE4\u56DE\u663E\u3002\u7528\u6237\u53EA\u5173\u5FC3\u300C\u505A\u4E86\u4EC0\u4E48\u3001\u7ED3\u679C\u600E\u6837\u300D\uFF0C\u4E0D\u5173\u5FC3\u300C\u600E\u4E48\u505A\u7684\u300D\u3002\u5185\u5BB9\u8F83\u591A\u65F6\u7528\u5C0F\u6807\u9898/\u5217\u8868\u7EC4\u7EC7\uFF0C\u7ED3\u8BBA\u5148\u7F6E\u9876\u3002
5470
+ - **PR \u6C47\u62A5\u4EC5\u5F53\u300C\u4EFB\u52A1\u6D89\u53CA\u4EE3\u7801\u6539\u52A8\u300D\u6216\u300C\u7528\u6237\u660E\u786E\u95EE\u5230 PR/\u5408\u5E76\u300D\u65F6\u7ED9\u51FA**\uFF1B\u7EAF\u95EE\u7B54/\u8C03\u7814\u7C7B\u4EFB\u52A1\u4E0D\u8981\u51FA\u73B0 PR/\u5206\u652F\u7AE0\u8282\u3002
5471
+ - **\u4EFB\u52A1\u7ED3\u679C\u6587\u672C\u540C\u6837\u53EA\u5199\u7ED3\u8BBA**\uFF1A\u4E0A\u62A5\u7684\u4EFB\u52A1\u7ED3\u679C\u4F1A\u88AB\u670D\u52A1\u7AEF\u7528\u4E8E\u7F3A\u5931\u515C\u5E95\u4E0E\u901A\u77E5\u6458\u8981\u2014\u2014\u82E5\u7ED3\u679C\u91CC\u6DF7\u5165\u4E86\u4E2D\u95F4\u8FC7\u7A0B\uFF08\u82F1\u6587\u63A2\u7D22\u53D9\u8FF0\u3001\u5DE5\u5177\u5C1D\u8BD5\u8BB0\u5F55\u3001\u601D\u8003\u7247\u6BB5\uFF09\uFF0C**\u5FC5\u987B**\u5728\u771F\u6B63\u7684\u603B\u7ED3\u4E4B\u524D\u63D2\u5165\u72EC\u5360\u4E00\u884C\u7684 \`---FINAL---\` \u5206\u9694\u7B26\uFF0C\u670D\u52A1\u7AEF\u515C\u5E95\u65F6\u53EA\u4FDD\u7559\u5B83\u4E4B\u540E\u7684\u5185\u5BB9\uFF08\`---SUMMARY---\` \u540C\u6837\u517C\u5BB9\uFF1B\u4E0D\u5199\u65F6\u670D\u52A1\u7AEF\u9000\u56DE\u300C\u6309\u603B\u7ED3\u7C7B\u6807\u9898 / \u6C34\u5E73\u7EBF\u731C\u6D4B\u300D\uFF0C\u731C\u4E0D\u4E2D\u4F1A\u628A\u8FC7\u7A0B\u6587\u672C\u4E00\u8D77\u515C\u5E95\u51FA\u53BB\uFF09\u3002
5458
5472
  - **\u4E0D\u8981\u628A\u672C\u5730\u7EDD\u5BF9\u8DEF\u5F84\u5F53\u4EA4\u4ED8\u7269**\uFF1A\u5DE5\u4F5C\u76EE\u5F55\u8DEF\u5F84\u53EA\u5C5E\u4E8E\u6267\u884C\u5B83\u7684\u673A\u5668\uFF0C\u8BFB\u8005\u6253\u4E0D\u5F00\uFF1B\u751F\u6210\u7684\u6587\u4EF6**\u5FC5\u987B\u5148\u56DE\u4F20\u5E73\u53F0**\uFF08\u89C1 1.1 \`morula attachment upload\`\uFF09\uFF0C\u4EA4\u4ED8\u7269\u7528\u5E73\u53F0\u4E0B\u8F7D\u94FE\u63A5\u6216\u4ED3\u5E93\u5185\u76F8\u5BF9\u8DEF\u5F84\u3002
5473
+ - \u4E0D\u8981\u5305\u542B\u5DE5\u5177\u8C03\u8BD5\u8FC7\u7A0B\u3001\u5185\u90E8\u65E5\u5FD7\u3001\u4EFB\u4F55\u51ED\u636E\u6216 token \u4FE1\u606F\u3002
5459
5474
 
5460
5475
  ## 6. \u7981\u6B62\u4E8B\u9879
5461
5476
 
5462
- - \u7981\u6B62\u76F4\u63A5\u8C03\u7528 morula REST API / \u975E CLI \u65B9\u5F0F\u5199\u8BC4\u8BBA\u3001\u6539\u72B6\u6001\u3001\u8BFB\u6570\u636E\uFF1B**\u4E0D\u8981\u8C03\u7528 \`morula comment add\` \u81EA\u884C\u7F6E\u8BC4**\u2014\u2014\u5B8C\u6210\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\u3002
5477
+ - \u7981\u6B62\u76F4\u63A5\u8C03\u7528 morula REST API / \u975E CLI \u65B9\u5F0F\u5199\u8BC4\u8BBA\u3001\u6539\u72B6\u6001\u3001\u8BFB\u6570\u636E\u2014\u2014\u4E0E\u5E73\u53F0\u4EA4\u4E92\u53EA\u8D70 morula CLI\uFF08\u8BC4\u8BBA\u7528\u7B2C 5 \u6761\u7684 \`morula comment add\`\uFF0C\u72B6\u6001\u7528\u7B2C 4 \u6761\u7684 \`morula task state\`\uFF09\u3002
5463
5478
  - \u7981\u6B62\u8BFB\u53D6 \`~/.morula-cli\`\u3001\u51ED\u636E\u6587\u4EF6\u3001\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u5BC6\u94A5\u3002
5464
5479
  - \u53EA\u5728\u4EFB\u52A1\u6307\u5B9A\u7684\u5DE5\u4F5C\u76EE\u5F55\u4E0E\u4ED3\u5E93\u5185\u64CD\u4F5C\uFF0C\u4E0D\u8981\u63A2\u7D22\u5DE5\u4F5C\u76EE\u5F55\u4E4B\u5916\u7684\u6587\u4EF6\u7CFB\u7EDF\u3002
5465
5480
  - \u7981\u6B62\u628A token\u3001\u5B89\u88C5 ID\u3001\u5185\u90E8 URL \u5199\u8FDB\u8BC4\u8BBA\u6216 commit\u3002
@@ -5486,7 +5501,7 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
5486
5501
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
5487
5502
  |------|------|------|
5488
5503
  | CLI \u8BFB\u8BC4\u8BBA | \`morula comment list --object <objectId>\` | \`packages/morula-cli/src/commands/comment.ts\` |
5489
- | CLI \u5199\u8BC4\u8BBA\uFF08**\u5951\u7EA6\u7981\u6B62 agent \u81EA\u884C\u8C03\u7528**\uFF1A\u5B8C\u6210\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u53D1\u9001\uFF0C\u89C1\u672C\u6587\u4EF6\u7B2C 5 \u6761\uFF09 | \`morula comment add\`\uFF08\u547D\u4EE4\u5B58\u5728\uFF0C\u4F46\u672C\u5951\u7EA6\u8981\u6C42\u4E0D\u8C03\u7528\uFF09 | \`packages/morula-cli/src/commands/comment.ts\` |
5504
+ | CLI \u5199\u8BC4\u8BBA\uFF08**\u7531 agent \u81EA\u5DF1\u53D1**\uFF0C\u542B \`--final\` \u6700\u7EC8\u603B\u7ED3\u4E0E \`--mention-agent\` @ \u8FD0\u884C\u65F6\uFF1B\u89C1\u672C\u6587\u4EF6\u7B2C 5 \u6761\uFF09 | \`morula comment add\` \u2192 \`POST /comments\`\uFF08agent_task \u51ED\u8BC1\uFF1B\`kind='final'\` \u6309 \`(taskId, kind)\` \u552F\u4E00\u9632\u91CD\uFF09 | \`packages/morula-cli/src/commands/comment.ts\`\u3001\`src/modules/task-management/comment/comment.service.ts\` |
5490
5505
  | CLI \u4EFB\u52A1\u8BE6\u60C5\uFF08**\u65E0\u53C2**\u53D6\u672C\u4EFB\u52A1\uFF1B\u5E26\u53C2\u53D6\u4E1A\u52A1\u5BF9\u8C61\uFF09 | \`morula task get\` \u2192 \`GET /agent-tasks/:id\`\uFF1B\`morula task get <objectId>\` \u2192 \`GET /objects/:id\` | \`packages/morula-cli/src/commands/task.ts\` |
5491
5506
  | daemon \u6CE8\u5165\u51ED\u636E\uFF08agent \u65E0\u9700\u81EA\u884C\u767B\u5F55\uFF09 | runner \u6CE8\u5165 \`env\` \u7ED9 Provider \u5B50\u8FDB\u7A0B | \`packages/runtime/src/daemon/runner.ts\` |
5492
5507
  | \u8C03 CLI \u7528 \`$MORULA_CLI\`\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u4E0E\u5F53\u524D daemon \u540C\u6E90\uFF1BPATH \u4E0A\u7684\u88F8 \`morula\` \u53EF\u80FD\u662F\u5BBF\u4E3B\u81EA\u5E26\u7684\u65E7\u7248\uFF09 | \`resolveAgentCliPath\`\uFF08\u4F18\u5148\u672C\u5305 \`dist/morula.js\`\uFF0C\u4EA7\u7269\u7F3A\u5931\u56DE\u9000\u63D2\u4EF6\u542F\u52A8\u5668\uFF09\uFF1B\u6CE8\u5165\u70B9\uFF1A\u4EFB\u52A1\u94FE\u8DEF \`env\`\u3001\u4F1A\u8BDD\u94FE\u8DEF \`env\` | \`packages/runtime/src/daemon/plugin-register.ts\`\u3001\`runner.ts\`\u3001\`session-host.ts\` |
@@ -5505,12 +5520,15 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
5505
5520
  | delivery \u4EE5\u672C\u5730\u5B9E\u9645\u5206\u652F\u4E3A\u51C6 | \`reportPrDelivery\` \u8BFB \`git branch --show-current\`\uFF0C\u8DF3\u8FC7\u5360\u4F4D/\u4E3B\u5E72 | \`packages/runtime/src/daemon/runner.ts\` |
5506
5521
  | \u4EA4\u4ED8\u4E0A\u62A5\u904D\u5386\u672C\u4EFB\u52A1\u5168\u90E8\u4ED3\u5E93 | \u4E3B\u4ED3\u5E93\u76EE\u5F55\uFF08\u5DE5\u4F5C\u533A\u6839\u4E0B\u7684\u4ED3\u5E93\u5B50\u76EE\u5F55\uFF09+ \u5176\u4F59\u540C\u7EA7\u4ED3\u5E93\u5B50\u76EE\u5F55\uFF08prepare \u4EA7\u7269\uFF0C\u78C1\u76D8\u4E8B\u5B9E\uFF1B\u6309 repoFullName \u5F52\u5C5E\u6392\u9664\u4E3B\u4ED3\u5E93\uFF09\uFF0C\u9010\u4ED3\u4E0A\u62A5 branch/pushState\u3001\u9010\u4ED3\u72EC\u7ACB\u6210\u8D25\uFF1B\u6B21\u4ED3\u5E93 \`workContextId\` \u7701\u7565\u65F6\u670D\u52A1\u7AEF\u6309 \`(lastAgentTaskId, repoFullName)\` \u53CD\u67E5 | \`reportPrDelivery\`/\`reportRepoTargets\`\uFF08\`runner.ts\`\uFF09\u3001\`collectMaterializedRepoDeliveryTargets\`\uFF08\`repo-checkout-service.ts\`\uFF09 |
5507
5522
 
5508
- ## 4. \u72B6\u6001\u7531\u670D\u52A1\u7AEF\u9A71\u52A8\uFF08agent \u4E0D\u624B\u52A8\u7FFB\u72B6\u6001\uFF09
5523
+ ## 4. \u72B6\u6001\u4E0E\u4EFB\u52A1\u64CD\u4F5C\uFF08\u7ED3\u8BBA\u4F18\u5148\u3001\u52A8\u4F5C\u4E3A\u8F85\uFF1Bproposal stage-runtime-as-task-actor\uFF09
5509
5524
 
5510
5525
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
5511
5526
  |------|------|------|
5512
- | 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\` |
5513
- | 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 |
5527
+ | merge \u81EA\u52A8\u5173\u5355\uFF08\u4E8B\u5B9E\u4E8B\u4EF6\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\` |
5528
+ | \u8FD0\u884C\u65F6\u53EF\u58F0\u660E\u72B6\u6001\u96C6\u5408 = \`in_progress\` / \`review\`\uFF08D8 \u843D\u5730\u53E3\u5F84\uFF1B\u7EC8\u6001\u4E0E backlog/todo \u4E0D\u53EF\u58F0\u660E\uFF0CD5\uFF09 | \`resolveRuntimeStateTransition\`\uFF08\u552F\u4E00\u5B9A\u4E49\uFF1A\u63A7\u5236\u5668\u7AEF\u70B9\u4E0E change_object_status \u5DE5\u5177\u5171\u7528\uFF09\uFF1B\u6B63\u5411\u8D70\u4E8B\u4EF6\u7EA7\u89C4\u5219 + ObjectService.update | \`src/modules/agent-task/domain/runtime-state-actions.ts\`\u3001\`agent-task.controller.ts\`\uFF08\`POST :id/state\`\uFF09 |
5529
+ | **\u5F85\u8BC4\u5BA1 \u2192 \u5F00\u53D1\u4E2D = \u9000\u56DE\u5F00\u53D1\uFF0C\u5355\u4E00\u6548\u5E94\u5165\u53E3**\uFF08\u8BB0\u8D26 + \u56DE\u9000 + \u6D3E\u4FEE\u590D + \u53CC\u89E6\u53D1\u53BB\u91CD\uFF1B\u4E0D\u505A\u88F8\u72B6\u6001\u5199\uFF09 | \`ReviewLoopService.runtimeRevertToDev\`\uFF08\u4E0E verdict \u8DEF\u5F84 \`handleRejected\` \u5171\u7528 \`dispatchDevRework\`\uFF1B\u4E8B\u5B9E\u53BB\u91CD = \u5BF9\u8C61\u5DF2\u6709\u672A\u5B8C\u6210 dev \u4EFB\u52A1\u5219\u53EA\u8BB0\u8D26\u4E0D\u6D3E\u5355\uFF09 | \`src/modules/agent-task/review-loop.service.ts\` |
5530
+ | \u4EFB\u52A1\u6D3B\u8DC3\u95E8\u7981\uFF08\u7EC8\u6001\u4EFB\u52A1\u7684 token \u4E0D\u5F97\u518D\u63A8\u72B6\u6001/\u6307\u6D3E\uFF09 | \`ACTIVE_TASK_STATUSES\`\uFF08queued/dispatched/running/awaiting_approval\uFF09 | \`src/modules/agent-task/domain/runtime-state-actions.ts\` |
5531
+ | \u6307\u6D3E\u8D1F\u8D23\u4EBA\uFF08\u76EE\u6807 = \u672C\u7A7A\u95F4\u6210\u5458\u6216\u9636\u6BB5\u6267\u884C\u5668 principal\uFF1B\u5931\u8D25\u663E\u5F0F\u62A5\u9519\uFF09 | \`POST /agent-tasks/:id/assignee\`\uFF08ObjectService.update \u843D\u5E93\uFF0Cagent_task \u5B57\u6BB5\u767D\u540D\u5355\u542B assigneePrincipalId\uFF09 | \`agent-task.controller.ts\`\u3001\`object.service.ts\` |
5514
5532
 
5515
5533
  ## 4.1 \u8BC4\u5BA1\u4EFB\u52A1\u7ED3\u8BBA\u56DE\u4F20\uFF08morula review submit\uFF09
5516
5534
 
@@ -5519,15 +5537,17 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
5519
5537
  | \u8BC4\u5BA1\u7ED3\u8BBA\u673A\u5668\u534F\u8BAE\uFF08approved / changes_requested / commented\uFF0Cchanges_requested \u5FC5\u987B\u5E26 details\uFF09 | \`morula review submit <objectId> --verdict ... [--details] [--pr-number]\` \u2192 \`POST /agent-collaborations/review/submit\`\uFF08\u670D\u52A1\u7AEF\u6821\u9A8C\u8C03\u7528\u8005 = \u8BC4\u5BA1\u4EFB\u52A1\u540E\u843D\u5E93\u5E76\u9A71\u52A8\u56DE\u73AF\uFF09 | \`packages/morula-cli/src/commands/review.ts\`\u3001\`src/modules/agent-task/review-loop.service.ts\` |
5520
5538
  | \u8BC4\u5BA1\u5BF9\u8C61\uFF08objectId\uFF09\u7ECF AgentTask \u672C\u4F53\u4E0B\u53D1\uFF08\`morula task get\` \u65E0\u53C2\u8FD4\u56DE\uFF09 | AgentTask \u8BB0\u5F55 \`objectId\` \u5B57\u6BB5\uFF08enqueue \u65F6\u5199\u5165\uFF09 | \`src/modules/agent-task/review-loop.service.ts\` |
5521
5539
 
5522
- ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B\uFF08\u4E00\u6761\u6700\u7EC8\u8BC4\u8BBA\uFF0C\u7ED3\u679C\u4E0A\u5E73\u53F0\uFF09
5540
+ ## 5. \u8BC4\u8BBA\u4EA4\u4ED8\u7EAA\u5F8B\uFF08\u6700\u7EC8\u603B\u7ED3\u7531\u8FD0\u884C\u65F6\u81EA\u53D1\uFF0C\u670D\u52A1\u7AEF\u9632\u91CD\u515C\u5E95\uFF09
5523
5541
 
5524
5542
  | \u5951\u7EA6 | \u5B9E\u73B0 | \u4F4D\u7F6E |
5525
5543
  |------|------|------|
5526
- | \u8BC4\u8BBA\u662F\u7528\u6237\u53EF\u89C1\u7684\u552F\u4E00\u901A\u9053\uFF1B**\u5B8C\u6210\u8BC4\u8BBA\u7531\u670D\u52A1\u7AEF\u7EDF\u4E00\u4EE5\u667A\u80FD\u4F53\u8EAB\u4EFD\u53D1\u9001**\uFF08agent \u4E0D\u8C03 \`comment add\`\uFF09 | \u670D\u52A1\u7AEF \`finalOutput\` \u56DE\u586B + \u805A\u5408\u53D1\u5E03\uFF1B\u8BC4\u8BBA @\u89E6\u53D1\u4EFB\u52A1\u7684\u603B\u7ED3\u81EA\u52A8\u6302\u5230\u89E6\u53D1\u8BC4\u8BBA\u4E0B\uFF08hasAgentRepliedToTrigger \u53BB\u91CD\uFF09 | \`src/modules/agent-task/task-lifecycle.service.ts\`\u3001\u672C SKILL \u7B2C 5 \u6761 |
5527
- | \u6700\u7EC8\u8F93\u51FA\u622A\u53D6\u300C\u603B\u7ED3\u6BB5\u300D\uFF08\u6700\u9AD8\u4F18\u5148\u7EA7 = \`---FINAL---\` \u5206\u9694\u7B26\uFF09 | \`cleanFinalOutput\`\uFF1A\u5206\u9694\u7B26 \u2192 \u603B\u7ED3\u7C7B\u6807\u9898 \u2192 \u6C34\u5E73\u7EBF \u2192 \u539F\u6587\u515C\u5E95 | \`src/modules/agent-task/task-lifecycle.service.ts\` |
5528
- | **\u8BC4\u8BBA @\u89E6\u53D1\u4EFB\u52A1 \u2192 \u6700\u7EC8\u603B\u7ED3\u7531\u670D\u52A1\u7AEF\u81EA\u52A8\u56DE\u590D\u5230\u89E6\u53D1\u8BC4\u8BBA\u4E0B**\uFF08agent \u65E0\u9700\u8BC4\u8BBA ID\uFF0C\u4E0D\u6CE8\u5165\u63D0\u793A\u8BCD\uFF09 | \u7ED3\u6784\u6027\u6302\u9760\uFF1A\`CommentService.create\`\uFF08resolveCommentAuthor \u8FD4\u56DE triggerCommentId \u2192 parentCommentId \u9ED8\u8BA4\u503C\uFF09\uFF1B\u751F\u547D\u5468\u671F\u515C\u5E95\uFF1A\`task-lifecycle.service.ts\` hasAgentRepliedToTrigger / isMentionReply | LOCAL-AGENT-COMMENT-REPLY-001 |
5544
+ | \u8BC4\u8BBA\u662F\u7528\u6237\u53EF\u89C1\u7684\u4EA7\u51FA\u901A\u9053\uFF1B**\u6700\u7EC8\u603B\u7ED3\u7531 agent \u81EA\u5DF1\u53D1**\uFF08\`morula comment add --final\`\uFF0C\u4F5C\u8005=\u667A\u80FD\u4F53\uFF09 | \`POST /comments\`\uFF08agent_task \u51ED\u8BC1\uFF1B\`kind='final'\` \u670D\u52A1\u7AEF\u56DE\u586B \`taskId\`\uFF0C\`(tenantId, taskId, kind)\` \u552F\u4E00\u7EA6\u675F \u2192 \u540C\u4E00\u4EFB\u52A1\u4E00\u6761\uFF09 | \`packages/morula-cli/src/commands/comment.ts\`\u3001\`src/modules/task-management/comment/comment.service.ts\`\uFF08FINAL-COMMENT-001\uFF09 |
5545
+ | **\u670D\u52A1\u7AEF\u7F3A\u5931\u515C\u5E95**\uFF08DP-1\uFF09\uFF1A\u4EFB\u52A1\u5B8C\u6210\u65F6\u68C0\u67E5\u6CA1\u53D1\u8FC7 final \u624D\u4EE3\u53D1\u4E00\u6761\uFF08\u5360\u4F4F\u552F\u4E00\u952E\uFF1B\u8FD0\u884C\u65F6\u8FDF\u5230\u8865\u53D1\u88AB\u62D2\u7EDD\uFF09 | \`task-lifecycle.service.ts\` onTaskCompleted\uFF1A\`runtimeFinalExists\` \u68C0\u67E5 + \u515C\u5E95 create \u5E26 \`taskId/kind\`\uFF08\u5E76\u53D1\u7531 P2002 \u6355\u83B7\uFF09 | \`src/modules/agent-task/task-lifecycle.service.ts\` |
5546
+ | \u515C\u5E95\u8DEF\u5F84\u622A\u53D6\u300C\u603B\u7ED3\u6BB5\u300D\uFF08\`---FINAL---\` \u5206\u9694\u7B26 \u2192 \u603B\u7ED3\u6807\u9898 \u2192 \u6C34\u5E73\u7EBF \u2192 \u539F\u6587\u515C\u5E95\uFF09 | \`cleanFinalOutput\`\uFF08\u4EC5\u515C\u5E95\u4EE3\u53D1\u8DEF\u5F84\u4F7F\u7528\uFF1B\u8FD0\u884C\u65F6\u81EA\u53D1\u8DEF\u5F84\u5185\u5BB9\u7EAA\u5F8B\u9760\u5951\u7EA6\uFF09 | \`src/modules/agent-task/task-lifecycle.service.ts\` |
5547
+ | **\u8BC4\u8BBA @\u89E6\u53D1\u4EFB\u52A1 \u2192 agent \u7684 final \u81EA\u52A8\u6302\u5230\u89E6\u53D1\u8BC4\u8BBA\u4E0B**\uFF08agent \u65E0\u9700\u8BC4\u8BBA ID\uFF09 | \u7ED3\u6784\u6027\u6302\u9760\uFF1A\`CommentService.create\`\uFF08resolveCommentAuthor \u8FD4\u56DE triggerCommentId \u2192 parentCommentId \u9ED8\u8BA4\u503C\uFF09\uFF1B\u751F\u547D\u5468\u671F\u515C\u5E95\uFF1AhasAgentRepliedToTrigger / isMentionReply | LOCAL-AGENT-COMMENT-REPLY-001 |
5548
+ | **@ \u9636\u6BB5\u8FD0\u884C\u65F6**\uFF08\`--mention-agent <stageExecutorId>\`\uFF09\u2192 \u89E6\u53D1\u8BE5\u6267\u884C\u5668\u5728\u4EFB\u52A1\u4E0A\u5E72\u6D3B | mentions \u6570\u7EC4 agent \u7C7B\u578B\u9879 \u2192 \`comment.mentioned\` \u4E8B\u4EF6 \u2192 AgentCollaborationService \u6D3E\u53D1 | \`comment.service.ts\`\u3001\`agent-collaboration.service.ts\` |
5529
5549
  | \u7981\u6B62\u672C\u5730\u7EDD\u5BF9\u8DEF\u5F84\u4F5C\u4E3A\u4EA4\u4ED8\u7269 | \u5951\u7EA6\u7B2C 5 \u6761\uFF1B\`reportPrDelivery\` \u53EA\u56DE\u62A5\u4ED3\u5E93\u76F8\u5BF9\u4FE1\u606F | \`packages/runtime/src/daemon/runner.ts\` |
5530
- | \u5355\u6761\u6700\u7EC8\u8BC4\u8BBA\uFF08\u4E0D\u5237\u8FDB\u5EA6\uFF09 | \u5951\u7EA6\u7B2C 5 \u6761 | \u672C SKILL |
5550
+ | \u5355\u6761\u6700\u7EC8\u8BC4\u8BBA\uFF08\u4E0D\u5237\u8FDB\u5EA6\uFF1B\u8BC4\u8BBA\u53EA\u5199\u7ED3\u8BBA\u4E0E\u4E8B\u5B9E\uFF0CD10\uFF09 | \u5951\u7EA6\u7B2C 5 \u6761 + \u552F\u4E00\u7EA6\u675F | \u672C SKILL |
5531
5551
 
5532
5552
  ## 6. \u7981\u6B62\u4E8B\u9879
5533
5553
 
@@ -5778,7 +5798,7 @@ function outboxAge(entry) {
5778
5798
  // src/daemon/plugin-register.ts
5779
5799
  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";
5780
5800
  import { homedir as homedir10 } from "node:os";
5781
- import { dirname as dirname7, join as join13 } from "node:path";
5801
+ import { dirname as dirname8, join as join13 } from "node:path";
5782
5802
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5783
5803
  var PLUGIN_NAME = "morula-cli";
5784
5804
  var MANIFEST_DIR_NAME = ".codex-plugin";
@@ -5793,9 +5813,9 @@ function defaultPluginsRoot(env = process.env, home = homedir10()) {
5793
5813
  return join13(resolveDimcodeHome(env, home), "plugins");
5794
5814
  }
5795
5815
  function resolvePackagePaths(entryUrl) {
5796
- const distDir = dirname7(fileURLToPath2(entryUrl));
5816
+ const distDir = dirname8(fileURLToPath2(entryUrl));
5797
5817
  return {
5798
- packageRoot: dirname7(distDir),
5818
+ packageRoot: dirname8(distDir),
5799
5819
  distDir,
5800
5820
  assetsDir: join13(distDir, "plugin")
5801
5821
  };
@@ -5811,7 +5831,25 @@ if [ -z "$NODE" ]; then
5811
5831
  echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
5812
5832
  exit 127
5813
5833
  fi
5814
- exec "$NODE" ${JSON.stringify(entry)} "$@"
5834
+ ENTRY=${JSON.stringify(entry)}
5835
+ # \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
5836
+ if [ ! -f "$ENTRY" ] && [ -n "$MORULA_CLI" ] && [ -f "$MORULA_CLI" ]; then
5837
+ ENTRY="$MORULA_CLI"
5838
+ fi
5839
+ if [ ! -f "$ENTRY" ]; then
5840
+ RT="$(command -v morula-runtime 2>/dev/null || true)"
5841
+ if [ -n "$RT" ]; then
5842
+ 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)"
5843
+ if [ -n "$ALT" ] && [ -f "$ALT" ]; then
5844
+ ENTRY="$ALT"
5845
+ fi
5846
+ fi
5847
+ fi
5848
+ if [ ! -f "$ENTRY" ]; then
5849
+ 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
5850
+ exit 127
5851
+ fi
5852
+ exec "$NODE" "$ENTRY" "$@"
5815
5853
  `;
5816
5854
  }
5817
5855
  function assetsReady(paths) {
@@ -6068,7 +6106,7 @@ function isAcpInitRetryableError(error) {
6068
6106
  async function withAcpInitRetry(setupOnce, deps = {}) {
6069
6107
  const delays = deps.delaysMs ?? ACP_INIT_RETRY_DELAYS_MS;
6070
6108
  const isRetryable = deps.isRetryable ?? isAcpInitRetryableError;
6071
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6109
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6072
6110
  const maxAttempts = delays.length + 1;
6073
6111
  let lastError;
6074
6112
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -6083,7 +6121,7 @@ async function withAcpInitRetry(setupOnce, deps = {}) {
6083
6121
  error: lastError instanceof Error ? lastError.message : String(lastError ?? "")
6084
6122
  });
6085
6123
  if (delayMs > 0)
6086
- await sleep2(delayMs);
6124
+ await sleep(delayMs);
6087
6125
  if (deps.isAborted?.())
6088
6126
  throw new Error("\u4EFB\u52A1\u88AB\u53D6\u6D88");
6089
6127
  }
@@ -6835,8 +6873,8 @@ var DimAgentAdapter = class {
6835
6873
  }
6836
6874
  }
6837
6875
  async probe() {
6838
- const { existsSync: existsSync19 } = await import("node:fs");
6839
- const installed = existsSync19(this.dimPath) || this.dimPath === "dim";
6876
+ const { existsSync: existsSync21 } = await import("node:fs");
6877
+ const installed = existsSync21(this.dimPath) || this.dimPath === "dim";
6840
6878
  let authStatus = installed ? "unknown" : "error";
6841
6879
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
6842
6880
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -7129,7 +7167,7 @@ function pickLegacyDimModels(inventory) {
7129
7167
  // src/daemon/repo-cache.ts
7130
7168
  import { execFileSync as execFileSync8 } from "node:child_process";
7131
7169
  import { existsSync as existsSync12, mkdirSync as mkdirSync10 } from "node:fs";
7132
- import { dirname as dirname8, join as join15 } from "node:path";
7170
+ import { dirname as dirname9, join as join15 } from "node:path";
7133
7171
  function ownerOf(repoFullName) {
7134
7172
  const idx = repoFullName.indexOf("/");
7135
7173
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
@@ -7164,7 +7202,7 @@ function openRepoCache(workdir, tenantId, runtimeId, repoFullName, env, agentRem
7164
7202
  syncRemoteUrl(repoCachePath, originUrl(), env);
7165
7203
  return;
7166
7204
  }
7167
- mkdirSync10(dirname8(repoCachePath), { recursive: true });
7205
+ mkdirSync10(dirname9(repoCachePath), { recursive: true });
7168
7206
  const cloneUrl = fetchUrl();
7169
7207
  try {
7170
7208
  execFileSync8("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
@@ -8282,11 +8320,333 @@ function materializeAdditionalRepo(input) {
8282
8320
  return input.targetPath;
8283
8321
  }
8284
8322
 
8323
+ // src/daemon/self-upgrade.ts
8324
+ import { spawn as spawn4 } from "node:child_process";
8325
+ import { closeSync as closeSync4, existsSync as existsSync16, mkdirSync as mkdirSync13, openSync as openSync4, writeFileSync as writeFileSync11 } from "node:fs";
8326
+ import { dirname as dirname10, join as join19 } from "node:path";
8327
+ var RUNTIME_PACKAGE = "@arcships/morula-runtime";
8328
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
8329
+ var REGISTRY_TIMEOUT_MS = 1e4;
8330
+ var NPM_INSTALL_TIMEOUT_MS = 10 * 60 * 1e3;
8331
+ var NPM_ROLLBACK_TIMEOUT_MS = 10 * 60 * 1e3;
8332
+ var RESTART_LOCK_WAIT_MS = 6e4;
8333
+ var RESTART_LIVENESS_CHECK_MS = 5e3;
8334
+ function resolveRegistry(env = process.env) {
8335
+ return (env.MORULA_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY).replace(/\/+$/, "");
8336
+ }
8337
+ function parseVersion(input) {
8338
+ if (typeof input !== "string")
8339
+ return null;
8340
+ const match = input.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
8341
+ if (!match)
8342
+ return null;
8343
+ const prerelease = match[4] ? match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part) : [];
8344
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease };
8345
+ }
8346
+ function compareVersions(a, b) {
8347
+ const pa = parseVersion(a);
8348
+ const pb = parseVersion(b);
8349
+ if (!pa || !pb)
8350
+ return a === b ? 0 : a < b ? -1 : 1;
8351
+ for (const key of ["major", "minor", "patch"]) {
8352
+ if (pa[key] !== pb[key])
8353
+ return pa[key] < pb[key] ? -1 : 1;
8354
+ }
8355
+ if (pa.prerelease.length === 0 && pb.prerelease.length === 0)
8356
+ return 0;
8357
+ if (pa.prerelease.length === 0)
8358
+ return 1;
8359
+ if (pb.prerelease.length === 0)
8360
+ return -1;
8361
+ for (let i = 0; i < Math.max(pa.prerelease.length, pb.prerelease.length); i++) {
8362
+ const sa = pa.prerelease[i];
8363
+ const sb = pb.prerelease[i];
8364
+ if (sa === void 0)
8365
+ return -1;
8366
+ if (sb === void 0)
8367
+ return 1;
8368
+ if (sa === sb)
8369
+ continue;
8370
+ if (typeof sa === "number" && typeof sb === "number")
8371
+ return sa < sb ? -1 : 1;
8372
+ if (typeof sa === "number")
8373
+ return -1;
8374
+ if (typeof sb === "number")
8375
+ return 1;
8376
+ return sa < sb ? -1 : 1;
8377
+ }
8378
+ return 0;
8379
+ }
8380
+ function isUpgradeAvailable(current, latest) {
8381
+ if (!parseVersion(current) || !parseVersion(latest))
8382
+ return false;
8383
+ return compareVersions(current, latest) < 0;
8384
+ }
8385
+ function run2(command, args, opts) {
8386
+ return new Promise((resolve2) => {
8387
+ let child;
8388
+ try {
8389
+ child = spawn4(command, args, { env: opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
8390
+ } catch (error) {
8391
+ resolve2({ ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : String(error) });
8392
+ return;
8393
+ }
8394
+ let stdout = "";
8395
+ let stderr = "";
8396
+ let settled = false;
8397
+ const timer = setTimeout(() => {
8398
+ if (!settled) {
8399
+ settled = true;
8400
+ try {
8401
+ child.kill("SIGKILL");
8402
+ } catch {
8403
+ }
8404
+ resolve2({ ok: false, code: null, stdout, stderr, error: `timeout after ${Math.round(opts.timeoutMs / 1e3)}s` });
8405
+ }
8406
+ }, opts.timeoutMs);
8407
+ timer.unref?.();
8408
+ child.stdout?.on("data", (d) => {
8409
+ stdout += d.toString();
8410
+ });
8411
+ child.stderr?.on("data", (d) => {
8412
+ stderr += d.toString();
8413
+ });
8414
+ child.on("error", (error) => {
8415
+ if (settled)
8416
+ return;
8417
+ settled = true;
8418
+ clearTimeout(timer);
8419
+ resolve2({ ok: false, code: null, stdout, stderr, error: error.message });
8420
+ });
8421
+ child.on("close", (code) => {
8422
+ if (settled)
8423
+ return;
8424
+ settled = true;
8425
+ clearTimeout(timer);
8426
+ resolve2({ ok: code === 0, code, stdout, stderr });
8427
+ });
8428
+ });
8429
+ }
8430
+ async function resolveNpmCommand() {
8431
+ const nodeDir = dirname10(process.execPath);
8432
+ const candidates = [
8433
+ { command: "npm", args: [] },
8434
+ { command: join19(nodeDir, "npm"), args: [] },
8435
+ { command: process.execPath, args: [join19(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")] }
8436
+ ];
8437
+ for (const candidate of candidates) {
8438
+ const res = await run2(candidate.command, [...candidate.args, "--version"], { timeoutMs: 15e3 });
8439
+ if (res.ok && /^\d+\.\d+\.\d+/.test(res.stdout.trim()))
8440
+ return candidate;
8441
+ }
8442
+ return null;
8443
+ }
8444
+ async function resolveNpmGlobalLayout() {
8445
+ const npm = await resolveNpmCommand();
8446
+ if (!npm)
8447
+ 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");
8448
+ const prefixRes = await run2(npm.command, [...npm.args, "prefix", "-g"], { timeoutMs: 3e4 });
8449
+ const rootRes = await run2(npm.command, [...npm.args, "root", "-g"], { timeoutMs: 3e4 });
8450
+ const prefix = prefixRes.ok ? prefixRes.stdout.trim() : "";
8451
+ const nodeModulesRoot = rootRes.ok ? rootRes.stdout.trim() : "";
8452
+ if (!prefix || !nodeModulesRoot)
8453
+ throw new Error(`\u89E3\u6790 npm \u5168\u5C40\u5E03\u5C40\u5931\u8D25\uFF08prefix=${prefix || "?"} root=${nodeModulesRoot || "?"}\uFF09`);
8454
+ return {
8455
+ prefix,
8456
+ nodeModulesRoot,
8457
+ runtimePackageRoot: join19(nodeModulesRoot, RUNTIME_PACKAGE),
8458
+ runtimeBinPath: process.platform === "win32" ? join19(prefix, `${RUNTIME_PACKAGE.split("/")[1] ?? "morula-runtime"}.cmd`) : join19(prefix, "bin", "morula-runtime")
8459
+ };
8460
+ }
8461
+ async function fetchLatestRuntimeVersion(opts = {}) {
8462
+ const registry2 = opts.registry ?? resolveRegistry();
8463
+ const url = `${registry2}/${encodeURIComponent(RUNTIME_PACKAGE)}/latest`;
8464
+ const controller = new AbortController();
8465
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? REGISTRY_TIMEOUT_MS);
8466
+ timer.unref?.();
8467
+ try {
8468
+ const res = await fetch(url, { signal: controller.signal, headers: { Accept: "application/json" } });
8469
+ if (!res.ok)
8470
+ throw new Error(`registry ${res.status}`);
8471
+ const body = await res.json();
8472
+ const version = typeof body.version === "string" ? body.version.trim() : "";
8473
+ if (!version)
8474
+ throw new Error("registry \u54CD\u5E94\u7F3A\u5C11 version");
8475
+ return version;
8476
+ } catch (error) {
8477
+ const message = error instanceof Error ? error.message : String(error);
8478
+ throw new Error(`\u67E5\u8BE2 npm registry \u5931\u8D25\uFF08${url}\uFF09: ${message}`);
8479
+ } finally {
8480
+ clearTimeout(timer);
8481
+ }
8482
+ }
8483
+ async function npmContext(registry2) {
8484
+ const npm = await resolveNpmCommand();
8485
+ if (!npm)
8486
+ 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");
8487
+ const layout = await resolveNpmGlobalLayout();
8488
+ return { npm, layout };
8489
+ }
8490
+ async function npmInstallGlobal(ctx, version, registry2, timeoutMs) {
8491
+ const args = [...ctx.npm.args, "install", "-g", `${RUNTIME_PACKAGE}@${version}`];
8492
+ if (registry2 && registry2 !== DEFAULT_NPM_REGISTRY)
8493
+ args.push("--registry", registry2);
8494
+ return run2(ctx.npm.command, args, { timeoutMs });
8495
+ }
8496
+ async function verifyRuntimeInstall(binPath, expected) {
8497
+ if (!existsSync16(binPath))
8498
+ return { ok: false, version: null, reason: `bin \u4E0D\u5B58\u5728\uFF1A${binPath}` };
8499
+ const res = await run2(binPath, ["--version"], { timeoutMs: 6e4 });
8500
+ if (!res.ok)
8501
+ 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"}` };
8502
+ try {
8503
+ const parsed = JSON.parse(res.stdout.trim());
8504
+ const version = typeof parsed.version === "string" ? parsed.version : null;
8505
+ if (!version)
8506
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u7F3A\u5C11 version\uFF1A${res.stdout.trim().slice(0, 200)}` };
8507
+ if (expected && version !== expected)
8508
+ return { ok: false, version, reason: `\u5B89\u88C5\u540E\u7248\u672C\u4E0D\u7B26\uFF1A\u671F\u671B ${expected}\uFF0C\u5B9E\u9645 ${version}` };
8509
+ return { ok: true, version };
8510
+ } catch {
8511
+ return { ok: false, version: null, reason: `--version \u8F93\u51FA\u975E JSON\uFF1A${res.stdout.trim().slice(0, 200)}` };
8512
+ }
8513
+ }
8514
+ async function performRuntimeUpgrade(opts) {
8515
+ const log = opts.log ?? (() => {
8516
+ });
8517
+ const registry2 = opts.registry ?? resolveRegistry();
8518
+ const steps = [];
8519
+ const from = opts.currentVersion;
8520
+ let target;
8521
+ if (opts.targetVersion) {
8522
+ target = opts.targetVersion;
8523
+ } else {
8524
+ target = await fetchLatestRuntimeVersion({ registry: registry2 });
8525
+ steps.push(`registry latest: ${target}`);
8526
+ }
8527
+ if (!isUpgradeAvailable(from, target)) {
8528
+ steps.push(`\u5F53\u524D ${from} \u5DF2\u4E0D\u4F4E\u4E8E ${target}\uFF0C\u65E0\u9700\u5347\u7EA7`);
8529
+ return { status: "up_to_date", fromVersion: from, targetVersion: target, steps };
8530
+ }
8531
+ log(`[upgrade] ${from} \u2192 ${target}\uFF08${registry2}\uFF09`);
8532
+ const ctx = await npmContext(registry2);
8533
+ steps.push(`npm install -g ${RUNTIME_PACKAGE}@${target}`);
8534
+ const install = await npmInstallGlobal(ctx, target, registry2, NPM_INSTALL_TIMEOUT_MS);
8535
+ if (!install.ok) {
8536
+ const error = `\u5B89\u88C5\u5931\u8D25\uFF1A${install.error ?? `exit ${install.code}`} ${install.stderr.trim().slice(0, 300)}`.trim();
8537
+ steps.push(error);
8538
+ log(`[upgrade] ${error}`);
8539
+ const still = await verifyRuntimeInstall(ctx.layout.runtimeBinPath);
8540
+ if (still.ok) {
8541
+ steps.push(`\u539F\u7248\u672C ${still.version ?? from} \u5B89\u88C5\u5B8C\u597D\uFF0C\u4FDD\u6301\u4E0D\u53D8`);
8542
+ return { status: "failed", fromVersion: from, toVersion: null, error, steps };
8543
+ }
8544
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8545
+ if (rollback.ok) {
8546
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from}`);
8547
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8548
+ }
8549
+ steps.push(`\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25\uFF1A${rollback.error ?? `exit ${rollback.code}`}`);
8550
+ 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 };
8551
+ }
8552
+ const verified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, target);
8553
+ if (!verified.ok) {
8554
+ const error = `\u65B0\u5B89\u88C5\u9A8C\u8BC1\u5931\u8D25\uFF1A${verified.reason ?? "\u672A\u77E5\u539F\u56E0"}`;
8555
+ steps.push(error);
8556
+ log(`[upgrade] ${error}`);
8557
+ const rollback = await npmInstallGlobal(ctx, from, registry2, NPM_ROLLBACK_TIMEOUT_MS);
8558
+ if (rollback.ok) {
8559
+ const rollbackVerified = await verifyRuntimeInstall(ctx.layout.runtimeBinPath, from);
8560
+ if (rollbackVerified.ok) {
8561
+ steps.push(`\u5DF2\u56DE\u88C5\u539F\u7248\u672C ${from} \u5E76\u9A8C\u8BC1\u53EF\u7528`);
8562
+ return { status: "rolled_back", fromVersion: from, toVersion: null, error, steps };
8563
+ }
8564
+ 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`);
8565
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u56DE\u88C5 ${from} \u540E\u9A8C\u8BC1\u4ECD\u5931\u8D25`, steps };
8566
+ }
8567
+ 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`);
8568
+ return { status: "failed", fromVersion: from, toVersion: null, error: `${error}\uFF1B\u4E14\u56DE\u88C5\u539F\u7248\u672C\u5931\u8D25`, steps };
8569
+ }
8570
+ steps.push(`\u65B0\u7248\u672C ${verified.version} \u9A8C\u8BC1\u901A\u8FC7`);
8571
+ const warnings = [];
8572
+ const setup = await run2(ctx.layout.runtimeBinPath, ["setup"], { timeoutMs: 6e4 });
8573
+ if (setup.ok) {
8574
+ steps.push("\u63D2\u4EF6\u76EE\u5F55\u5DF2\u91CD\u70D8\u7119\uFF08morula-runtime setup\uFF09");
8575
+ } else {
8576
+ 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`;
8577
+ warnings.push(warning);
8578
+ steps.push(warning);
8579
+ log(`[upgrade] ${warning}`);
8580
+ }
8581
+ log(`[upgrade] \u5347\u7EA7\u5B8C\u6210\uFF1A${from} \u2192 ${verified.version}`);
8582
+ return { status: "succeeded", fromVersion: from, toVersion: verified.version ?? target, warnings, steps };
8583
+ }
8584
+ async function spawnUpgradedDaemon(opts) {
8585
+ let packageRoot = opts.runtimePackageRoot;
8586
+ if (!packageRoot || !existsSync16(join19(packageRoot, "dist", "index.js"))) {
8587
+ const layout = await resolveNpmGlobalLayout();
8588
+ packageRoot = layout.runtimePackageRoot;
8589
+ }
8590
+ const entry = join19(packageRoot, "dist", "index.js");
8591
+ if (!existsSync16(entry))
8592
+ return { pid: 0, entry, error: `\u65B0\u5305\u5165\u53E3\u4E0D\u5B58\u5728\uFF1A${entry}`, waitExit: async () => ({ exited: true, code: null }) };
8593
+ mkdirSync13(dirname10(opts.logFile), { recursive: true, mode: 448 });
8594
+ let logFd;
8595
+ try {
8596
+ logFd = openSync4(opts.logFile, "a", 384);
8597
+ } catch (error) {
8598
+ 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 }) };
8599
+ }
8600
+ try {
8601
+ const child = spawn4(process.execPath, [entry, "daemon", "run"], {
8602
+ cwd: process.cwd(),
8603
+ detached: true,
8604
+ stdio: ["ignore", logFd, logFd],
8605
+ env: {
8606
+ ...opts.env,
8607
+ MORULA_DAEMON_NAME: opts.daemonConfig?.name ?? "",
8608
+ MORULA_DAEMON_WORKDIR: opts.daemonConfig?.workdir ?? "",
8609
+ MORULA_DAEMON_MAX_CONCURRENT: String(opts.daemonConfig?.maxConcurrent ?? ""),
8610
+ MORULA_DAEMON_RESTART_WAIT_MS: String(RESTART_LOCK_WAIT_MS)
8611
+ }
8612
+ });
8613
+ child.unref();
8614
+ if (!child.pid)
8615
+ return { pid: 0, entry, error: "spawn \u672A\u8FD4\u56DE pid", waitExit: async () => ({ exited: true, code: null }) };
8616
+ const exitPromise = new Promise((resolve2) => {
8617
+ child.on("exit", (code) => {
8618
+ resolve2(code ?? null);
8619
+ });
8620
+ });
8621
+ const waitExit = async (ms) => {
8622
+ return await Promise.race([
8623
+ exitPromise.then((code) => ({ exited: true, code })),
8624
+ new Promise((resolve2) => {
8625
+ const timer = setTimeout(() => {
8626
+ resolve2({ exited: false, code: null });
8627
+ }, ms);
8628
+ timer.unref?.();
8629
+ })
8630
+ ]);
8631
+ };
8632
+ mkdirSync13(dirname10(opts.pidFile), { recursive: true, mode: 448 });
8633
+ writeFileSync11(opts.pidFile, String(child.pid), { mode: 384 });
8634
+ return { pid: child.pid, entry, waitExit };
8635
+ } catch (error) {
8636
+ return { pid: 0, entry, error: error instanceof Error ? error.message : String(error), waitExit: async () => ({ exited: true, code: null }) };
8637
+ } finally {
8638
+ try {
8639
+ closeSync4(logFd);
8640
+ } catch {
8641
+ }
8642
+ }
8643
+ }
8644
+
8285
8645
  // src/daemon/session-host.ts
8286
8646
  init_dist2();
8287
8647
  init_acp_connection();
8288
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync11 } from "node:fs";
8289
- import { join as join19 } from "node:path";
8648
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync12 } from "node:fs";
8649
+ import { join as join20 } from "node:path";
8290
8650
 
8291
8651
  // src/daemon/steward-instructions.ts
8292
8652
  var STEWARD_SYSTEM_INSTRUCTIONS = `\u4F60\u662F\u300C\u7A7A\u95F4\u7BA1\u5BB6\u667A\u80FD\u4F53\u300D\uFF08Project Steward\uFF09\u3002
@@ -8540,8 +8900,8 @@ var SessionHost = class {
8540
8900
  async open(cmd) {
8541
8901
  if (!cmd?.sessionId || !cmd?.tenantId || !cmd?.projectId)
8542
8902
  return { ok: false, error: "SESSION_OPEN_INVALID_ARGS" };
8543
- const workspaceDir = join19(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8544
- mkdirSync13(workspaceDir, { recursive: true });
8903
+ const workspaceDir = join20(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8904
+ mkdirSync14(workspaceDir, { recursive: true });
8545
8905
  this.writeInjections(workspaceDir, cmd);
8546
8906
  const existing = this.sessions.get(cmd.sessionId);
8547
8907
  if (existing?.conn.isAlive) {
@@ -8827,7 +9187,7 @@ var SessionHost = class {
8827
9187
  ---
8828
9188
 
8829
9189
  ${cmd.prompt}`;
8830
- const run3 = (async () => {
9190
+ const run5 = (async () => {
8831
9191
  try {
8832
9192
  const { blocks, fileRefs } = await this.materializeAttachments(managed.workspaceDir, cmd.attachments ?? []);
8833
9193
  const promptBlocks = [{ type: "text", text: fileRefs.length > 0 ? `${text}
@@ -8874,8 +9234,8 @@ ${fileRefs.join("\n")}` : text }];
8874
9234
  this.touch(managed);
8875
9235
  }
8876
9236
  })();
8877
- managed.promptInFlight = run3;
8878
- return run3;
9237
+ managed.promptInFlight = run5;
9238
+ return run5;
8879
9239
  }
8880
9240
  /**
8881
9241
  * 附件物化(§五 P2-3):
@@ -8890,7 +9250,7 @@ ${fileRefs.join("\n")}` : text }];
8890
9250
  const fileRefs = [];
8891
9251
  if (attachments.length === 0)
8892
9252
  return { blocks, fileRefs };
8893
- const dir = join19(workspaceDir, "attachments");
9253
+ const dir = join20(workspaceDir, "attachments");
8894
9254
  for (const attachment of attachments) {
8895
9255
  try {
8896
9256
  const res = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
@@ -8900,9 +9260,9 @@ ${fileRefs.join("\n")}` : text }];
8900
9260
  if (attachment.isImage) {
8901
9261
  blocks.push({ type: "image", data: bytes.toString("base64"), mimeType: attachment.fileType || "image/png" });
8902
9262
  } else {
8903
- mkdirSync13(dir, { recursive: true });
8904
- const target = join19(dir, safeAttachmentName(attachment.fileName));
8905
- writeFileSync11(target, bytes);
9263
+ mkdirSync14(dir, { recursive: true });
9264
+ const target = join20(dir, safeAttachmentName(attachment.fileName));
9265
+ writeFileSync12(target, bytes);
8906
9266
  fileRefs.push(`- ${attachment.fileName} \u2192 ${target}`);
8907
9267
  }
8908
9268
  } catch (err) {
@@ -8998,13 +9358,13 @@ ${fileRefs.join("\n")}` : text }];
8998
9358
  /** 工作区注入物(幂等覆盖;daemon 侧组装,P1-6) */
8999
9359
  writeInjections(workspaceDir, cmd) {
9000
9360
  writeProjectAgentsMd(workspaceDir, cmd.agentsMd, this.log);
9001
- const skillDir = join19(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
9002
- mkdirSync13(skillDir, { recursive: true });
9003
- writeFileSync11(join19(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
9004
- const contextDir = join19(workspaceDir, ".agent_context");
9005
- mkdirSync13(contextDir, { recursive: true });
9006
- writeFileSync11(
9007
- join19(contextDir, "steward-context.md"),
9361
+ const skillDir = join20(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
9362
+ mkdirSync14(skillDir, { recursive: true });
9363
+ writeFileSync12(join20(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
9364
+ const contextDir = join20(workspaceDir, ".agent_context");
9365
+ mkdirSync14(contextDir, { recursive: true });
9366
+ writeFileSync12(
9367
+ join20(contextDir, "steward-context.md"),
9008
9368
  buildStewardContextMd(cmd.facts ?? { projectId: cmd.projectId, tenantId: cmd.tenantId }),
9009
9369
  "utf8"
9010
9370
  );
@@ -9548,6 +9908,12 @@ var DaemonRunner = class {
9548
9908
  outboxConsecutiveFailures = 0;
9549
9909
  outboxPausedUntil = 0;
9550
9910
  processing = /* @__PURE__ */ new Set();
9911
+ /**
9912
+ * runtime-self-upgrade:远程升级执行中标志。
9913
+ * 置位期间 tick / WS 派发**不领新任务**——升级完成后 daemon 会重启,中途接的任务会被打断
9914
+ * (心跳超时 → 服务端回收重派),违背「升级不破坏正在执行的任务」的约束。
9915
+ */
9916
+ upgradeInProgress = false;
9551
9917
  /** M1-A1:任务级取消信号(WS cancelled 事件 / 状态轮询触发 abort) */
9552
9918
  cancels = /* @__PURE__ */ new Map();
9553
9919
  /** Phase 2:审批决策等待表(approvalId → resolver;WS 事件 resolve,轮询兜底) */
@@ -9689,7 +10055,10 @@ var DaemonRunner = class {
9689
10055
  models: dimModels,
9690
10056
  // LOCAL-AGENT-COLLAB-001 Phase 1:宿主协议 + Provider inventory
9691
10057
  hostProtocolVersion: 1,
9692
- hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false },
10058
+ // selfUpgrade(runtime-self-upgrade 评审 P2):本 daemon 认识心跳响应里的 upgradeRequested
10059
+ // 指令并具备自升级能力。管理页据此门控远程升级按钮——旧版 daemon 会静默吞掉指令,
10060
+ // 对它显示按钮只会误导用户(能力自报比服务端维护最低版本号更准确,register 即刷新)
10061
+ hostCapabilities: { worktreeV2: true, workspaceCleanupV1: false, selfUpgrade: true },
9693
10062
  inventoryRevision: `daemon-v${BUILD_VERSION2}`,
9694
10063
  providers: inventory
9695
10064
  });
@@ -9707,10 +10076,33 @@ var DaemonRunner = class {
9707
10076
  await this.tick();
9708
10077
  if (!this.running)
9709
10078
  break;
9710
- await sleep(POLL_INTERVAL_MS);
10079
+ await this.waitForPollInterval();
9711
10080
  }
9712
10081
  this.stop();
9713
10082
  }
10083
+ /** 下一拍等待(POLL_INTERVAL_MS,可被 wakeMainLoop 提前唤醒) */
10084
+ pollWake = null;
10085
+ waitForPollInterval() {
10086
+ return new Promise((resolve2) => {
10087
+ const timer = setTimeout(() => {
10088
+ this.pollWake = null;
10089
+ resolve2();
10090
+ }, POLL_INTERVAL_MS);
10091
+ timer.unref?.();
10092
+ this.pollWake = () => {
10093
+ clearTimeout(timer);
10094
+ this.pollWake = null;
10095
+ resolve2();
10096
+ };
10097
+ });
10098
+ }
10099
+ /**
10100
+ * 唤醒主循环的 poll 等待(评审 P4 配套):远程升级完成(running=false)后立即生效,
10101
+ * 不必最多干等 15s 的 poll 间隔——旧进程越快退出释放锁,新进程越快接管,自重启窗口越小。
10102
+ */
10103
+ wakeMainLoop() {
10104
+ this.pollWake?.();
10105
+ }
9714
10106
  stop() {
9715
10107
  this.running = false;
9716
10108
  this.stopHeartbeatTimer();
@@ -9775,6 +10167,10 @@ var DaemonRunner = class {
9775
10167
  this.running = false;
9776
10168
  return;
9777
10169
  }
10170
+ if (res?.upgradeRequested) {
10171
+ this.log(`[daemon] \u6536\u5230\u670D\u52A1\u7AEF\u8FDC\u7A0B\u5347\u7EA7\u6307\u4EE4${res.upgradeVersion ? `\uFF08\u76EE\u6807 ${res.upgradeVersion}\uFF09` : ""}`);
10172
+ void this.handleRemoteUpgrade(res.upgradeVersion ?? null);
10173
+ }
9778
10174
  if (!res.ok && res.status === "unknown") {
9779
10175
  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");
9780
10176
  this.running = false;
@@ -9793,6 +10189,95 @@ var DaemonRunner = class {
9793
10189
  this.heartbeatInFlight = false;
9794
10190
  }
9795
10191
  }
10192
+ /**
10193
+ * runtime-self-upgrade:远程升级指令执行(心跳认领后异步触发,见 sendHeartbeat)。
10194
+ *
10195
+ * 边界:
10196
+ * - 有任务在跑(processing 非空)→ 回报 busy 拒绝(指令已消耗,页面可重新下发);
10197
+ * - 空闲 → performRuntimeUpgrade(安装/验证/失败回装/重烘焙插件目录);期间心跳照常、不领新任务;
10198
+ * - 成功 → 用**新包入口** detached 拉起新 daemon(等旧进程释放单实例锁)→ 回报 → 本实例退出;
10199
+ * - 失败/回退 → 回报后**继续用原版本运行**(升级失败不中断服务)。
10200
+ */
10201
+ async handleRemoteUpgrade(targetVersion) {
10202
+ if (!this.runtimeId)
10203
+ return;
10204
+ if (this.upgradeInProgress) {
10205
+ this.log("[daemon] \u5347\u7EA7\u5DF2\u5728\u8FDB\u884C\uFF0C\u5FFD\u7565\u91CD\u590D\u6307\u4EE4");
10206
+ return;
10207
+ }
10208
+ this.upgradeInProgress = true;
10209
+ const fromVersion = BUILD_VERSION2;
10210
+ try {
10211
+ if (this.processing.size > 0) {
10212
+ const error = `${this.processing.size} \u4E2A\u4EFB\u52A1\u6267\u884C\u4E2D`;
10213
+ this.log(`[daemon] \u5347\u7EA7\u6307\u4EE4\u88AB\u62D2\u7EDD\uFF1A${error}`);
10214
+ await this.reportUpgradeResultSafe({ status: "busy", fromVersion, error });
10215
+ return;
10216
+ }
10217
+ const report = await performRuntimeUpgrade({
10218
+ currentVersion: fromVersion,
10219
+ targetVersion,
10220
+ log: (line) => this.log(line)
10221
+ });
10222
+ if (report.status === "up_to_date") {
10223
+ this.log(`[daemon] \u5F53\u524D ${fromVersion} \u5DF2\u4E0D\u4F4E\u4E8E ${report.targetVersion}\uFF0C\u65E0\u9700\u5347\u7EA7`);
10224
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: fromVersion, restart: false });
10225
+ return;
10226
+ }
10227
+ if (report.status !== "succeeded") {
10228
+ this.log(`[daemon] \u5347\u7EA7\u5931\u8D25\uFF08${report.status}\uFF09\uFF1A${report.error}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10229
+ await this.reportUpgradeResultSafe({
10230
+ status: report.status,
10231
+ fromVersion,
10232
+ toVersion: report.toVersion ?? void 0,
10233
+ error: report.error
10234
+ });
10235
+ return;
10236
+ }
10237
+ const cfg = loadDaemonConfig(this.store);
10238
+ const dir = daemonDirForEnv(process.env);
10239
+ const spawned = await spawnUpgradedDaemon({
10240
+ env: process.env,
10241
+ logFile: join21(dir, "daemon.log"),
10242
+ pidFile: join21(dir, "daemon.pid"),
10243
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
10244
+ });
10245
+ if (spawned.pid <= 0) {
10246
+ 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`;
10247
+ this.log(`[daemon] ${error}`);
10248
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10249
+ return;
10250
+ }
10251
+ 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`);
10252
+ const liveness = await spawned.waitExit(RESTART_LIVENESS_CHECK_MS);
10253
+ if (liveness.exited) {
10254
+ 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`;
10255
+ this.log(`[daemon] ${error}`);
10256
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, toVersion: report.toVersion, error });
10257
+ return;
10258
+ }
10259
+ 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");
10260
+ await this.reportUpgradeResultSafe({ status: "succeeded", fromVersion, toVersion: report.toVersion, restart: true });
10261
+ this.running = false;
10262
+ this.wakeMainLoop();
10263
+ } catch (error) {
10264
+ const message = error instanceof Error ? error.message : String(error);
10265
+ this.log(`[daemon] \u81EA\u5347\u7EA7\u5F02\u5E38\uFF1A${message}\u2014\u2014\u7EE7\u7EED\u7528 ${fromVersion} \u8FD0\u884C`);
10266
+ await this.reportUpgradeResultSafe({ status: "failed", fromVersion, error: message });
10267
+ } finally {
10268
+ this.upgradeInProgress = false;
10269
+ }
10270
+ }
10271
+ /** 升级结果回报(best-effort:服务端不可达不影响本地升级流程) */
10272
+ async reportUpgradeResultSafe(body) {
10273
+ if (!this.runtimeId)
10274
+ return;
10275
+ try {
10276
+ await this.api.reportUpgradeResult(this.runtimeId, body);
10277
+ } catch (error) {
10278
+ 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)}`);
10279
+ }
10280
+ }
9796
10281
  /**
9797
10282
  * tick:poll 兜底 + 取消轮询 + cleanup 补拉(心跳与 outbox 补投均已独立于本循环)。
9798
10283
  * D1:整轮加时间预算——超预算记 WARN 并丢弃本轮等待下一拍,避免单个慢请求把 tick 拖死。
@@ -9821,6 +10306,8 @@ var DaemonRunner = class {
9821
10306
  async tickOnce() {
9822
10307
  if (!this.runtimeId)
9823
10308
  return;
10309
+ if (this.upgradeInProgress)
10310
+ return;
9824
10311
  try {
9825
10312
  const tasks = await this.api.pollTasks(this.runtimeId);
9826
10313
  for (const task of tasks) {
@@ -10220,6 +10707,10 @@ var DaemonRunner = class {
10220
10707
  async handleTask(taskId) {
10221
10708
  if (this.processing.has(taskId))
10222
10709
  return;
10710
+ if (this.upgradeInProgress) {
10711
+ this.log(`[daemon] \u5347\u7EA7\u8FDB\u884C\u4E2D\uFF0C\u5FFD\u7565\u4EFB\u52A1\u6D3E\u53D1 task=${taskId}`);
10712
+ return;
10713
+ }
10223
10714
  this.processing.add(taskId);
10224
10715
  const ctrl = new AbortController();
10225
10716
  this.cancels.set(taskId, ctrl);
@@ -10489,12 +10980,12 @@ var DaemonRunner = class {
10489
10980
  });
10490
10981
  if (spaceToken?.token)
10491
10982
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
10492
- repoDir = join20(cfg.workdir, "scratch", taskId);
10983
+ repoDir = join21(cfg.workdir, "scratch", taskId);
10493
10984
  taskRoot = repoDir;
10494
- mkdirSync14(repoDir, { recursive: true });
10495
- const readme = join20(repoDir, "README.md");
10496
- if (!existsSync16(readme)) {
10497
- writeFileSync12(
10985
+ mkdirSync15(repoDir, { recursive: true });
10986
+ const readme = join21(repoDir, "README.md");
10987
+ if (!existsSync17(readme)) {
10988
+ writeFileSync13(
10498
10989
  readme,
10499
10990
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
10500
10991
 
@@ -10792,7 +11283,7 @@ var DaemonRunner = class {
10792
11283
  }, { log: (line) => this.log(`[daemon] ${line}`) });
10793
11284
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
10794
11285
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
10795
- const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync16(resumeWorkDir);
11286
+ const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync17(resumeWorkDir);
10796
11287
  const input = {
10797
11288
  executionId: `${taskId}:1`,
10798
11289
  // task-workspace-layout:新会话 cwd = 任务工作区根(AGENTS.md/.agents 在根下,仓库在子目录)
@@ -11053,7 +11544,7 @@ var DaemonRunner = class {
11053
11544
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
11054
11545
  return;
11055
11546
  }
11056
- const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
11547
+ const managedRoot = join21(cfg.workdir, "worktrees", this.tenantId);
11057
11548
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
11058
11549
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
11059
11550
  ok: false,
@@ -11092,47 +11583,44 @@ var DaemonRunner = class {
11092
11583
  }
11093
11584
  }
11094
11585
  };
11095
- function sleep(ms) {
11096
- return new Promise((r) => setTimeout(r, ms));
11097
- }
11098
11586
  function isDebugLogEnabled() {
11099
11587
  return isFlagEnabled("MORULA_DAEMON_DEBUG_LOG", false);
11100
11588
  }
11101
11589
  var spaceTokenFailureLogged = false;
11102
11590
  function writeCustomSkills(targetDir, skills, log) {
11103
11591
  const NAME_RE = /^[\w-]{1,64}$/;
11104
- const base = join20(targetDir, ".agents", "skills");
11592
+ const base = join21(targetDir, ".agents", "skills");
11105
11593
  for (const skill of skills) {
11106
11594
  const name = skill.name.trim();
11107
11595
  if (!NAME_RE.test(name)) {
11108
11596
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
11109
11597
  continue;
11110
11598
  }
11111
- const dir = join20(base, name);
11112
- if (dirname9(dir) !== base) {
11599
+ const dir = join21(base, name);
11600
+ if (dirname11(dir) !== base) {
11113
11601
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
11114
11602
  continue;
11115
11603
  }
11116
- mkdirSync14(dir, { recursive: true });
11604
+ mkdirSync15(dir, { recursive: true });
11117
11605
  const meta = ["---", `name: ${name}`];
11118
11606
  if (skill.description?.trim()) {
11119
11607
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
11120
11608
  }
11121
11609
  meta.push("---", "");
11122
- writeFileSync12(join20(dir, "SKILL.md"), `${meta.join("\n")}
11610
+ writeFileSync13(join21(dir, "SKILL.md"), `${meta.join("\n")}
11123
11611
  ${skill.content ?? ""}`, "utf8");
11124
11612
  }
11125
11613
  }
11126
11614
  function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
11127
- const dir = join20(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11128
- const refsDir = join20(dir, "references");
11129
- mkdirSync14(refsDir, { recursive: true });
11615
+ const dir = join21(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
11616
+ const refsDir = join21(dir, "references");
11617
+ mkdirSync15(refsDir, { recursive: true });
11130
11618
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
11131
11619
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
11132
11620
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
11133
- writeFileSync12(join20(dir, "SKILL.md"), `${front}
11621
+ writeFileSync13(join21(dir, "SKILL.md"), `${front}
11134
11622
  ${body}`, "utf8");
11135
- writeFileSync12(join20(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11623
+ writeFileSync13(join21(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
11136
11624
  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/`);
11137
11625
  }
11138
11626
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -11163,8 +11651,8 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
11163
11651
  };
11164
11652
  }
11165
11653
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11166
- const briefDir = join20(targetDir, ".agent_context");
11167
- mkdirSync14(briefDir, { recursive: true });
11654
+ const briefDir = join21(targetDir, ".agent_context");
11655
+ mkdirSync15(briefDir, { recursive: true });
11168
11656
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
11169
11657
 
11170
11658
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
@@ -11174,7 +11662,7 @@ function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
11174
11662
  ---
11175
11663
 
11176
11664
  ${buildTaskPromptWithContract(detail, void 0)}`;
11177
- writeFileSync12(join20(briefDir, "task-context.md"), brief, "utf8");
11665
+ writeFileSync13(join21(briefDir, "task-context.md"), brief, "utf8");
11178
11666
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
11179
11667
  }
11180
11668
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -11188,8 +11676,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
11188
11676
  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`);
11189
11677
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
11190
11678
  }
11191
- mkdirSync14(targetDir, { recursive: true });
11192
- writeFileSync12(join20(targetDir, "AGENTS.md"), `${content}
11679
+ mkdirSync15(targetDir, { recursive: true });
11680
+ writeFileSync13(join21(targetDir, "AGENTS.md"), `${content}
11193
11681
  `, "utf8");
11194
11682
  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`);
11195
11683
  return true;
@@ -11248,7 +11736,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
11248
11736
  }
11249
11737
  async function reportOneRepoTarget(taskId, target, env, ops) {
11250
11738
  const execEnv = mergeExecEnv(env);
11251
- if (!existsSync16(target.dir) || !existsSync16(join20(target.dir, ".git"))) {
11739
+ if (!existsSync17(target.dir) || !existsSync17(join21(target.dir, ".git"))) {
11252
11740
  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`);
11253
11741
  return;
11254
11742
  }
@@ -11416,9 +11904,9 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
11416
11904
 
11417
11905
  // src/commands/doctor.ts
11418
11906
  init_dist2();
11419
- import { existsSync as existsSync17, readFileSync as readFileSync11, realpathSync } from "node:fs";
11907
+ import { existsSync as existsSync18, readFileSync as readFileSync11, realpathSync } from "node:fs";
11420
11908
  import { homedir as homedir12 } from "node:os";
11421
- import { dirname as dirname10, join as join21 } from "node:path";
11909
+ import { dirname as dirname12, join as join22 } from "node:path";
11422
11910
  init_daemon_config();
11423
11911
  init_daemon_lock();
11424
11912
  init_dim_locate();
@@ -11438,16 +11926,16 @@ async function doctorCommand(context) {
11438
11926
  async function collectDoctor(context, probe) {
11439
11927
  const checks = [];
11440
11928
  const env = context.env;
11441
- const configDir = env.MORULA_CONFIG_DIR ?? join21(homedir12(), ".morula-cli");
11442
- const configPath = env.MORULA_CONFIG ?? join21(configDir, "config.yaml");
11443
- const daemonPath = join21(dirname10(configPath), "daemon");
11444
- const logFile = join21(daemonPath, "daemon.log");
11929
+ const configDir = env.MORULA_CONFIG_DIR ?? join22(homedir12(), ".morula-cli");
11930
+ const configPath = env.MORULA_CONFIG ?? join22(configDir, "config.yaml");
11931
+ const daemonPath = join22(dirname12(configPath), "daemon");
11932
+ const logFile = join22(daemonPath, "daemon.log");
11445
11933
  const contextName = context.runtime.context ?? null;
11446
11934
  const baseUrl = context.runtime.baseUrl ?? null;
11447
11935
  const tokenPresent = Boolean(context.runtime.token);
11448
11936
  const tokenSource = context.runtime.tokenSource ?? null;
11449
11937
  let configReadable = false;
11450
- if (existsSync17(configPath)) {
11938
+ if (existsSync18(configPath)) {
11451
11939
  try {
11452
11940
  readFileSync11(configPath, "utf8");
11453
11941
  configReadable = true;
@@ -11455,9 +11943,9 @@ async function collectDoctor(context, probe) {
11455
11943
  configReadable = false;
11456
11944
  }
11457
11945
  }
11458
- const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
11946
+ const lockPath = join22(daemonPath, DAEMON_LOCK_FILE);
11459
11947
  const lock = probeDaemonLock(lockPath);
11460
- const pidFile = join21(daemonPath, DAEMON_PID_FILE);
11948
+ const pidFile = join22(daemonPath, DAEMON_PID_FILE);
11461
11949
  const filePid = readPidFile(pidFile);
11462
11950
  const daemonPid = probeDaemonInstance(configDir).pid;
11463
11951
  const cfg = loadDaemonConfig(context.store);
@@ -11509,8 +11997,8 @@ async function collectDoctor(context, probe) {
11509
11997
  hint: "morula-runtime auth login"
11510
11998
  });
11511
11999
  } else if (probe) {
11512
- const check = await probeAuth(context);
11513
- checks.push(check);
12000
+ const check2 = await probeAuth(context);
12001
+ checks.push(check2);
11514
12002
  } else {
11515
12003
  checks.push({
11516
12004
  id: "auth.token",
@@ -11519,7 +12007,7 @@ async function collectDoctor(context, probe) {
11519
12007
  hint: "morula-runtime daemon doctor --probe\uFF08\u8054\u7F51\u786E\u8BA4\u767B\u5F55\u6001\uFF09"
11520
12008
  });
11521
12009
  }
11522
- 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" });
12010
+ 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" });
11523
12011
  if (!dimFound) {
11524
12012
  checks.push({
11525
12013
  id: "dim.present",
@@ -11626,19 +12114,13 @@ function detectInstallMode() {
11626
12114
  }
11627
12115
 
11628
12116
  // src/commands/daemon.ts
11629
- function configFilePath(env) {
11630
- return env.MORULA_CONFIG ?? join22(env.MORULA_CONFIG_DIR ?? join22(homedir13(), ".morula-cli"), "config.yaml");
11631
- }
11632
- function daemonDir(configPath) {
11633
- return join22(dirname11(configPath), "daemon");
11634
- }
11635
12117
  var PID_FILE = DAEMON_PID_FILE;
11636
12118
  var LOG_FILE = "daemon.log";
11637
12119
  var DAEMON_EXIT_FALLBACK_MS = 3e3;
11638
12120
  async function daemonCommand(context) {
11639
12121
  const { action } = context.parsed;
11640
12122
  if (action === "run")
11641
- return run2(context);
12123
+ return run3(context);
11642
12124
  if (action === "start")
11643
12125
  return start(context);
11644
12126
  if (action === "stop")
@@ -11815,7 +12297,7 @@ async function models(context) {
11815
12297
  contextWindow: m.contextWindow
11816
12298
  }));
11817
12299
  }
11818
- async function run2(context) {
12300
+ async function run3(context) {
11819
12301
  rejectUnknownFlags(context.parsed.flags, ["name", "workdir", "max-concurrent"]);
11820
12302
  const armBoundedExit = () => {
11821
12303
  setTimeout(() => process.exit(0), DAEMON_EXIT_FALLBACK_MS).unref();
@@ -11828,9 +12310,18 @@ async function run2(context) {
11828
12310
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
11829
12311
  }
11830
12312
  const resolvedWorkdir = workdir ?? loadDaemonConfig(context.store)?.workdir ?? DEFAULT_WORKDIR;
11831
- mkdirSync15(resolvedWorkdir, { recursive: true });
11832
- const lockPath = join22(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
11833
- const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12313
+ mkdirSync16(resolvedWorkdir, { recursive: true });
12314
+ const lockPath = join23(daemonDirForEnv(context.env), DAEMON_LOCK_FILE);
12315
+ let acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12316
+ const restartWaitMs = Number(context.env.MORULA_DAEMON_RESTART_WAIT_MS ?? 0) || 0;
12317
+ if (!acquired.ok && restartWaitMs > 0) {
12318
+ const deadline = Date.now() + restartWaitMs;
12319
+ 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`);
12320
+ while (!acquired.ok && Date.now() < deadline) {
12321
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
12322
+ acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
12323
+ }
12324
+ }
11834
12325
  if (!acquired.ok) {
11835
12326
  const pid = acquired.existing?.pid;
11836
12327
  if (context.env.MORULA_DAEMON_SUPERVISED === "1") {
@@ -11856,7 +12347,7 @@ async function run2(context) {
11856
12347
  log: (line) => context.err(line),
11857
12348
  // dim acp 子进程治理(issues/dim-acp-process-leak-shared-space-contention.md):
11858
12349
  // spawn 记录落盘(供 daemon 重启后的启动自愈认领自己的孤儿)+ 无归属子进程周期巡检
11859
- acpChildRecordFile: join22(daemonDir(configFilePath(context.env)), "dim-acp-children.json"),
12350
+ acpChildRecordFile: join23(daemonDirForEnv(context.env), "dim-acp-children.json"),
11860
12351
  // D6:认证失败时重新读 config 换票(不传则退回「启动即快照」旧行为)。
11861
12352
  // 只重算凭证,不重建 client——WS 用返回值更新 socket.auth 后重连。
11862
12353
  resolveToken: () => {
@@ -11907,12 +12398,12 @@ async function start(context) {
11907
12398
  throw error;
11908
12399
  identityError = error instanceof Error ? error.message : String(error);
11909
12400
  }
11910
- const dir = daemonDir(configFilePath(context.env));
12401
+ const dir = daemonDirForEnv(context.env);
11911
12402
  if (!dryRun)
11912
- mkdirSync15(dir, { recursive: true, mode: 448 });
11913
- const pidFile = join22(dir, PID_FILE);
11914
- const logFile = join22(dir, LOG_FILE);
11915
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12403
+ mkdirSync16(dir, { recursive: true, mode: 448 });
12404
+ const pidFile = join23(dir, PID_FILE);
12405
+ const logFile = join23(dir, LOG_FILE);
12406
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11916
12407
  const lock = probeDaemonLock(lockPath);
11917
12408
  if (lock.state === "active") {
11918
12409
  throw new CliError(
@@ -11923,7 +12414,7 @@ async function start(context) {
11923
12414
  { hint: "morula-runtime daemon status" }
11924
12415
  );
11925
12416
  }
11926
- if (existsSync18(pidFile)) {
12417
+ if (existsSync19(pidFile)) {
11927
12418
  const pid = Number(readFileSync12(pidFile, "utf8").trim());
11928
12419
  if (isProcessAlive(pid)) {
11929
12420
  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, {
@@ -11974,19 +12465,19 @@ async function start(context) {
11974
12465
  hint: `tail -n 50 ${logFile}`
11975
12466
  });
11976
12467
  }
11977
- writeFileSync13(pidFile, String(result.pid), { mode: 384 });
12468
+ writeFileSync14(pidFile, String(result.pid), { mode: 384 });
11978
12469
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
11979
12470
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
11980
12471
  }
11981
12472
  async function stop(context) {
11982
12473
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
11983
12474
  const dryRun = isDryRun(context);
11984
- const dir = daemonDir(configFilePath(context.env));
11985
- const pidFile = join22(dir, PID_FILE);
11986
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12475
+ const dir = daemonDirForEnv(context.env);
12476
+ const pidFile = join23(dir, PID_FILE);
12477
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
11987
12478
  const lock = probeDaemonLock(lockPath);
11988
12479
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
11989
- const filePid = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
12480
+ const filePid = existsSync19(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : null;
11990
12481
  const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
11991
12482
  if (dryRun) {
11992
12483
  return {
@@ -12013,9 +12504,9 @@ async function stop(context) {
12013
12504
  async function status2(context) {
12014
12505
  rejectUnknownFlags(context.parsed.flags, []);
12015
12506
  const cfg = loadDaemonConfig(context.store);
12016
- const dir = daemonDir(configFilePath(context.env));
12017
- const lockPath = join22(dir, DAEMON_LOCK_FILE);
12018
- const probe = probeDaemonInstance(dirname11(configFilePath(context.env)));
12507
+ const dir = daemonDirForEnv(context.env);
12508
+ const lockPath = join23(dir, DAEMON_LOCK_FILE);
12509
+ const probe = probeDaemonInstance(dirname13(configFilePathForEnv(context.env)));
12019
12510
  return {
12020
12511
  running: probe.running,
12021
12512
  pid: probe.pid,
@@ -12024,7 +12515,7 @@ async function status2(context) {
12024
12515
  name: cfg?.name ?? null,
12025
12516
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
12026
12517
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
12027
- logFile: join22(dir, LOG_FILE),
12518
+ logFile: join23(dir, LOG_FILE),
12028
12519
  lockFile: lockPath
12029
12520
  };
12030
12521
  }
@@ -12081,11 +12572,228 @@ async function setupCommand(context) {
12081
12572
  };
12082
12573
  }
12083
12574
 
12575
+ // src/commands/upgrade.ts
12576
+ init_dist2();
12577
+ import { existsSync as existsSync20, mkdirSync as mkdirSync17, readFileSync as readFileSync13, rmSync as rmSync11 } from "node:fs";
12578
+ import { join as join24 } from "node:path";
12579
+ init_daemon_config();
12580
+ init_daemon_lock();
12581
+ var PID_FILE2 = DAEMON_PID_FILE;
12582
+ var LOG_FILE2 = "daemon.log";
12583
+ async function upgradeCommand(context) {
12584
+ const { action } = context.parsed;
12585
+ if (action === "check")
12586
+ return check(context);
12587
+ if (action === "run")
12588
+ return run4(context);
12589
+ throw usage("usage: morula-runtime upgrade <check|run>", {
12590
+ hint: "morula-runtime upgrade --help"
12591
+ });
12592
+ }
12593
+ async function check(context) {
12594
+ rejectUnknownFlags(context.parsed.flags, ["registry"]);
12595
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12596
+ let latest = null;
12597
+ let error = null;
12598
+ try {
12599
+ latest = await fetchLatestRuntimeVersion({ registry: registry2 });
12600
+ } catch (e) {
12601
+ error = e instanceof Error ? e.message : String(e);
12602
+ }
12603
+ if (latest === null) {
12604
+ context.setExitCode(EXIT.UPSTREAM);
12605
+ return {
12606
+ ok: false,
12607
+ current: BUILD_VERSION2,
12608
+ latest: null,
12609
+ updateAvailable: null,
12610
+ registry: registry2,
12611
+ error,
12612
+ 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"
12613
+ };
12614
+ }
12615
+ const updateAvailable = isUpgradeAvailable(BUILD_VERSION2, latest);
12616
+ return {
12617
+ ok: true,
12618
+ current: BUILD_VERSION2,
12619
+ latest,
12620
+ updateAvailable,
12621
+ registry: registry2,
12622
+ ...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" } : {}
12623
+ };
12624
+ }
12625
+ function probeDaemonPid(env) {
12626
+ const dir = daemonDirForEnv(env);
12627
+ const pidFile = join24(dir, PID_FILE2);
12628
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12629
+ const lock = probeDaemonLock(lockPath);
12630
+ const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
12631
+ const filePid = existsSync20(pidFile) ? Number(readFileSync13(pidFile, "utf8").trim()) : null;
12632
+ const pid = [lockPid, filePid].find((p) => p !== null && isProcessAlive(p)) ?? null;
12633
+ return { pid, pidFile, lockPath, lockState: lock.state };
12634
+ }
12635
+ async function reportUpgradeResult(context, body) {
12636
+ const cfg = loadDaemonConfig(context.store);
12637
+ if (!context.runtime.token || !cfg?.runtimeId)
12638
+ return;
12639
+ try {
12640
+ await context.client.http.post(`/agent-runtimes/${cfg.runtimeId}/upgrade-result`, body);
12641
+ } catch (error) {
12642
+ 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)}`);
12643
+ }
12644
+ }
12645
+ async function countActiveTasks(context, runtimeId) {
12646
+ const res = await context.client.http.get(
12647
+ "/agent-tasks",
12648
+ { runtimeId, statusGroup: "active", page: 1, pageSize: 100 }
12649
+ );
12650
+ return (res?.data ?? []).filter((t) => ["dispatched", "running", "awaiting_approval"].includes(t.status)).length;
12651
+ }
12652
+ async function run4(context) {
12653
+ rejectUnknownFlags(context.parsed.flags, ["yes", "version", "registry"]);
12654
+ const yes = flagBoolean(context.parsed.flags, "yes");
12655
+ const targetVersion = flagString(context.parsed.flags, "version") ?? null;
12656
+ const registry2 = flagString(context.parsed.flags, "registry") ?? resolveRegistry(context.env);
12657
+ if (!yes) {
12658
+ throw usage("upgrade run \u9700\u8981 --yes \u663E\u5F0F\u786E\u8BA4\uFF08\u5148\u8DD1 morula-runtime upgrade check \u67E5\u770B\u5DEE\u8DDD\uFF09", {
12659
+ hint: "morula-runtime upgrade run --yes"
12660
+ });
12661
+ }
12662
+ const dir = daemonDirForEnv(context.env);
12663
+ const pidFile = join24(dir, PID_FILE2);
12664
+ const lockPath = join24(dir, DAEMON_LOCK_FILE);
12665
+ const probe = probeDaemonPid(context.env);
12666
+ const wasRunning = probe.pid !== null;
12667
+ const cfgForSafety = loadDaemonConfig(context.store);
12668
+ let activeTasks = 0;
12669
+ if (wasRunning && cfgForSafety?.runtimeId) {
12670
+ if (!context.runtime.token) {
12671
+ throw new CliError(
12672
+ "TASKS_UNKNOWN",
12673
+ EXIT.UNAUTHENTICATED,
12674
+ "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",
12675
+ false,
12676
+ { hint: "morula-runtime auth login \u540E\u91CD\u8BD5\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12677
+ );
12678
+ }
12679
+ try {
12680
+ activeTasks = await countActiveTasks(context, cfgForSafety.runtimeId);
12681
+ } catch (error) {
12682
+ throw new CliError(
12683
+ "TASKS_UNKNOWN",
12684
+ EXIT.UPSTREAM,
12685
+ `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`,
12686
+ false,
12687
+ { 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" }
12688
+ );
12689
+ }
12690
+ }
12691
+ if (activeTasks > 0) {
12692
+ throw new CliError(
12693
+ "TASKS_RUNNING",
12694
+ EXIT.CONFLICT,
12695
+ `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`,
12696
+ false,
12697
+ { hint: "\u7B49\u4EFB\u52A1\u5B8C\u6210\u540E\u518D\u5347\u7EA7\uFF1B\u6216\u786E\u8BA4\u53EF\u4E2D\u65AD\u540E\u5148 morula-runtime daemon stop" }
12698
+ );
12699
+ }
12700
+ const report = await performRuntimeUpgrade({
12701
+ currentVersion: BUILD_VERSION2,
12702
+ targetVersion,
12703
+ registry: registry2,
12704
+ log: (line) => context.err(line)
12705
+ });
12706
+ if (report.status === "up_to_date") {
12707
+ await reportUpgradeResult(context, { status: "succeeded", fromVersion: BUILD_VERSION2, toVersion: BUILD_VERSION2, restart: false });
12708
+ return { ok: true, upToDate: true, current: BUILD_VERSION2, target: report.targetVersion, steps: report.steps };
12709
+ }
12710
+ if (report.status !== "succeeded") {
12711
+ await reportUpgradeResult(context, { status: report.status, fromVersion: report.fromVersion, toVersion: report.toVersion ?? void 0, error: report.error });
12712
+ context.setExitCode(EXIT.OTHER);
12713
+ return {
12714
+ ok: false,
12715
+ status: report.status,
12716
+ from: report.fromVersion,
12717
+ to: report.toVersion,
12718
+ error: report.error,
12719
+ steps: report.steps,
12720
+ 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"
12721
+ };
12722
+ }
12723
+ const cfg = loadDaemonConfig(context.store);
12724
+ let restart = null;
12725
+ let deferredRestart = false;
12726
+ if (wasRunning) {
12727
+ if (cfgForSafety?.runtimeId && context.runtime.token) {
12728
+ const stillBusy = await countActiveTasks(context, cfgForSafety.runtimeId);
12729
+ if (stillBusy > 0) {
12730
+ deferredRestart = true;
12731
+ 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`);
12732
+ }
12733
+ }
12734
+ if (!deferredRestart) {
12735
+ if (probe.pid !== null) {
12736
+ const stopped = await stopDaemonByPid(probe.pid);
12737
+ context.err(`[upgrade] \u65E7 daemon\uFF08pid=${probe.pid}\uFF09\u5DF2\u505C\u6B62\uFF08${stopped.outcome}\uFF09`);
12738
+ }
12739
+ if (probe.lockState === "stale")
12740
+ releaseDaemonLock(lockPath);
12741
+ else if (probe.pid !== null)
12742
+ releaseDaemonLock(lockPath, probe.pid);
12743
+ rmSync11(pidFile, { force: true });
12744
+ mkdirSync17(dir, { recursive: true, mode: 448 });
12745
+ restart = await spawnUpgradedDaemon({
12746
+ env: context.env,
12747
+ logFile: join24(dir, LOG_FILE2),
12748
+ pidFile,
12749
+ daemonConfig: { name: cfg?.name, workdir: cfg?.workdir, maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT }
12750
+ });
12751
+ if (restart.pid > 0) {
12752
+ const liveness = await restart.waitExit(RESTART_LIVENESS_CHECK_MS);
12753
+ if (liveness.exited) {
12754
+ 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`);
12755
+ 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 }) };
12756
+ } else {
12757
+ context.err(`[upgrade] \u65B0 daemon \u5DF2\u62C9\u8D77\uFF08pid=${restart.pid}\uFF09\uFF0C\u65E5\u5FD7: ${join24(dir, LOG_FILE2)}`);
12758
+ }
12759
+ } else {
12760
+ 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`);
12761
+ }
12762
+ }
12763
+ } else {
12764
+ 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");
12765
+ }
12766
+ const spawnFailed = wasRunning && !deferredRestart && restart !== null && restart.pid <= 0;
12767
+ await reportUpgradeResult(context, {
12768
+ status: spawnFailed ? "failed" : "succeeded",
12769
+ fromVersion: report.fromVersion,
12770
+ toVersion: report.toVersion,
12771
+ restart: !spawnFailed && wasRunning && !deferredRestart,
12772
+ ...spawnFailed ? { error: `\u65B0\u7248\u672C\u5DF2\u5B89\u88C5\u4F46\u81EA\u52A8\u91CD\u542F\u5931\u8D25\uFF1A${restart?.error ?? "\u672A\u77E5\u539F\u56E0"}` } : {}
12773
+ });
12774
+ if (spawnFailed) {
12775
+ context.setExitCode(EXIT.OTHER);
12776
+ }
12777
+ return {
12778
+ ok: !spawnFailed,
12779
+ status: spawnFailed ? "restart_failed" : deferredRestart ? "installed_pending_restart" : "succeeded",
12780
+ from: report.fromVersion,
12781
+ to: report.toVersion,
12782
+ // deferredRestart(评审 P3):安装期有任务被派发 → 不打断 daemon,新版本下次重启生效
12783
+ restarted: !spawnFailed && wasRunning && !deferredRestart,
12784
+ pid: !spawnFailed && wasRunning && !deferredRestart ? restart?.pid ?? null : null,
12785
+ ...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" } : {},
12786
+ warnings: report.warnings,
12787
+ steps: report.steps
12788
+ };
12789
+ }
12790
+
12084
12791
  // src/commands/registry-runtime.ts
12085
12792
  var COMMANDS = {
12086
12793
  auth: authCommand,
12087
12794
  daemon: daemonCommand,
12088
- setup: setupCommand
12795
+ setup: setupCommand,
12796
+ upgrade: upgradeCommand
12089
12797
  };
12090
12798
  var EXIT_CODES = {
12091
12799
  0: "success",
@@ -12107,7 +12815,7 @@ var GLOBAL_FLAGS2 = {
12107
12815
  };
12108
12816
  var SPEC = {
12109
12817
  name: "morula-runtime",
12110
- 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.",
12818
+ 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.",
12111
12819
  resources: [
12112
12820
  {
12113
12821
  resource: "auth",
@@ -12278,6 +12986,44 @@ var SPEC = {
12278
12986
  "canonical form is the bare `morula-runtime setup`; postinstall runs the same registration automatically",
12279
12987
  "only writes <root>/morula-cli; never deletes other plugins or the legacy ~/.agents/plugins/morula-cli (reported as legacyRoot)"
12280
12988
  ]
12989
+ },
12990
+ {
12991
+ resource: "upgrade",
12992
+ 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)",
12993
+ actions: [
12994
+ {
12995
+ action: "check",
12996
+ summary: "read-only: compare current version with the latest published version on the npm registry (one network call)",
12997
+ flags: [
12998
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to query (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
12999
+ ],
13000
+ examples: ["morula-runtime upgrade check"],
13001
+ writes: "none",
13002
+ interactive: false,
13003
+ idempotent: true,
13004
+ emits: ["ok", "current", "latest", "updateAvailable", "registry"]
13005
+ },
13006
+ {
13007
+ action: "run",
13008
+ 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",
13009
+ flags: [
13010
+ { name: "yes", type: "boolean", description: "REQUIRED: explicit confirmation; without it the command refuses (exit 2). No silent auto-upgrade" },
13011
+ { name: "version", type: "string", valueHint: "<semver>", description: "target version (default: latest from the registry)" },
13012
+ { name: "registry", type: "string", valueHint: "<url>", description: "npm registry to install from (default https://registry.npmjs.org, or $MORULA_NPM_REGISTRY)" }
13013
+ ],
13014
+ examples: ["morula-runtime upgrade run --yes", "morula-runtime upgrade run --yes --version 0.1.0-alpha.16"],
13015
+ writes: "local",
13016
+ interactive: false,
13017
+ idempotent: false,
13018
+ emits: ["ok", "status", "from", "to", "restarted", "pid", "steps"]
13019
+ }
13020
+ ],
13021
+ notes: [
13022
+ "check: exit 0 = compared (see updateAvailable); 7 = registry unreachable (see error/hint)",
13023
+ "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)",
13024
+ "the daemon restart only happens when the daemon was running before the upgrade; a stopped daemon stays stopped",
13025
+ "a stopped daemon is upgraded but not started \u2014 start it manually with morula-runtime daemon start"
13026
+ ]
12281
13027
  }
12282
13028
  ]
12283
13029
  };