@arcships/morula-runtime 0.1.0-alpha.7 → 0.1.0-alpha.9

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
@@ -72,6 +72,11 @@ function parseArgs(argv) {
72
72
  const positionals = [];
73
73
  const flags = {};
74
74
  const bareFlags = [];
75
+ const valueOccurrences = {};
76
+ const recordValue = (key, value) => {
77
+ flags[key] = value;
78
+ (valueOccurrences[key] ??= []).push(value);
79
+ };
75
80
  let positionalOnly = false;
76
81
  for (let index = 0; index < argv.length; index += 1) {
77
82
  const value = argv[index];
@@ -93,7 +98,7 @@ function parseArgs(argv) {
93
98
  }
94
99
  const equal = value.indexOf("=");
95
100
  if (equal > 2) {
96
- flags[value.slice(2, equal)] = value.slice(equal + 1);
101
+ recordValue(value.slice(2, equal), value.slice(equal + 1));
97
102
  continue;
98
103
  }
99
104
  const key = value.slice(2);
@@ -101,19 +106,25 @@ function parseArgs(argv) {
101
106
  throw usage("invalid empty flag");
102
107
  const next = argv[index + 1];
103
108
  if (next !== void 0 && !next.startsWith("--")) {
104
- flags[key] = next;
109
+ recordValue(key, next);
105
110
  index += 1;
106
111
  } else {
107
112
  flags[key] = true;
108
113
  bareFlags.push(key);
109
114
  }
110
115
  }
116
+ const repeatedFlags = {};
117
+ for (const [key, values] of Object.entries(valueOccurrences)) {
118
+ if (values.length > 1)
119
+ repeatedFlags[key] = values;
120
+ }
111
121
  return {
112
122
  command: positionals.shift(),
113
123
  action: positionals.shift(),
114
124
  positionals,
115
125
  flags,
116
- bareFlags
126
+ bareFlags,
127
+ repeatedFlags
117
128
  };
118
129
  }
119
130
  function flagString(flags, ...names) {
@@ -729,7 +740,7 @@ var BUILD_ID, BUILD_CONTEXTS, BUILD_DEFAULT_CONTEXT;
729
740
  var init_build_env = __esm({
730
741
  "../morula-cli-core/dist/build-env.js"() {
731
742
  "use strict";
732
- BUILD_ID = "20260917073516.nogit";
743
+ BUILD_ID = "20260921135326.48e72dd8";
733
744
  BUILD_CONTEXTS = {
734
745
  "local": "http://localhost:8080/api",
735
746
  "test": "https://morula.basecastle.com/api",
@@ -739,6 +750,48 @@ var init_build_env = __esm({
739
750
  }
740
751
  });
741
752
 
753
+ // ../morula-api-client/dist/agent-task.js
754
+ function requiredId(id) {
755
+ const normalized = id.trim();
756
+ if (!normalized)
757
+ throw new TypeError("agent task id is required");
758
+ return normalized;
759
+ }
760
+ var AgentTaskApi;
761
+ var init_agent_task = __esm({
762
+ "../morula-api-client/dist/agent-task.js"() {
763
+ "use strict";
764
+ AgentTaskApi = class {
765
+ client;
766
+ constructor(client) {
767
+ this.client = client;
768
+ }
769
+ /** 列出 AgentTask(objectId 必填;status/page/pageSize 可选) */
770
+ list(query = {}) {
771
+ return this.client.get("/agent-tasks", query);
772
+ }
773
+ /**
774
+ * 取消执行。
775
+ *
776
+ * 服务端语义(`agent-task.controller.ts` cancel,2026-09-20 起):
777
+ * - 成功 → 200 `{ ok: true, message: 'Task cancelled' }`;
778
+ * - 任务不存在 → **404** `NOT_FOUND`;
779
+ * - 状态不允许(`completed` / `cancelled`)→ **409** `CONFLICT`(带可读原因)。
780
+ *
781
+ * 即:**失败会 reject**(不再 200 假成功,见 issues/agent-task-cancel-returns-200-on-failure.md)——
782
+ * 调用方按异常/退出码判断,不要假定「调用返回了就是已取消」。
783
+ */
784
+ cancel(id) {
785
+ return this.client.post(`/agent-tasks/${encodeURIComponent(requiredId(id))}/cancel`);
786
+ }
787
+ /** 重跑失败/已取消的执行(服务端复制原任务重新入队,返回**新** taskId) */
788
+ rerun(id) {
789
+ return this.client.post(`/agent-tasks/${encodeURIComponent(requiredId(id))}/rerun`);
790
+ }
791
+ };
792
+ }
793
+ });
794
+
742
795
  // ../morula-api-client/dist/attachment.js
743
796
  var AttachmentApi;
744
797
  var init_attachment = __esm({
@@ -788,6 +841,13 @@ var init_attachment = __esm({
788
841
  getDownloadUrl(id) {
789
842
  return this.client.get(`/attachments/${encodeURIComponent(id)}/download-url`);
790
843
  }
844
+ /**
845
+ * 读取附件的解析产物全文(MinerU markdown)。
846
+ * 未解析/解析中/解析失败由服务端在返回体里说明,不当作 HTTP 错误。
847
+ */
848
+ content(id, maxChars) {
849
+ return this.client.get(`/attachments/${encodeURIComponent(id)}/content`, { maxChars });
850
+ }
791
851
  /** 更新附件关联的资源 ID */
792
852
  updateResourceId(id, resourceId) {
793
853
  return this.client.patch(`/attachments/${encodeURIComponent(id)}`, { resourceId });
@@ -826,7 +886,115 @@ var init_auth = __esm({
826
886
  }
827
887
  });
828
888
 
889
+ // ../morula-api-client/dist/autopilot.js
890
+ var AutopilotApi;
891
+ var init_autopilot = __esm({
892
+ "../morula-api-client/dist/autopilot.js"() {
893
+ "use strict";
894
+ AutopilotApi = class {
895
+ client;
896
+ constructor(client) {
897
+ this.client = client;
898
+ }
899
+ list(query = {}) {
900
+ return this.client.get("/agent-autopilots", query);
901
+ }
902
+ create(input) {
903
+ return this.client.post("/agent-autopilots", input);
904
+ }
905
+ };
906
+ }
907
+ });
908
+
909
+ // ../morula-api-client/dist/ci.js
910
+ function requiredId2(id, label) {
911
+ const normalized = id.trim();
912
+ if (!normalized)
913
+ throw new TypeError(`${label} is required`);
914
+ return normalized;
915
+ }
916
+ var CiApi;
917
+ var init_ci = __esm({
918
+ "../morula-api-client/dist/ci.js"() {
919
+ "use strict";
920
+ CiApi = class {
921
+ client;
922
+ constructor(client) {
923
+ this.client = client;
924
+ }
925
+ /** 任务面 CI 视图(本任务关联的流水线运行 + 部署记录 + 归属依据) */
926
+ taskView(taskId, query = {}) {
927
+ return this.client.get(`/agent-tasks/${encodeURIComponent(requiredId2(taskId, "task id"))}/vcs/ci`, query);
928
+ }
929
+ /** 任务面部署记录(D3:仅展示,不驱动流转) */
930
+ taskDeployments(taskId) {
931
+ return this.client.get(`/agent-tasks/${encodeURIComponent(requiredId2(taskId, "task id"))}/vcs/ci/deployments`);
932
+ }
933
+ /** 空间面 CI 巡检视图(管家主入口;`ref` / `failed` / `limit` 为服务端过滤) */
934
+ spaceView(spaceId, query = {}) {
935
+ return this.client.get(`/vcs/spaces/${encodeURIComponent(requiredId2(spaceId, "space id"))}/ci`, query);
936
+ }
937
+ /** 空间面部署记录(D3:仅展示) */
938
+ spaceDeployments(spaceId) {
939
+ return this.client.get(`/vcs/spaces/${encodeURIComponent(requiredId2(spaceId, "space id"))}/ci/deployments`);
940
+ }
941
+ /**
942
+ * job 日志(按 job id;服务端反查所属运行)。
943
+ *
944
+ * 两个面共用本端点:任务凭证按 tenantId 收敛,会话凭证按 `connection.spaceId === scopeId` 收敛。
945
+ * `truncated` 显式返回——日志被尾部截断时调用方必须知道(不能把截断当全文)。
946
+ */
947
+ jobLog(jobId, query = {}) {
948
+ return this.client.get(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/log`, query);
949
+ }
950
+ /** 失败用例清单(平台可选能力:GitLab test_report 有、GitHub 无 → `CAPABILITY_UNSUPPORTED`) */
951
+ testReport(runId) {
952
+ return this.client.get(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/test-report`);
953
+ }
954
+ /**
955
+ * 重跑整条流水线的失败 job(Phase 3;开放面 = 人 + 管家)。
956
+ *
957
+ * 只允许 failure / cancelled 的运行,其余状态服务端回 `CI_RETRY_NOT_ALLOWED`(不是 4xx)。
958
+ * **非幂等**:每次调用都会在平台上产生一次新的重跑,重复调用不会去重。
959
+ */
960
+ retryRun(runId) {
961
+ return this.client.post(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/retry`);
962
+ }
963
+ /**
964
+ * 重跑单个 job(Phase 3;开放面 = 人 + 管家)。
965
+ *
966
+ * 服务端按 jobId 反查所属运行(`CI_JOB_NOT_FOUND` 覆盖「job 不存在」与「反查不到运行」两种情况);
967
+ * 状态门禁与 `retryRun` 相同。非幂等。
968
+ */
969
+ retryJob(jobId) {
970
+ return this.client.post(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/retry`);
971
+ }
972
+ };
973
+ }
974
+ });
975
+
829
976
  // ../morula-api-client/dist/client.js
977
+ function describeFetchFailure(error) {
978
+ const message = error instanceof Error ? error.message : String(error);
979
+ const detail = formatFetchCause(error?.cause);
980
+ return detail ? `${message} (cause: ${detail})` : message;
981
+ }
982
+ function formatFetchCause(cause) {
983
+ if (cause === void 0 || cause === null)
984
+ return "";
985
+ if (cause instanceof Error) {
986
+ const detail = cause;
987
+ const parts = [
988
+ typeof detail.code === "string" ? detail.code : "",
989
+ typeof detail.errno === "number" || typeof detail.errno === "string" ? `errno=${detail.errno}` : "",
990
+ typeof detail.syscall === "string" ? `syscall=${detail.syscall}` : "",
991
+ // cause 的 message 常与外层重复('fetch failed'),只有不同才带
992
+ detail.message && detail.message !== "fetch failed" ? detail.message : ""
993
+ ].filter(Boolean);
994
+ return parts.join(" ").slice(0, CAUSE_DETAIL_MAX_LENGTH);
995
+ }
996
+ return String(cause).slice(0, CAUSE_DETAIL_MAX_LENGTH);
997
+ }
830
998
  function sanitizeHeaderValue(raw, maxLength) {
831
999
  if (!raw)
832
1000
  return void 0;
@@ -870,28 +1038,35 @@ function isEnvelope(value) {
870
1038
  function errorFromResponse(status3, parsed, raw) {
871
1039
  const message = extractMessage(parsed, raw, status3);
872
1040
  const details = isEnvelope(parsed) ? parsed.data : parsed;
1041
+ const errorCode = extractErrorCode(parsed);
873
1042
  if (status3 === 400 || status3 === 422) {
874
- return new MorulaApiError("BAD_REQUEST", message, EXIT_CODE.USAGE, status3, false, details);
1043
+ return new MorulaApiError("BAD_REQUEST", message, EXIT_CODE.USAGE, status3, false, details, errorCode);
875
1044
  }
876
1045
  if (status3 === 401) {
877
- return new MorulaApiError("UNAUTHENTICATED", message, EXIT_CODE.UNAUTHENTICATED, status3, false, details);
1046
+ return new MorulaApiError("UNAUTHENTICATED", message, EXIT_CODE.UNAUTHENTICATED, status3, false, details, errorCode);
878
1047
  }
879
1048
  if (status3 === 403) {
880
- return new MorulaApiError("FORBIDDEN", message, EXIT_CODE.FORBIDDEN, status3, false, details);
1049
+ return new MorulaApiError("FORBIDDEN", message, EXIT_CODE.FORBIDDEN, status3, false, details, errorCode);
881
1050
  }
882
1051
  if (status3 === 404) {
883
- return new MorulaApiError("NOT_FOUND", message, EXIT_CODE.NOT_FOUND, status3, false, details);
1052
+ return new MorulaApiError("NOT_FOUND", message, EXIT_CODE.NOT_FOUND, status3, false, details, errorCode);
884
1053
  }
885
1054
  if (status3 === 409) {
886
- return new MorulaApiError("CONFLICT", message, EXIT_CODE.CONFLICT, status3, false, details);
1055
+ return new MorulaApiError("CONFLICT", message, EXIT_CODE.CONFLICT, status3, false, details, errorCode);
887
1056
  }
888
1057
  if (status3 === 429) {
889
- return new MorulaApiError("RATE_LIMITED", message, EXIT_CODE.UPSTREAM, status3, true, details);
1058
+ return new MorulaApiError("RATE_LIMITED", message, EXIT_CODE.UPSTREAM, status3, true, details, errorCode);
890
1059
  }
891
1060
  if (status3 >= 500) {
892
- return new MorulaApiError("UPSTREAM", message, EXIT_CODE.UPSTREAM, status3, true, details);
1061
+ return new MorulaApiError("UPSTREAM", message, EXIT_CODE.UPSTREAM, status3, true, details, errorCode);
893
1062
  }
894
- return new MorulaApiError("HTTP_ERROR", message, EXIT_CODE.OTHER, status3, false, details);
1063
+ return new MorulaApiError("HTTP_ERROR", message, EXIT_CODE.OTHER, status3, false, details, errorCode);
1064
+ }
1065
+ function extractErrorCode(parsed) {
1066
+ if (!parsed || typeof parsed !== "object")
1067
+ return void 0;
1068
+ const value = parsed.errorCode;
1069
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
895
1070
  }
896
1071
  function extractMessage(parsed, raw, status3) {
897
1072
  if (parsed && typeof parsed === "object" && "message" in parsed) {
@@ -907,7 +1082,7 @@ function extractMessage(parsed, raw, status3) {
907
1082
  return raw.slice(0, 500);
908
1083
  return `HTTP ${status3}`;
909
1084
  }
910
- var EXIT_CODE, MorulaApiError, CLI_BUILD_HEADER, CLI_CAPS_HEADER, CLI_BUILD_MAX_LENGTH, CLI_CAPS_MAX_LENGTH, DEFAULT_TIMEOUT_MS, MorulaApiClient;
1085
+ var EXIT_CODE, MorulaApiError, CLI_BUILD_HEADER, CLI_CAPS_HEADER, CLI_BUILD_MAX_LENGTH, CLI_CAPS_MAX_LENGTH, DEFAULT_TIMEOUT_MS, CAUSE_DETAIL_MAX_LENGTH, MorulaApiClient;
911
1086
  var init_client = __esm({
912
1087
  "../morula-api-client/dist/client.js"() {
913
1088
  "use strict";
@@ -927,14 +1102,16 @@ var init_client = __esm({
927
1102
  status;
928
1103
  retryable;
929
1104
  details;
1105
+ errorCode;
930
1106
  name = "MorulaApiError";
931
- constructor(code, message, exitCode, status3, retryable = false, details) {
1107
+ constructor(code, message, exitCode, status3, retryable = false, details, errorCode) {
932
1108
  super(message);
933
1109
  this.code = code;
934
1110
  this.exitCode = exitCode;
935
1111
  this.status = status3;
936
1112
  this.retryable = retryable;
937
1113
  this.details = details;
1114
+ this.errorCode = errorCode;
938
1115
  }
939
1116
  };
940
1117
  CLI_BUILD_HEADER = "X-Morula-Cli-Build";
@@ -942,6 +1119,7 @@ var init_client = __esm({
942
1119
  CLI_BUILD_MAX_LENGTH = 64;
943
1120
  CLI_CAPS_MAX_LENGTH = 255;
944
1121
  DEFAULT_TIMEOUT_MS = 3e4;
1122
+ CAUSE_DETAIL_MAX_LENGTH = 200;
945
1123
  MorulaApiClient = class {
946
1124
  baseUrl;
947
1125
  token;
@@ -1039,7 +1217,7 @@ var init_client = __esm({
1039
1217
  if (controller.signal.aborted) {
1040
1218
  throw new MorulaApiError("TIMEOUT", `request timed out after ${timeoutMs}ms`, EXIT_CODE.UPSTREAM, void 0, true);
1041
1219
  }
1042
- throw new MorulaApiError("NETWORK", `request failed: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.UPSTREAM, void 0, true);
1220
+ throw new MorulaApiError("NETWORK", `request failed: ${describeFetchFailure(error)}`, EXIT_CODE.UPSTREAM, void 0, true);
1043
1221
  } finally {
1044
1222
  clearTimeout(timer);
1045
1223
  }
@@ -1061,7 +1239,7 @@ var init_client = __esm({
1061
1239
  });
1062
1240
 
1063
1241
  // ../morula-api-client/dist/comment.js
1064
- function requiredId(id) {
1242
+ function requiredId3(id) {
1065
1243
  const normalized = id.trim();
1066
1244
  if (!normalized)
1067
1245
  throw new TypeError("comment id is required");
@@ -1083,23 +1261,142 @@ var init_comment = __esm({
1083
1261
  return this.client.get("/comments", params);
1084
1262
  }
1085
1263
  get(id) {
1086
- return this.client.get(`/comments/${encodeURIComponent(requiredId(id))}`);
1264
+ return this.client.get(`/comments/${encodeURIComponent(requiredId3(id))}`);
1087
1265
  }
1088
1266
  create(input) {
1089
1267
  return this.client.post("/comments", input);
1090
1268
  }
1091
1269
  update(id, input) {
1092
- return this.client.patch(`/comments/${encodeURIComponent(requiredId(id))}`, input);
1270
+ return this.client.patch(`/comments/${encodeURIComponent(requiredId3(id))}`, input);
1093
1271
  }
1094
1272
  delete(id) {
1095
- return this.client.delete(`/comments/${encodeURIComponent(requiredId(id))}`);
1273
+ return this.client.delete(`/comments/${encodeURIComponent(requiredId3(id))}`);
1274
+ }
1275
+ };
1276
+ }
1277
+ });
1278
+
1279
+ // ../morula-api-client/dist/dashboard.js
1280
+ function requiredId4(id) {
1281
+ const normalized = id.trim();
1282
+ if (!normalized)
1283
+ throw new TypeError("space id is required");
1284
+ return normalized;
1285
+ }
1286
+ var DashboardApi;
1287
+ var init_dashboard = __esm({
1288
+ "../morula-api-client/dist/dashboard.js"() {
1289
+ "use strict";
1290
+ DashboardApi = class {
1291
+ client;
1292
+ constructor(client) {
1293
+ this.client = client;
1294
+ }
1295
+ get(spaceId) {
1296
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/dashboard`);
1297
+ }
1298
+ statistics(spaceId) {
1299
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/statistics`);
1300
+ }
1301
+ workload(spaceId) {
1302
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/workload`);
1303
+ }
1304
+ };
1305
+ }
1306
+ });
1307
+
1308
+ // ../morula-api-client/dist/document.js
1309
+ function requiredId5(id) {
1310
+ const normalized = String(id).trim();
1311
+ if (!normalized)
1312
+ throw new TypeError("document id is required");
1313
+ return normalized;
1314
+ }
1315
+ var DocumentApi;
1316
+ var init_document = __esm({
1317
+ "../morula-api-client/dist/document.js"() {
1318
+ "use strict";
1319
+ DocumentApi = class {
1320
+ client;
1321
+ constructor(client) {
1322
+ this.client = client;
1323
+ }
1324
+ list(query = {}) {
1325
+ return this.client.get("/documents", query);
1326
+ }
1327
+ get(id) {
1328
+ return this.client.get(`/documents/${encodeURIComponent(requiredId5(id))}`);
1329
+ }
1330
+ /** 已解析归档的文档内容(Markdown 正文等) */
1331
+ artifacts(id) {
1332
+ return this.client.get(`/documents/${encodeURIComponent(requiredId5(id))}/artifacts`);
1333
+ }
1334
+ };
1335
+ }
1336
+ });
1337
+
1338
+ // ../morula-api-client/dist/object-meta.js
1339
+ function requiredId6(id) {
1340
+ const normalized = id.trim();
1341
+ if (!normalized)
1342
+ throw new TypeError("object id is required");
1343
+ return normalized;
1344
+ }
1345
+ function requiredKey(key) {
1346
+ const normalized = key.trim();
1347
+ if (!normalized)
1348
+ throw new TypeError("meta key is required");
1349
+ return normalized;
1350
+ }
1351
+ var ObjectMetaApi;
1352
+ var init_object_meta = __esm({
1353
+ "../morula-api-client/dist/object-meta.js"() {
1354
+ "use strict";
1355
+ ObjectMetaApi = class {
1356
+ client;
1357
+ constructor(client) {
1358
+ this.client = client;
1359
+ }
1360
+ list(objectId) {
1361
+ return this.client.get(`/objects/${encodeURIComponent(requiredId6(objectId))}/agent-meta`);
1362
+ }
1363
+ set(objectId, key, value) {
1364
+ return this.client.request(`/objects/${encodeURIComponent(requiredId6(objectId))}/agent-meta/${encodeURIComponent(requiredKey(key))}`, { method: "PUT", body: { value } });
1365
+ }
1366
+ };
1367
+ }
1368
+ });
1369
+
1370
+ // ../morula-api-client/dist/repo.js
1371
+ function requiredId7(id) {
1372
+ const normalized = id.trim();
1373
+ if (!normalized)
1374
+ throw new TypeError("space id is required");
1375
+ return normalized;
1376
+ }
1377
+ var RepoApi;
1378
+ var init_repo = __esm({
1379
+ "../morula-api-client/dist/repo.js"() {
1380
+ "use strict";
1381
+ RepoApi = class {
1382
+ client;
1383
+ constructor(client) {
1384
+ this.client = client;
1385
+ }
1386
+ /** 目录清单(path 缺省 = 仓库根) */
1387
+ files(spaceId, query = {}) {
1388
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId7(spaceId))}/repo/files`, query);
1389
+ }
1390
+ /** 单文件内容(path 必填) */
1391
+ read(spaceId, query) {
1392
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId7(spaceId))}/repo/file`, query);
1096
1393
  }
1097
1394
  };
1098
1395
  }
1099
1396
  });
1100
1397
 
1101
1398
  // ../morula-api-client/dist/report.js
1102
- function requiredId2(id, label = "report") {
1399
+ function requiredId8(id, label = "report") {
1103
1400
  const normalized = id.trim();
1104
1401
  if (!normalized)
1105
1402
  throw new TypeError(`${label} id is required`);
@@ -1118,17 +1415,17 @@ var init_report = __esm({
1118
1415
  return this.client.post("/agent-reports", input);
1119
1416
  }
1120
1417
  get(id) {
1121
- return this.client.get(`/agent-reports/${encodeURIComponent(requiredId2(id))}`);
1418
+ return this.client.get(`/agent-reports/${encodeURIComponent(requiredId8(id))}`);
1122
1419
  }
1123
1420
  listByAutopilot(autopilotId, query = {}) {
1124
- return this.client.get(`/agent-autopilots/${encodeURIComponent(requiredId2(autopilotId, "autopilot"))}/reports`, { ...query });
1421
+ return this.client.get(`/agent-autopilots/${encodeURIComponent(requiredId8(autopilotId, "autopilot"))}/reports`, { ...query });
1125
1422
  }
1126
1423
  };
1127
1424
  }
1128
1425
  });
1129
1426
 
1130
1427
  // ../morula-api-client/dist/space-member.js
1131
- function requiredId3(id) {
1428
+ function requiredId9(id) {
1132
1429
  const normalized = id.trim();
1133
1430
  if (!normalized)
1134
1431
  throw new TypeError("space member id is required");
@@ -1147,23 +1444,23 @@ var init_space_member = __esm({
1147
1444
  return this.client.get("/space-members", { spaceId, ...query });
1148
1445
  }
1149
1446
  get(id) {
1150
- return this.client.get(`/space-members/${encodeURIComponent(requiredId3(id))}`);
1447
+ return this.client.get(`/space-members/${encodeURIComponent(requiredId9(id))}`);
1151
1448
  }
1152
1449
  create(input) {
1153
1450
  return this.client.post("/space-members", input);
1154
1451
  }
1155
1452
  update(id, input) {
1156
- return this.client.patch(`/space-members/${encodeURIComponent(requiredId3(id))}`, input);
1453
+ return this.client.patch(`/space-members/${encodeURIComponent(requiredId9(id))}`, input);
1157
1454
  }
1158
1455
  delete(id) {
1159
- return this.client.delete(`/space-members/${encodeURIComponent(requiredId3(id))}`);
1456
+ return this.client.delete(`/space-members/${encodeURIComponent(requiredId9(id))}`);
1160
1457
  }
1161
1458
  };
1162
1459
  }
1163
1460
  });
1164
1461
 
1165
1462
  // ../morula-api-client/dist/space.js
1166
- function requiredId4(id) {
1463
+ function requiredId10(id) {
1167
1464
  const normalized = id.trim();
1168
1465
  if (!normalized)
1169
1466
  throw new TypeError("space id is required");
@@ -1182,23 +1479,51 @@ var init_space = __esm({
1182
1479
  return this.client.get("/spaces", query);
1183
1480
  }
1184
1481
  get(id) {
1185
- return this.client.get(`/spaces/${encodeURIComponent(requiredId4(id))}`);
1482
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId10(id))}`);
1186
1483
  }
1187
1484
  create(input) {
1188
1485
  return this.client.post("/spaces", input);
1189
1486
  }
1190
1487
  update(id, input) {
1191
- return this.client.patch(`/spaces/${encodeURIComponent(requiredId4(id))}`, input);
1488
+ return this.client.patch(`/spaces/${encodeURIComponent(requiredId10(id))}`, input);
1192
1489
  }
1193
1490
  delete(id) {
1194
- return this.client.delete(`/spaces/${encodeURIComponent(requiredId4(id))}`);
1491
+ return this.client.delete(`/spaces/${encodeURIComponent(requiredId10(id))}`);
1492
+ }
1493
+ };
1494
+ }
1495
+ });
1496
+
1497
+ // ../morula-api-client/dist/steward.js
1498
+ function requiredId11(id) {
1499
+ const normalized = id.trim();
1500
+ if (!normalized)
1501
+ throw new TypeError("project id is required");
1502
+ return normalized;
1503
+ }
1504
+ var StewardApi;
1505
+ var init_steward = __esm({
1506
+ "../morula-api-client/dist/steward.js"() {
1507
+ "use strict";
1508
+ StewardApi = class {
1509
+ client;
1510
+ constructor(client) {
1511
+ this.client = client;
1512
+ }
1513
+ /** 管家信息(未接管的空间返回 null) */
1514
+ get(projectId) {
1515
+ return this.client.get(`/stewards/projects/${encodeURIComponent(requiredId11(projectId))}`);
1516
+ }
1517
+ /** 派单:建任务并派发(目标状态由 state 决定,backlog 不派发执行) */
1518
+ dispatch(projectId, input) {
1519
+ return this.client.post(`/stewards/projects/${encodeURIComponent(requiredId11(projectId))}/dispatch`, input);
1195
1520
  }
1196
1521
  };
1197
1522
  }
1198
1523
  });
1199
1524
 
1200
1525
  // ../morula-api-client/dist/task.js
1201
- function requiredId5(id) {
1526
+ function requiredId12(id) {
1202
1527
  const normalized = id.trim();
1203
1528
  if (!normalized)
1204
1529
  throw new TypeError("task id is required");
@@ -1217,16 +1542,16 @@ var init_task = __esm({
1217
1542
  return this.client.get("/objects", query);
1218
1543
  }
1219
1544
  get(id) {
1220
- return this.client.get(`/objects/${encodeURIComponent(requiredId5(id))}`);
1545
+ return this.client.get(`/objects/${encodeURIComponent(requiredId12(id))}`);
1221
1546
  }
1222
1547
  create(input) {
1223
1548
  return this.client.post("/objects", input);
1224
1549
  }
1225
1550
  update(id, input) {
1226
- return this.client.patch(`/objects/${encodeURIComponent(requiredId5(id))}`, input);
1551
+ return this.client.patch(`/objects/${encodeURIComponent(requiredId12(id))}`, input);
1227
1552
  }
1228
1553
  delete(id) {
1229
- return this.client.delete(`/objects/${encodeURIComponent(requiredId5(id))}`);
1554
+ return this.client.delete(`/objects/${encodeURIComponent(requiredId12(id))}`);
1230
1555
  }
1231
1556
  /** 查询空间字段模板(字段 key → id 映射、options) */
1232
1557
  getSchemaFields(spaceId) {
@@ -1293,44 +1618,76 @@ var MorulaClient;
1293
1618
  var init_dist = __esm({
1294
1619
  "../morula-api-client/dist/index.js"() {
1295
1620
  "use strict";
1621
+ init_agent_task();
1296
1622
  init_attachment();
1297
1623
  init_auth();
1624
+ init_autopilot();
1625
+ init_ci();
1298
1626
  init_client();
1299
1627
  init_comment();
1628
+ init_dashboard();
1629
+ init_document();
1630
+ init_object_meta();
1631
+ init_repo();
1300
1632
  init_report();
1301
1633
  init_space_member();
1302
1634
  init_space();
1635
+ init_steward();
1303
1636
  init_task();
1304
1637
  init_webhook();
1638
+ init_agent_task();
1305
1639
  init_attachment();
1306
1640
  init_auth();
1641
+ init_autopilot();
1642
+ init_ci();
1307
1643
  init_client();
1308
1644
  init_comment();
1645
+ init_dashboard();
1646
+ init_document();
1647
+ init_object_meta();
1648
+ init_repo();
1309
1649
  init_report();
1310
1650
  init_space_member();
1311
1651
  init_space();
1652
+ init_steward();
1312
1653
  init_task();
1313
1654
  init_types();
1314
1655
  init_webhook();
1315
1656
  MorulaClient = class {
1316
1657
  http;
1658
+ agentTasks;
1317
1659
  attachments;
1318
1660
  auth;
1661
+ autopilots;
1662
+ ci;
1319
1663
  comments;
1664
+ dashboard;
1665
+ documents;
1666
+ objectMeta;
1320
1667
  reports;
1668
+ repos;
1321
1669
  tasks;
1322
1670
  spaces;
1323
1671
  spaceMembers;
1672
+ stewards;
1324
1673
  webhooks;
1325
1674
  constructor(options) {
1326
1675
  this.http = new MorulaApiClient(options);
1676
+ this.agentTasks = new AgentTaskApi(this.http);
1327
1677
  this.attachments = new AttachmentApi(this.http);
1328
1678
  this.auth = new AuthApi(this.http);
1679
+ this.autopilots = new AutopilotApi(this.http);
1680
+ this.ci = new CiApi(this.http);
1329
1681
  this.comments = new CommentApi(this.http);
1682
+ this.dashboard = new DashboardApi(this.http);
1683
+ this.documents = new DocumentApi(this.http);
1684
+ this.objectMeta = new ObjectMetaApi(this.http);
1330
1685
  this.reports = new ReportApi(this.http);
1686
+ this.repos = new RepoApi(this.http);
1331
1687
  this.tasks = new TaskApi(this.http);
1332
1688
  this.spaces = new SpaceApi(this.http);
1333
1689
  this.spaceMembers = new SpaceMemberApi(this.http);
1690
+ this.stewards = new StewardApi(this.http);
1334
1691
  this.webhooks = new WebhookApi(this.http);
1335
1692
  }
1336
1693
  };
@@ -1384,7 +1741,8 @@ function renderActionHelp(base, resource, action) {
1384
1741
  ...f.valueHint ? { valueHint: f.valueHint } : {},
1385
1742
  required: f.required ?? false,
1386
1743
  ...f.enum ? { enum: f.enum } : {},
1387
- ...f.default !== void 0 ? { default: f.default } : {}
1744
+ ...f.default !== void 0 ? { default: f.default } : {},
1745
+ ...f.repeatable ? { repeatable: true } : {}
1388
1746
  })),
1389
1747
  examples: action.examples ?? [],
1390
1748
  writes: action.writes ?? "none",
@@ -1522,6 +1880,16 @@ function validateArgs(resource, action, parsed, options = {}) {
1522
1880
  hint: `${usageHint} (e.g. --${flag} ${valuePart})`
1523
1881
  });
1524
1882
  }
1883
+ for (const [name, values] of Object.entries(parsed.repeatedFlags)) {
1884
+ if (values.length < 2)
1885
+ continue;
1886
+ const spec = declaredFlags.find((f) => f.name === name);
1887
+ if (spec?.repeatable)
1888
+ continue;
1889
+ throw usage(`duplicate flag --${name} (given ${values.length} times; only the last value would be used)`, {
1890
+ hint: usageHint
1891
+ });
1892
+ }
1525
1893
  for (const spec of declaredFlags) {
1526
1894
  if (!spec.required)
1527
1895
  continue;
@@ -1631,6 +1999,8 @@ function resolveToken(flags, env, context, baseUrl, saved, options) {
1631
1999
  return { token: explicitToken, tokenSource: "flag" };
1632
2000
  if (!options.excludeTaskToken && env.MORULA_TASK_TOKEN)
1633
2001
  return { token: env.MORULA_TASK_TOKEN, tokenSource: "env:MORULA_TASK_TOKEN" };
2002
+ if (!options.excludeTaskToken && env.MORULA_SESSION_TOKEN)
2003
+ return { token: env.MORULA_SESSION_TOKEN, tokenSource: "env:MORULA_SESSION_TOKEN" };
1634
2004
  if (env.MORULA_TOKEN)
1635
2005
  return { token: env.MORULA_TOKEN, tokenSource: "env:MORULA_TOKEN" };
1636
2006
  const savedToken = saved?.url && normalizeBaseUrl(saved.url) === baseUrl ? saved.token : void 0;
@@ -1775,6 +2145,10 @@ async function run(argv, deps = {}) {
1775
2145
  code: error.code,
1776
2146
  message: error.message,
1777
2147
  ...error.retryable ? { retryable: true } : {},
2148
+ // 服务端业务错误码(如 SESSION_SCOPE_DENIED):`code` 是按 HTTP 状态派生的粗档位,
2149
+ // 行为契约要判断的是业务码——不打印出来,契约就永远不可执行
2150
+ // (issues/api-client-drops-business-error-code.md)。退出码仍由 `error.exitCode` 决定。
2151
+ ...error instanceof MorulaApiError && error.errorCode ? { errorCode: error.errorCode } : {},
1778
2152
  // 新增字段(可选):可执行的下一步 + typo 建议
1779
2153
  ...error instanceof CliError && error.hint ? { hint: error.hint } : {},
1780
2154
  ...error instanceof CliError && error.didYouMean ? { didYouMean: error.didYouMean } : {}
@@ -2293,8 +2667,27 @@ var init_daemon_config = __esm({
2293
2667
 
2294
2668
  // src/daemon/daemon-lock.ts
2295
2669
  import { execFileSync as execFileSync3 } from "node:child_process";
2296
- import { closeSync as closeSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync2, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync3, writeSync } from "node:fs";
2670
+ 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";
2297
2671
  import { dirname as dirname4, join as join6 } from "node:path";
2672
+ function daemonPidPath(configDir) {
2673
+ return join6(configDir, "daemon", DAEMON_PID_FILE);
2674
+ }
2675
+ function readPidFile(file) {
2676
+ if (!existsSync5(file))
2677
+ return 0;
2678
+ try {
2679
+ return Number(readFileSync2(file, "utf8").trim() || 0);
2680
+ } catch {
2681
+ return 0;
2682
+ }
2683
+ }
2684
+ function probeDaemonInstance(configDir) {
2685
+ const lock = probeDaemonLock(join6(configDir, "daemon", DAEMON_LOCK_FILE));
2686
+ const lockPid = lock.state === "active" ? lock.record.pid : 0;
2687
+ const filePid = readPidFile(daemonPidPath(configDir));
2688
+ const pid = lockPid || (isProcessAlive(filePid) ? filePid : 0);
2689
+ return { running: pid > 0, pid: pid > 0 ? pid : null, lockState: lock.state };
2690
+ }
2298
2691
  function isProcessAlive(pid) {
2299
2692
  if (!pid || pid <= 0)
2300
2693
  return false;
@@ -2444,11 +2837,12 @@ async function stopDaemonByPid(pid, options = {}) {
2444
2837
  await waitForExit(pid, 2e3, pollMs);
2445
2838
  return { pid, outcome: "killed" };
2446
2839
  }
2447
- var DAEMON_LOCK_FILE;
2840
+ var DAEMON_LOCK_FILE, DAEMON_PID_FILE;
2448
2841
  var init_daemon_lock = __esm({
2449
2842
  "src/daemon/daemon-lock.ts"() {
2450
2843
  "use strict";
2451
2844
  DAEMON_LOCK_FILE = "daemon.lock";
2845
+ DAEMON_PID_FILE = "daemon.pid";
2452
2846
  }
2453
2847
  });
2454
2848
 
@@ -2458,10 +2852,9 @@ __export(portal_instances_exports, {
2458
2852
  candidateConfigFiles: () => candidateConfigFiles,
2459
2853
  createPortalEnvironmentResolver: () => createPortalEnvironmentResolver,
2460
2854
  normalizeBaseUrl: () => normalizeBaseUrl2,
2461
- parseBaseUrl: () => parseBaseUrl,
2462
- readPidFile: () => readPidFile
2855
+ parseBaseUrl: () => parseBaseUrl
2463
2856
  });
2464
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
2857
+ import { readdirSync, statSync } from "node:fs";
2465
2858
  import { homedir as homedir5 } from "node:os";
2466
2859
  import { dirname as dirname5, join as join7 } from "node:path";
2467
2860
  function parseBaseUrl(raw) {
@@ -2527,16 +2920,6 @@ function readContexts(file) {
2527
2920
  return null;
2528
2921
  }
2529
2922
  }
2530
- function readPidFile(configDir) {
2531
- const file = join7(configDir, "daemon", "daemon.pid");
2532
- if (!existsSync5(file))
2533
- return 0;
2534
- try {
2535
- return Number(readFileSync3(file, "utf8").trim() || 0);
2536
- } catch {
2537
- return 0;
2538
- }
2539
- }
2540
2923
  function createPortalEnvironmentResolver(options = {}) {
2541
2924
  const env = options.env ?? process.env;
2542
2925
  const log = options.log ?? (() => {
@@ -2565,7 +2948,7 @@ function createPortalEnvironmentResolver(options = {}) {
2565
2948
  baseUrl: entry.url,
2566
2949
  normalized,
2567
2950
  token: entry.token,
2568
- hasLiveDaemon: isProcessAlive(readPidFile(configDir)),
2951
+ hasLiveDaemon: probeDaemonInstance(configDir).running,
2569
2952
  isDefaultDir: configDir === defaultConfigDir,
2570
2953
  configMtimeMs: configMtimeMs(file)
2571
2954
  });
@@ -2611,6 +2994,25 @@ function createPortalEnvironmentResolver(options = {}) {
2611
2994
  return void 0;
2612
2995
  }
2613
2996
  },
2997
+ daemonId: () => {
2998
+ try {
2999
+ return loadDaemonConfig(new FileConfigStore(join7(instance.configDir, "config.yaml")))?.daemonId;
3000
+ } catch {
3001
+ return void 0;
3002
+ }
3003
+ },
3004
+ isDeviceOnline: async (daemonId) => {
3005
+ if (!instance.token)
3006
+ return null;
3007
+ try {
3008
+ const client = createMorulaClient({ baseUrl: instance.baseUrl, token: instance.token, fetch: options.fetch });
3009
+ const rows = await client.http.get("/agent-runtimes");
3010
+ return (Array.isArray(rows) ? rows : []).some((row) => row.daemonId === daemonId && row.status === "online");
3011
+ } catch (error) {
3012
+ log(`[portal] \u67E5\u8BE2 ${instance.context} \u7684\u8BBE\u5907\u5728\u7EBF\u72B6\u6001\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
3013
+ return null;
3014
+ }
3015
+ },
2614
3016
  validateToken: async (token) => {
2615
3017
  if (!instance.token)
2616
3018
  return false;
@@ -2669,9 +3071,13 @@ var init_portal_instances = __esm({
2669
3071
  var portal_server_exports = {};
2670
3072
  __export(portal_server_exports, {
2671
3073
  DEFAULT_PORTAL_PORT: () => DEFAULT_PORTAL_PORT,
2672
- PortalServer: () => PortalServer
3074
+ PortalServer: () => PortalServer,
3075
+ REGISTRATION_CONFIRM_TIMEOUT_MS: () => REGISTRATION_CONFIRM_TIMEOUT_MS,
3076
+ START_CONFIRM_TIMEOUT_MS: () => START_CONFIRM_TIMEOUT_MS,
3077
+ waitForDaemonRegistration: () => waitForDaemonRegistration,
3078
+ waitForDaemonStart: () => waitForDaemonStart
2673
3079
  });
2674
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
3080
+ import { readFileSync as readFileSync3, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "node:fs";
2675
3081
  import { createServer as createServer2 } from "node:http";
2676
3082
  import { homedir as homedir6 } from "node:os";
2677
3083
  import { join as join8 } from "node:path";
@@ -2679,35 +3085,61 @@ function stateDir() {
2679
3085
  return process.env.MORULA_CONFIG_DIR ?? join8(homedir6(), ".morula-cli");
2680
3086
  }
2681
3087
  function pidFileIn(configDir) {
2682
- return join8(configDir, "daemon", "daemon.pid");
3088
+ return join8(configDir, "daemon", DAEMON_PID_FILE);
2683
3089
  }
2684
3090
  function daemonLogIn(configDir) {
2685
3091
  return join8(configDir, "daemon", "daemon.log");
2686
3092
  }
2687
- function isPidAlive(pid) {
2688
- if (!pid || pid <= 0)
2689
- return false;
3093
+ async function waitForDaemonStart(configDir, childPid, timeoutMs = START_CONFIRM_TIMEOUT_MS) {
3094
+ const deadline = Date.now() + timeoutMs;
3095
+ for (; ; ) {
3096
+ const probe = probeDaemonInstance(configDir);
3097
+ if (probe.running)
3098
+ return { running: true, pid: probe.pid };
3099
+ if (!isProcessAlive(childPid))
3100
+ return { running: false, reason: "exited" };
3101
+ if (Date.now() >= deadline)
3102
+ return { running: false, reason: "timeout" };
3103
+ await new Promise((resolve2) => setTimeout(resolve2, START_CONFIRM_POLL_MS));
3104
+ }
3105
+ }
3106
+ async function waitForDaemonRegistration(isDeviceOnline, daemonId, childPid, timeoutMs = REGISTRATION_CONFIRM_TIMEOUT_MS) {
3107
+ const deadline = Date.now() + timeoutMs;
3108
+ let queryFailed = false;
3109
+ for (; ; ) {
3110
+ const online = await isDeviceOnline(daemonId);
3111
+ if (online === true)
3112
+ return "registered";
3113
+ if (online === null)
3114
+ queryFailed = true;
3115
+ if (!isProcessAlive(childPid))
3116
+ return "exited";
3117
+ if (Date.now() >= deadline)
3118
+ return queryFailed ? "unknown" : "not-registered";
3119
+ await new Promise((resolve2) => setTimeout(resolve2, START_CONFIRM_POLL_MS));
3120
+ }
3121
+ }
3122
+ function tailDaemonLog(configDir, maxLines = 5) {
2690
3123
  try {
2691
- process.kill(pid, 0);
2692
- return true;
3124
+ const lines = readFileSync3(daemonLogIn(configDir), "utf8").trimEnd().split("\n");
3125
+ return lines.slice(-maxLines).join(" / ") || null;
2693
3126
  } catch {
2694
- return false;
3127
+ return null;
2695
3128
  }
2696
3129
  }
2697
- function readPidFrom(file) {
2698
- if (!existsSync6(file))
2699
- return 0;
2700
- return Number(readFileSync4(file, "utf8").trim() || 0);
2701
- }
2702
- var DEFAULT_PORTAL_PORT, DEFAULT_CORS_ORIGINS, PortalServer;
3130
+ var DEFAULT_PORTAL_PORT, DEFAULT_CORS_ORIGINS, START_CONFIRM_TIMEOUT_MS, REGISTRATION_CONFIRM_TIMEOUT_MS, START_CONFIRM_POLL_MS, PortalServer;
2703
3131
  var init_portal_server = __esm({
2704
3132
  "src/daemon/portal-server.ts"() {
2705
3133
  "use strict";
2706
3134
  init_daemon_config();
3135
+ init_daemon_lock();
2707
3136
  init_portal_instances();
2708
3137
  init_spawn();
2709
3138
  DEFAULT_PORTAL_PORT = 23491;
2710
3139
  DEFAULT_CORS_ORIGINS = ["*"];
3140
+ START_CONFIRM_TIMEOUT_MS = 5e3;
3141
+ REGISTRATION_CONFIRM_TIMEOUT_MS = 5e3;
3142
+ START_CONFIRM_POLL_MS = 200;
2711
3143
  PortalServer = class {
2712
3144
  port;
2713
3145
  server = null;
@@ -2848,14 +3280,13 @@ var init_portal_server = __esm({
2848
3280
  if (failed)
2849
3281
  return;
2850
3282
  const configDir = target?.configDir ?? stateDir();
2851
- const pid = readPidFrom(pidFileIn(configDir));
2852
- const running = isPidAlive(pid);
3283
+ const probe = probeDaemonInstance(configDir);
2853
3284
  const runtimeId = target ? target.runtimeId() : this.store ? loadDaemonConfig(this.store)?.runtimeId : void 0;
2854
3285
  const self = target ? { context: target.context, baseUrl: target.baseUrl } : this.selfEnvironment();
2855
3286
  this.json(res, 200, {
2856
3287
  ok: true,
2857
- daemonRunning: running,
2858
- pid: running ? pid : null,
3288
+ daemonRunning: probe.running,
3289
+ pid: probe.pid,
2859
3290
  runtimeId: runtimeId ?? null,
2860
3291
  // 支持按请求声明目标环境(前端据此决定是否走多环境路径;缺省 = 旧版 portal)
2861
3292
  envRouting: true,
@@ -2870,10 +3301,10 @@ var init_portal_server = __esm({
2870
3301
  if (!await this.authorize(req, res, target))
2871
3302
  return;
2872
3303
  const configDir = target?.configDir ?? stateDir();
2873
- const pidFile = pidFileIn(configDir);
2874
- const existing = readPidFrom(pidFile);
2875
- if (isPidAlive(existing)) {
2876
- this.json(res, 200, { ok: true, pid: existing, alreadyRunning: true });
3304
+ const where = target ? `\uFF08\u73AF\u5883 ${target.context}\uFF0C\u76EE\u5F55 ${configDir}\uFF09` : "";
3305
+ const existing = probeDaemonInstance(configDir);
3306
+ if (existing.running) {
3307
+ this.json(res, 200, { ok: true, pid: existing.pid, alreadyRunning: true });
2877
3308
  return;
2878
3309
  }
2879
3310
  const result = spawnDaemonProcess({
@@ -2884,18 +3315,54 @@ var init_portal_server = __esm({
2884
3315
  this.json(res, 500, { ok: false, error: result.error ?? `spawn \u5931\u8D25\uFF08pid=${result.pid}\uFF09` });
2885
3316
  return;
2886
3317
  }
3318
+ const confirmed = await waitForDaemonStart(configDir, result.pid);
3319
+ if (!confirmed.running) {
3320
+ if (!isProcessAlive(readPidFile(pidFileIn(configDir))))
3321
+ rmSync5(pidFileIn(configDir), { force: true });
3322
+ this.log(`[portal] daemon \u672A\u80FD\u542F\u52A8 pid=${result.pid}${where}\uFF1A${confirmed.reason}`);
3323
+ this.json(res, 500, {
3324
+ ok: false,
3325
+ error: confirmed.reason === "exited" ? "DAEMON_EXITED" : "DAEMON_START_TIMEOUT",
3326
+ hint: `daemon \u672A\u542F\u52A8\u6210\u529F\uFF08${confirmed.reason === "exited" ? "\u5B50\u8FDB\u7A0B\u5DF2\u9000\u51FA" : "\u7B49\u5F85\u8D85\u65F6"}\uFF09\uFF1A${tailDaemonLog(configDir) ?? "\u8BE6\u89C1 daemon \u65E5\u5FD7"}`,
3327
+ logFile: daemonLogIn(configDir)
3328
+ });
3329
+ return;
3330
+ }
3331
+ if (confirmed.pid !== result.pid) {
3332
+ this.log(`[portal] \u5DF2\u6709\u5B9E\u4F8B\u5728\u8FD0\u884C pid=${confirmed.pid}${where}\uFF0C\u672C\u6B21 spawn \u7684\u5B50\u8FDB\u7A0B\u5DF2\u8BA9\u4F4D`);
3333
+ this.json(res, 200, { ok: true, pid: confirmed.pid, alreadyRunning: true });
3334
+ return;
3335
+ }
3336
+ const daemonId = target?.daemonId();
3337
+ if (target && daemonId) {
3338
+ const reg = await waitForDaemonRegistration(target.isDeviceOnline, daemonId, confirmed.pid);
3339
+ if (reg === "exited" || reg === "not-registered") {
3340
+ if (!isProcessAlive(readPidFile(pidFileIn(configDir))))
3341
+ rmSync5(pidFileIn(configDir), { force: true });
3342
+ this.log(`[portal] daemon \u8FDB\u7A0B\u5DF2\u8D77\u6765\u4F46\u672A\u6CE8\u518C\u4E0A\u7EBF pid=${confirmed.pid}${where}\uFF1A${reg}`);
3343
+ this.json(res, 500, {
3344
+ ok: false,
3345
+ error: "DAEMON_NOT_REGISTERED",
3346
+ hint: `daemon \u8FDB\u7A0B\u5DF2\u542F\u52A8\u4F46\u672A\u5728\u670D\u52A1\u7AEF\u6CE8\u518C\u4E0A\u7EBF\uFF08${reg === "exited" ? "\u8FDB\u7A0B\u5DF2\u9000\u51FA" : "\u7B49\u5F85\u8D85\u65F6"}\uFF09\uFF1A${tailDaemonLog(configDir) ?? "\u8BE6\u89C1 daemon \u65E5\u5FD7"}`,
3347
+ logFile: daemonLogIn(configDir)
3348
+ });
3349
+ return;
3350
+ }
3351
+ if (reg === "unknown")
3352
+ this.log(`[portal] \u65E0\u6CD5\u786E\u8BA4\u6CE8\u518C\u72B6\u6001\uFF08\u67E5\u8BE2\u5931\u8D25\uFF09pid=${confirmed.pid}${where}\uFF0C\u6309\u542F\u52A8\u6210\u529F\u5904\u7406`);
3353
+ }
2887
3354
  try {
2888
- writeFileSync4(pidFile, String(result.pid), { mode: 384 });
3355
+ writeFileSync4(pidFileIn(configDir), String(confirmed.pid), { mode: 384 });
2889
3356
  } catch {
2890
3357
  try {
2891
- process.kill(result.pid, "SIGTERM");
3358
+ process.kill(confirmed.pid, "SIGTERM");
2892
3359
  } catch {
2893
3360
  }
2894
3361
  this.json(res, 500, { ok: false, error: "pid \u6587\u4EF6\u5199\u5165\u5931\u8D25" });
2895
3362
  return;
2896
3363
  }
2897
- this.log(`[portal] daemon \u5DF2\u62C9\u8D77 pid=${result.pid}${target ? `\uFF08\u73AF\u5883 ${target.context}\uFF0C\u76EE\u5F55 ${configDir}\uFF09` : ""}`);
2898
- this.json(res, 200, { ok: true, pid: result.pid, alreadyRunning: false });
3364
+ this.log(`[portal] daemon \u5DF2\u62C9\u8D77 pid=${confirmed.pid}${where}`);
3365
+ this.json(res, 200, { ok: true, pid: confirmed.pid, alreadyRunning: false });
2899
3366
  }
2900
3367
  async handleStop(req, res, url) {
2901
3368
  const { target, failed } = this.resolveTarget(res, url);
@@ -2904,15 +3371,15 @@ var init_portal_server = __esm({
2904
3371
  if (!await this.authorize(req, res, target))
2905
3372
  return;
2906
3373
  const configDir = target?.configDir ?? stateDir();
2907
- const existing = readPidFrom(pidFileIn(configDir));
2908
- if (isPidAlive(existing)) {
3374
+ const existing = probeDaemonInstance(configDir);
3375
+ if (existing.running) {
2909
3376
  try {
2910
- process.kill(existing, "SIGTERM");
3377
+ process.kill(existing.pid, "SIGTERM");
2911
3378
  } catch {
2912
3379
  }
2913
- this.log(`[portal] daemon \u505C\u6B62\u4FE1\u53F7\u5DF2\u53D1\u9001 pid=${existing}${target ? `\uFF08\u73AF\u5883 ${target.context}\uFF09` : ""}`);
3380
+ this.log(`[portal] daemon \u505C\u6B62\u4FE1\u53F7\u5DF2\u53D1\u9001 pid=${existing.pid}${target ? `\uFF08\u73AF\u5883 ${target.context}\uFF09` : ""}`);
2914
3381
  }
2915
- this.json(res, 200, { ok: true, pid: existing || null });
3382
+ this.json(res, 200, { ok: true, pid: existing.pid });
2916
3383
  }
2917
3384
  /**
2918
3385
  * start/stop 鉴权:目标环境用**它自己的**服务端校验 token(这正是跨环境 401 的修复点);
@@ -3020,6 +3487,7 @@ __export(acp_connection_exports, {
3020
3487
  AcpConnection: () => AcpConnection,
3021
3488
  DEFAULT_ACP_INIT_TIMEOUT_MS: () => DEFAULT_ACP_INIT_TIMEOUT_MS,
3022
3489
  countDimAcpProcesses: () => countDimAcpProcesses,
3490
+ normalizeAcpConfigOptions: () => normalizeAcpConfigOptions,
3023
3491
  resolveAcpInitTimeoutMs: () => resolveAcpInitTimeoutMs
3024
3492
  });
3025
3493
  import { execFileSync as execFileSync4, spawn as spawn3 } from "node:child_process";
@@ -3048,6 +3516,16 @@ function countDimAcpProcesses() {
3048
3516
  return null;
3049
3517
  }
3050
3518
  }
3519
+ function normalizeAcpConfigOptions(raw) {
3520
+ if (Array.isArray(raw))
3521
+ return raw;
3522
+ if (raw && typeof raw === "object") {
3523
+ const nested = raw.configOptions;
3524
+ if (Array.isArray(nested))
3525
+ return nested;
3526
+ }
3527
+ return null;
3528
+ }
3051
3529
  var DEFAULT_ACP_INIT_TIMEOUT_MS, cachedDimVersion, requestSeq, AcpConnection;
3052
3530
  var init_acp_connection = __esm({
3053
3531
  "src/daemon/acp/acp-connection.ts"() {
@@ -3063,6 +3541,7 @@ var init_acp_connection = __esm({
3063
3541
  outcome: { outcome: "selected", optionId: "allow" }
3064
3542
  }));
3065
3543
  this.onExit = options.onExit;
3544
+ this.elicitationHandler = options.onElicitation;
3066
3545
  }
3067
3546
  options;
3068
3547
  child = null;
@@ -3070,6 +3549,7 @@ var init_acp_connection = __esm({
3070
3549
  pending = /* @__PURE__ */ new Map();
3071
3550
  exited = false;
3072
3551
  permissionHandler;
3552
+ elicitationHandler;
3073
3553
  onExit;
3074
3554
  /** sessionId → 当前 update 处理器(同一 session 单活跃 prompt) */
3075
3555
  updateHandlers = /* @__PURE__ */ new Map();
@@ -3103,9 +3583,9 @@ var init_acp_connection = __esm({
3103
3583
  process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
3104
3584
  `);
3105
3585
  try {
3106
- const { existsSync: existsSync19 } = await import("node:fs");
3586
+ const { existsSync: existsSync18 } = await import("node:fs");
3107
3587
  process.stderr.write(
3108
- `[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 ?? "-"}
3588
+ `[daemon] diag: fileExists=${existsSync18(dimPath)} cwdExists=${this.options.cwd ? existsSync18(this.options.cwd) : "n/a"} PATH=${(process.env.PATH ?? "").slice(0, 140)} DYLD=${process.env.DYLD_LIBRARY_PATH ?? "-"} NODE_OPTIONS=${process.env.NODE_OPTIONS ?? "-"}
3109
3589
  `
3110
3590
  );
3111
3591
  } catch {
@@ -3155,7 +3635,12 @@ var init_acp_connection = __esm({
3155
3635
  child.stdout.on("data", (d) => this.onStdout(d.toString()));
3156
3636
  const init = {
3157
3637
  protocolVersion: 1,
3158
- clientCapabilities: { auth: { terminal: true } },
3638
+ // elicitation.form 只在**有处理器时**声明:声明了却不应答会让 dim 挂起等用户输入。
3639
+ // 形状是对象而非 boolean(V2 实测:boolean 被 dim 以 -32602 拒绝)。
3640
+ clientCapabilities: {
3641
+ auth: { terminal: true },
3642
+ ...this.elicitationHandler ? { elicitation: { form: {} } } : {}
3643
+ },
3159
3644
  clientInfo: { name: "morula-runtime", version: "1.0.0" }
3160
3645
  };
3161
3646
  const initTimeoutMs = resolveAcpInitTimeoutMs();
@@ -3200,26 +3685,38 @@ var init_acp_connection = __esm({
3200
3685
  return this.request("session/set_config_option", { sessionId, configId, value }).then(() => {
3201
3686
  });
3202
3687
  }
3688
+ /**
3689
+ * 设置会话配置项并**全量返回** configOptions(ACP 规范:set_config_option 响应即全量配置)。
3690
+ * 会话宿主用(§4.4 配置变更回写);任务链路只需 fire-and-forget,继续用 setConfigOption。
3691
+ *
3692
+ * **返回已归一化为数组**:ACP 的响应体是 `{ configOptions: ConfigOption[] }`(对象),
3693
+ * 而 `session/new` / `resume` 的 `configOptions` 字段本身就是数组——两处形状不同,
3694
+ * 调用方(会话宿主 → 服务端落库 → 前端输入区值域)必须拿到同一种形状,否则前端拿到对象会崩。
3695
+ */
3696
+ setConfigOptionFull(sessionId, configId, value) {
3697
+ return this.request("session/set_config_option", { sessionId, configId, value }).then((res) => normalizeAcpConfigOptions(res) ?? []);
3698
+ }
3203
3699
  /**
3204
3700
  * 发起 prompt:立即返回;事件流经 updateHandler 推送;返回的 Promise 在
3205
3701
  * turn 结束(prompt 响应)时 resolve。
3206
3702
  *
3207
3703
  * @param sessionId ACP 会话 ID
3208
- * @param text 用户输入文本
3704
+ * @param prompt 用户输入。字符串 = 纯文本;数组 = 多模态 content block
3705
+ * (`image` block 用于对话附件,见 steward-core-acp-migration §五 P2-3)
3209
3706
  * @param updateHandler 会话事件回调
3210
- * @param timeoutMs prompt 整轮 RPC 超时。注意:dim 直到整轮 agent 循环结束
3211
- * (stopReason)才回复 prompt,长时间的多步工具执行/模型生成都会占满该窗口,
3212
- * 因此调用方必须传入 ≥ 任务总超时(如 executor 的 timeoutMs)的值,否则长任务
3213
- * 会被 RPC 层提前误杀。默认 300s 仅为向后兼容的保守值。
3707
+ * @param timeoutMs prompt 整轮 RPC 超时。**未设 / <= 0 = 不设超时(默认)**:dim 直到整轮
3708
+ * agent 循环结束(stopReason)才回复 prompt,多步工具执行/长模型生成都会占满窗口,
3709
+ * 给 RPC 层设超时等于给「合法的长回合」埋误杀点。限时与否由调用方(executor 的
3710
+ * 单回合预算)决定,executor 默认即传 0(不设超时)。
3214
3711
  */
3215
- prompt(sessionId, text, updateHandler, timeoutMs = 3e5) {
3712
+ prompt(sessionId, prompt, updateHandler, timeoutMs = 0) {
3216
3713
  if (this.updateHandlers.has(sessionId)) {
3217
3714
  return Promise.reject(new Error(`session ${sessionId} \u5DF2\u6709\u6D3B\u8DC3 prompt\uFF08ACP \u5355\u6D3B\u8DC3\u7EA6\u675F\uFF09`));
3218
3715
  }
3219
3716
  this.updateHandlers.set(sessionId, updateHandler);
3220
3717
  const params = {
3221
3718
  sessionId,
3222
- prompt: [{ type: "text", text }],
3719
+ prompt: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt,
3223
3720
  messageId: `m-${randomUUID2().slice(0, 8)}`
3224
3721
  };
3225
3722
  return this.request("session/prompt", params, timeoutMs).then(
@@ -3278,17 +3775,20 @@ var init_acp_connection = __esm({
3278
3775
  return Promise.reject(new Error(`dim acp \u8FDB\u7A0B\u4E0D\u53EF\u7528\uFF08${method}\uFF09`));
3279
3776
  const id = ++requestSeq;
3280
3777
  return new Promise((resolve2, reject) => {
3281
- const timer = setTimeout(() => {
3778
+ const onTimeout = () => {
3282
3779
  this.pending.delete(id);
3283
3780
  reject(new Error(`dim acp ${method} \u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)}s\uFF09`));
3284
- }, timeoutMs);
3781
+ };
3782
+ const timer = timeoutMs > 0 ? setTimeout(onTimeout, timeoutMs) : null;
3285
3783
  this.pending.set(id, {
3286
3784
  resolve: (v) => {
3287
- clearTimeout(timer);
3785
+ if (timer)
3786
+ clearTimeout(timer);
3288
3787
  resolve2(v);
3289
3788
  },
3290
3789
  reject: (e) => {
3291
- clearTimeout(timer);
3790
+ if (timer)
3791
+ clearTimeout(timer);
3292
3792
  reject(e);
3293
3793
  }
3294
3794
  });
@@ -3359,6 +3859,21 @@ var init_acp_connection = __esm({
3359
3859
  );
3360
3860
  return;
3361
3861
  }
3862
+ if (method === "elicitation/create") {
3863
+ const id = msg.id;
3864
+ const params = msg.params;
3865
+ const handler = this.elicitationHandler;
3866
+ if (!handler) {
3867
+ this.respond(id, { action: "decline" });
3868
+ return;
3869
+ }
3870
+ void handler(params).then(
3871
+ (response) => this.respond(id, response),
3872
+ // 处理器异常(含超时):回 cancel,让 agent 走「用户没答」的分支,而不是挂死
3873
+ () => this.respond(id, { action: "cancel" })
3874
+ );
3875
+ return;
3876
+ }
3362
3877
  if (method === "$/cancel_request") {
3363
3878
  return;
3364
3879
  }
@@ -3378,7 +3893,7 @@ __export(dim_home_exports, {
3378
3893
  resolveIsolatedDimEnv: () => resolveIsolatedDimEnv
3379
3894
  });
3380
3895
  import { execFileSync as execFileSync5 } from "node:child_process";
3381
- import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync7, readFileSync as readFileSync5, rmSync as rmSync5 } from "node:fs";
3896
+ import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync6, readFileSync as readFileSync4, rmSync as rmSync6 } from "node:fs";
3382
3897
  import { homedir as homedir8 } from "node:os";
3383
3898
  import { join as join10 } from "node:path";
3384
3899
  function isolatedDimHome() {
@@ -3405,11 +3920,11 @@ __export(dim_model_exports, {
3405
3920
  import { createRequire } from "node:module";
3406
3921
  import { spawnSync } from "node:child_process";
3407
3922
  import { closeSync as closeSync3, existsSync as existsSync10, openSync as openSync3, readFileSync as readFileSync7, unlinkSync } from "node:fs";
3408
- import { homedir as homedir10, tmpdir } from "node:os";
3409
- import { join as join13 } from "node:path";
3923
+ import { homedir as homedir11, tmpdir } from "node:os";
3924
+ import { join as join14 } from "node:path";
3410
3925
  function dimSqlitePath(env) {
3411
3926
  const home = env?.DIMCODE_HOME?.trim();
3412
- const candidates = home && home !== "" ? [join13(home, "v2", "dimcode.sqlite"), join13(home, "dimcode.sqlite")] : [join13(homedir10(), ".dimcode", "v2", "dimcode.sqlite")];
3927
+ const candidates = home && home !== "" ? [join14(home, "v2", "dimcode.sqlite"), join14(home, "dimcode.sqlite")] : [join14(homedir11(), ".dimcode", "v2", "dimcode.sqlite")];
3413
3928
  return candidates.find((p) => existsSync10(p)) ?? null;
3414
3929
  }
3415
3930
  function listDimModels(dimPath = "dim", env) {
@@ -3444,12 +3959,12 @@ function listDimModels(dimPath = "dim", env) {
3444
3959
  function hostDimEnv(env) {
3445
3960
  const home = env?.DIMCODE_HOME?.trim();
3446
3961
  if (home && home !== "" && home.includes(".morula") && home.includes("dimcode")) {
3447
- return { ...process.env, DIMCODE_HOME: join13(homedir10(), ".dimcode", "v2") };
3962
+ return { ...process.env, DIMCODE_HOME: join14(homedir11(), ".dimcode", "v2") };
3448
3963
  }
3449
3964
  return env ? { ...env } : null;
3450
3965
  }
3451
3966
  function listDimModelsFromCli(dimPath, env) {
3452
- const tmpFile = join13(tmpdir(), `${DIM_MODELS_TMP_PREFIX}${process.pid}-${Date.now()}.json`);
3967
+ const tmpFile = join14(tmpdir(), `${DIM_MODELS_TMP_PREFIX}${process.pid}-${Date.now()}.json`);
3453
3968
  const fd = openSync3(tmpFile, "w");
3454
3969
  let collected = "";
3455
3970
  try {
@@ -3486,8 +4001,8 @@ function listDimModelsFromCli(dimPath, env) {
3486
4001
  }
3487
4002
  function dimOAuthTokenPath(env) {
3488
4003
  const home = env?.DIMCODE_HOME?.trim();
3489
- const dir = home && home !== "" ? home : join13(homedir10(), ".dimcode", "v2");
3490
- for (const p of [join13(dir, "auth.json"), join13(dir, "v2", "auth.json")]) {
4004
+ const dir = home && home !== "" ? home : join14(homedir11(), ".dimcode", "v2");
4005
+ for (const p of [join14(dir, "auth.json"), join14(dir, "v2", "auth.json")]) {
3491
4006
  if (existsSync10(p))
3492
4007
  return p;
3493
4008
  }
@@ -3632,14 +4147,14 @@ init_dist2();
3632
4147
  init_dist2();
3633
4148
 
3634
4149
  // src/build-env.ts
3635
- var BUILD_VERSION2 = "0.1.0-alpha.7";
4150
+ var BUILD_VERSION2 = "0.1.0-alpha.9";
3636
4151
 
3637
4152
  // src/commands/daemon.ts
3638
4153
  init_dist();
3639
4154
  init_dist2();
3640
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync10 } from "node:fs";
3641
- import { homedir as homedir12 } from "node:os";
3642
- import { dirname as dirname9, join as join20 } from "node:path";
4155
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync11, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "node:fs";
4156
+ import { homedir as homedir13 } from "node:os";
4157
+ import { dirname as dirname10, join as join22 } from "node:path";
3643
4158
 
3644
4159
  // src/daemon/daemon-autostart.ts
3645
4160
  init_portal_autostart();
@@ -3708,9 +4223,9 @@ init_portal_server();
3708
4223
  // src/daemon/runner.ts
3709
4224
  init_dist2();
3710
4225
  import { execFileSync as execFileSync10 } from "node:child_process";
3711
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, statSync as statSync3, writeFileSync as writeFileSync9 } from "node:fs";
4226
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
3712
4227
  import { arch, hostname } from "node:os";
3713
- import { dirname as dirname7, join as join18 } from "node:path";
4228
+ import { dirname as dirname8, join as join20 } from "node:path";
3714
4229
 
3715
4230
  // src/daemon/acp/acp-connection-pool.ts
3716
4231
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
@@ -3881,6 +4396,17 @@ var DaemonApi = class {
3881
4396
  ...token ? { token } : {}
3882
4397
  });
3883
4398
  }
4399
+ /**
4400
+ * 终态结果回传(§6.2-⑤)。
4401
+ *
4402
+ * `runtimeId` **必传**:服务端按它校验任务归属(不匹配 → `RUNTIME_MISMATCH`,不落终态),
4403
+ * 缺失同样被拒(P1-a 定案「立即拒绝」——旧版 daemon 的任务会全部失败,升级期须先升执行设备,
4404
+ * 见 issues/agent-task-false-failure-on-daemon-fetch-error.md)。
4405
+ *
4406
+ * `cancelled: true`(取消留痕,issues/daemon-stream-events-silently-stop-mid-run.md 未实施项 5):
4407
+ * 本轮**被人取消**时只补记已产出内容(`result.text` → 服务端 `agent_tasks.result.cancelled.lastOutput`),
4408
+ * **不改任务状态**、不触发生命周期。旧服务端经 `whitelist` 剥离该字段 → 退化为原「终态回传被幂等丢弃」。
4409
+ */
3884
4410
  submitResult(taskId, body) {
3885
4411
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/result`, { body });
3886
4412
  }
@@ -3954,7 +4480,7 @@ var DaemonApi = class {
3954
4480
  recordEvent(taskId, body) {
3955
4481
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/events`, { body });
3956
4482
  }
3957
- /** ACP 流式事件(Phase 1):daemon → Server → WS 前端 */
4483
+ /** ACP 流式事件(Phase 1 → P2-2):daemon → Server → WS 前端(`delta` 为 transient,不落库) */
3958
4484
  recordStreamChunk(taskId, body) {
3959
4485
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/stream-chunk`, { body });
3960
4486
  }
@@ -4015,20 +4541,201 @@ var DaemonApi = class {
4015
4541
  getGitHubToken(taskId, repo) {
4016
4542
  return this.client.get("/vcs/daemon-token", { taskId, repo });
4017
4543
  }
4018
- /**
4019
- * A2(issues/multi-repo-task-missing-workcontext-link.md):**不传 repo** 的平台凭证
4020
- * ——服务端按「任务空间内 active 的连接」择一签发(凭证是平台凭据,不是某仓库的凭据)。
4021
- *
4022
- * 用于无主仓库 / 多仓库任务(daemon 走 scratch 隔离目录、`resolveRepo(detail) = null`):
4023
- * 这些任务的 agent 同样需要 MORULA_GITLAB_TOKEN / MORULA_GITLAB_URL 才能自己走 GitLab API
4024
- * 兜底建 MR(否则链路两端都没有自愈路径)。旧版服务端强制校验 repo(400)——调用方需容忍失败。
4025
- */
4026
- getSpaceDaemonToken(taskId) {
4027
- return this.client.get("/vcs/daemon-token", { taskId });
4544
+ // ── 会话宿主机器协议(steward-core-acp-migration §4.3/§4.4,P0 骨架) ──
4545
+ /** 会话级 token 缓存(每会话独立凭证,注入工作区 env MORULA_SESSION_TOKEN) */
4546
+ sessionTokens = /* @__PURE__ */ new Map();
4547
+ setSessionToken(sessionId, token) {
4548
+ this.sessionTokens.set(sessionId, token);
4028
4549
  }
4029
- };
4030
-
4031
- // src/daemon/contract-skill.ts
4550
+ clearSessionToken(sessionId) {
4551
+ this.sessionTokens.delete(sessionId);
4552
+ }
4553
+ /** 会话级机器协议请求(session token 通道;无 token 时回退设备级 client 凭证) */
4554
+ sessionRequest(sessionId, path, options = {}) {
4555
+ const token = this.sessionTokens.get(sessionId);
4556
+ return this.client.request(path, {
4557
+ method: "POST",
4558
+ ...options,
4559
+ ...token ? { token } : {}
4560
+ });
4561
+ }
4562
+ /** 会话级机器协议请求(可指定 method;elicitation 查询用 GET + 长轮询) */
4563
+ sessionRequestWithMethod(sessionId, method, path, options = {}) {
4564
+ const token = this.sessionTokens.get(sessionId);
4565
+ return this.client.request(path, {
4566
+ method,
4567
+ ...options,
4568
+ ...token ? { token } : {}
4569
+ });
4570
+ }
4571
+ /** 会话宿主事件上报(→ agent_events owner=session + WS exec:session 房间) */
4572
+ recordSessionEvent(sessionId, body) {
4573
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/events`, { body });
4574
+ }
4575
+ /**
4576
+ * 会话流式增量上报(P2-2:transient,**不落库**;服务端转推 `agent-task:delta` 到 exec:session 房间)。
4577
+ * 与 `recordSessionEvent` 的语义差别:这里是「可丢的过程信号」,失败不重试、不占 seq。
4578
+ * `kind`:`text` = assistant 正文,`thinking` = 推理原文(durable 对应 `agent_thought_delta`)。
4579
+ */
4580
+ recordSessionStreamChunk(sessionId, body) {
4581
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/stream-chunk`, { body });
4582
+ }
4583
+ /** 会话绑定与指针回传(dimSessionId / workspaceDir / runtimeId;只写绑定列不改状态) */
4584
+ reportSessionBinding(sessionId, body) {
4585
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/session`, { body });
4586
+ }
4587
+ /**
4588
+ * 会话关闭上报。
4589
+ * `reason='idle'`(空闲回收)→ 服务端只回到 `idle`(保留 dimSessionId,下次可 resume);
4590
+ * `reason='command'`(用户/平台显式关闭)→ 终态 `closed`(§4.4/§4.1 的衔接)。
4591
+ */
4592
+ closeAgentSession(sessionId, reason = "command") {
4593
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/close`, { body: { reason } });
4594
+ }
4595
+ /**
4596
+ * 结构化提问登记(§4.6 ②):服务端落库 + 推前端确认卡,立即返回 elicitationId。
4597
+ * 用户应答由 `getElicitation` 长轮询取回(见 session-host.handleElicitation)。
4598
+ */
4599
+ createElicitation(sessionId, body) {
4600
+ return this.sessionRequest(
4601
+ sessionId,
4602
+ `/agent-sessions/${sessionId}/elicitation`,
4603
+ { body }
4604
+ );
4605
+ }
4606
+ /** 结构化提问结果查询(waitMs 内被回答则提前返回;服务端最多持 waitMs) */
4607
+ getElicitation(sessionId, elicitationId, waitMs) {
4608
+ return this.sessionRequestWithMethod(sessionId, "GET", `/agent-sessions/${sessionId}/elicitations/${elicitationId}`, {
4609
+ query: { waitMs },
4610
+ // 服务端最多持 waitMs,客户端留 10s 余量(避免「服务端刚返回、客户端已超时」的假失败)
4611
+ timeoutMs: waitMs + 1e4
4612
+ });
4613
+ }
4614
+ /**
4615
+ * A2(issues/multi-repo-task-missing-workcontext-link.md):**不传 repo** 的平台凭证
4616
+ * ——服务端按「任务空间内 active 的连接」择一签发(凭证是平台凭据,不是某仓库的凭据)。
4617
+ *
4618
+ * 用于无主仓库 / 多仓库任务(daemon 走 scratch 隔离目录、`resolveRepo(detail) = null`):
4619
+ * 这些任务的 agent 同样需要 MORULA_GITLAB_TOKEN / MORULA_GITLAB_URL 才能自己走 GitLab API
4620
+ * 兜底建 MR(否则链路两端都没有自愈路径)。旧版服务端强制校验 repo(400)——调用方需容忍失败。
4621
+ */
4622
+ getSpaceDaemonToken(taskId) {
4623
+ return this.client.get("/vcs/daemon-token", { taskId });
4624
+ }
4625
+ };
4626
+
4627
+ // src/daemon/assistant-text-stream.ts
4628
+ var TEXT_DELTA_WINDOW_MS = 200;
4629
+ var TEXT_SEGMENT_MAX_CHARS = 2e3;
4630
+ var PARAGRAPH_BREAK = "\n\n";
4631
+ var AssistantTextStream = class {
4632
+ constructor(sink, options = {}) {
4633
+ this.sink = sink;
4634
+ this.windowMs = options.windowMs ?? TEXT_DELTA_WINDOW_MS;
4635
+ this.segmentMaxChars = Math.max(1, options.segmentMaxChars ?? TEXT_SEGMENT_MAX_CHARS);
4636
+ this.log = options.log ?? (() => {
4637
+ });
4638
+ }
4639
+ sink;
4640
+ windowMs;
4641
+ segmentMaxChars;
4642
+ log;
4643
+ /** 当前段的完整文本(未落段部分) */
4644
+ buffer = "";
4645
+ /** 当前段已通过 onDelta 发出的字符数 */
4646
+ sent = 0;
4647
+ /** 当前段序号(落段后 +1) */
4648
+ seg = 0;
4649
+ timer = null;
4650
+ disposed = false;
4651
+ /** 段落落库串行链:段落事件必须按 seg 顺序落库(上行 HTTP 并发会乱序 → 回放正文错序) */
4652
+ paragraphChain = Promise.resolve();
4653
+ /** 当前段落序号(诊断/测试用) */
4654
+ get currentSeg() {
4655
+ return this.seg;
4656
+ }
4657
+ /** 喂入一个文本增量 */
4658
+ push(text) {
4659
+ if (this.disposed || !text)
4660
+ return;
4661
+ this.buffer += text;
4662
+ let idx = this.buffer.lastIndexOf(PARAGRAPH_BREAK);
4663
+ while (idx >= 0) {
4664
+ this.cutParagraph(idx + PARAGRAPH_BREAK.length);
4665
+ idx = this.buffer.lastIndexOf(PARAGRAPH_BREAK);
4666
+ }
4667
+ while (this.buffer.length >= this.segmentMaxChars)
4668
+ this.cutParagraph(this.segmentMaxChars);
4669
+ if (this.buffer.length > 0)
4670
+ this.scheduleDelta();
4671
+ }
4672
+ /** 把当前 buffer 的前 `length` 个字符作为一段落切出去(同步改状态,落库走串行链) */
4673
+ cutParagraph(length) {
4674
+ const head = this.buffer.slice(0, length);
4675
+ const rest = this.buffer.slice(length);
4676
+ this.buffer = head;
4677
+ this.sent = Math.min(this.sent, head.length);
4678
+ void this.flushParagraph();
4679
+ this.buffer = rest;
4680
+ this.sent = 0;
4681
+ }
4682
+ /** 强制结束当前段落(工具调用前 / 回合结束前调用并 await) */
4683
+ async flushParagraph() {
4684
+ if (this.timer) {
4685
+ clearTimeout(this.timer);
4686
+ this.timer = null;
4687
+ }
4688
+ this.flushDelta();
4689
+ if (!this.buffer)
4690
+ return this.paragraphChain;
4691
+ const text = this.buffer;
4692
+ const seg = this.seg;
4693
+ this.buffer = "";
4694
+ this.sent = 0;
4695
+ this.seg += 1;
4696
+ this.paragraphChain = this.paragraphChain.then(async () => {
4697
+ try {
4698
+ await this.sink.onParagraph(seg, text);
4699
+ } catch (err) {
4700
+ this.log(`[text-stream] \u6BB5\u843D ${seg} \u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4701
+ }
4702
+ });
4703
+ return this.paragraphChain;
4704
+ }
4705
+ /** 释放定时器(会话关闭 / 任务收尾) */
4706
+ dispose() {
4707
+ this.disposed = true;
4708
+ if (this.timer) {
4709
+ clearTimeout(this.timer);
4710
+ this.timer = null;
4711
+ }
4712
+ this.buffer = "";
4713
+ this.sent = 0;
4714
+ }
4715
+ scheduleDelta() {
4716
+ if (this.timer || this.disposed)
4717
+ return;
4718
+ this.timer = setTimeout(() => {
4719
+ this.timer = null;
4720
+ this.flushDelta();
4721
+ }, this.windowMs);
4722
+ this.timer.unref?.();
4723
+ }
4724
+ /** 把「未发出的新增文本」作为一次 transient 增量直通 */
4725
+ flushDelta() {
4726
+ if (this.buffer.length <= this.sent)
4727
+ return;
4728
+ const text = this.buffer.slice(this.sent);
4729
+ this.sent = this.buffer.length;
4730
+ try {
4731
+ this.sink.onDelta(this.seg, text);
4732
+ } catch (err) {
4733
+ this.log(`[text-stream] \u589E\u91CF\u76F4\u901A\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4734
+ }
4735
+ }
4736
+ };
4737
+
4738
+ // src/daemon/contract-skill.ts
4032
4739
  var COLLABORATION_SKILL_NAME = "morula-working-on-issues";
4033
4740
  var COLLABORATION_SKILL_FRONTMATTER = `---
4034
4741
  name: morula-working-on-issues
@@ -4099,13 +4806,20 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4099
4806
 
4100
4807
  ## 0. \u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587\u4E0E\u4EFB\u52A1\u8BE6\u60C5\u83B7\u53D6\uFF08\u5BF9\u5E94\u4EFB\u52A1\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF0C\u5FC5\u987B\u4E00\u4E00\u5BF9\u5E94\uFF09
4101
4808
 
4102
- \u4EFB\u52A1\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF08\u53D1\u7ED9\u4F60\u7684\u9996\u6761\u6D88\u606F\uFF09\u53EA\u627F\u8F7D\uFF1A**\u4F60\u7684\u8EAB\u4EFD**\uFF08\u9636\u6BB5\u804C\u8D23\uFF09\u3001**\u4F60\u7684\u76EE\u6807**\uFF08\u300C\u4F60\u7684\u76EE\u6807\u300D\u6BB5\uFF09\u3001
4103
- **\u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177**\uFF08\u4ED3\u5E93\u6E05\u5355\u3001skill \u6307\u5F15\uFF09\u3002**\u4EFB\u52A1\u8BE6\u60C5\u4E0D\u8FDB\u63D0\u793A\u8BCD**\uFF0C\u4E00\u5F8B\u7528 CLI \u83B7\u53D6\uFF1A
4809
+ \u4EFB\u52A1\u7CFB\u7EDF\u63D0\u793A\u8BCD\uFF08\u53D1\u7ED9\u4F60\u7684\u9996\u6761\u6D88\u606F\uFF09\u53EA\u627F\u8F7D\u56DB\u6837\uFF1A**\u4F60\u7684\u8EAB\u4EFD\u4E0E\u8FB9\u754C**\uFF08\u9636\u6BB5\u804C\u8D23\uFF09\u3001**\u4F60\u7684\u76EE\u6807**\uFF08\u300C\u4F60\u7684\u76EE\u6807\u300D\u6BB5\uFF09\u3001
4810
+ **\u94A5\u5319**\uFF08\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\u7684\u4EFB\u52A1 ID / \u7A7A\u95F4 ID\u2014\u2014\u53EA\u662F\u547D\u4EE4\u5165\u53C2\uFF0C\u4E0D\u662F\u4EFB\u52A1\u4FE1\u606F\uFF09\u3001**\u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177\u5165\u53E3**\uFF08\u672C skill \u4E0E
4811
+ \`morula task get\` / \`morula repo list\` \u7684\u7528\u6CD5\u6307\u5F15\uFF09\u3002**\u5176\u4F59\u4E0A\u4E0B\u6587\u4E0E\u4EFB\u52A1\u8BE6\u60C5\u4E00\u5F8B\u4E0D\u8FDB\u63D0\u793A\u8BCD**\uFF0C\u7528 CLI \u83B7\u53D6\uFF1A
4104
4812
 
4105
4813
  - **\u5F53\u524D\u4EFB\u52A1\u8BE6\u60C5**\uFF1A\`morula task get\`\uFF08\u65E0\u53C2\uFF09\u2014\u2014taskId \u7531\u6267\u884C\u73AF\u5883\u63D0\u4F9B\uFF08\`MORULA_TASK_ID\` env / \`.morula/checkout.json\`\uFF09\uFF0C\u65E0\u9700\u4E5F\u4E0D\u5E94\u4ECE\u522B\u5904\u731C\u3002\u8FD4\u56DE\uFF1A\`id\`\uFF08\u672C\u4EFB\u52A1 AgentTask ID\uFF0CUUID\uFF09\u3001\`code\`\uFF08\u4EFB\u52A1\u7F16\u53F7\uFF0C\u5982 \`SPW299-59\`\uFF09\u3001\`objectId\`\uFF08\u5173\u8054\u4E1A\u52A1\u4EFB\u52A1\u5BF9\u8C61\uFF09\u3001\`taskPrompt\`\uFF08\u672C\u6B21\u76EE\u6807\u539F\u6587\uFF09\u3001\`contextSnapshot\`\u3001\`status\`\u3002
4106
4814
  - **\u4E1A\u52A1\u4EFB\u52A1\u8BE6\u60C5**\uFF08\u6807\u9898/\u63CF\u8FF0\uFF09\uFF1A\`morula task get <objectId>\`\uFF1B**\u5173\u952E\u8BC4\u8BBA**\uFF08\u7528\u6237\u539F\u59CB\u8BC9\u6C42/\u5206\u6790\u7ED3\u8BBA\uFF09\uFF1A\`morula comment list --object <objectId>\`\u3002
4815
+ - **\u7A7A\u95F4\u4ED3\u5E93\u6E05\u5355**\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\u5224\u65AD\u4F9D\u636E\uFF09\uFF1A\`morula repo list --space <spaceId>\`\u2014\u2014\u63D0\u793A\u8BCD\u4E0D\u5217\u4ED3\u5E93\uFF0C\u9700\u8981\u65F6\u81EA\u5DF1\u67E5\u3002
4107
4816
  - **\u300C\u4F60\u7684\u76EE\u6807\u300D**\uFF1A\u7CFB\u7EDF\u63D0\u793A\u300C\u4F60\u7684\u76EE\u6807\u300D\u6BB5 = \u672C\u6B21\u4EFB\u52A1\u7684\u539F\u59CB\u8BC9\u6C42\uFF0C\u636E\u6B64\u5224\u65AD\u8981\u505A\u4EC0\u4E48\u3002
4108
4817
 
4818
+ > **\u8C03 CLI \u4E00\u5F8B\u7528 \`$MORULA_CLI\`**\uFF08daemon \u6CE8\u5165\u7684\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u6307\u5411\u4E0E\u672C\u6B21\u8FD0\u884C\u540C\u6E90\u7684\u6784\u5EFA\uFF1B\u4E0B\u6587\u6240\u6709 \`morula xxx\`
4819
+ > \u90FD\u6309 \`$MORULA_CLI xxx\` \u6267\u884C\uFF09\u3002PATH \u4E0A\u7684\u88F8 \`morula\` \u53EF\u80FD\u662F\u5BBF\u4E3B\u81EA\u5E26\u7684\u65E7\u7248\u672C\uFF08\u7F3A\u547D\u4EE4\u9762\u3001\u4E14\u4E0D\u8BA4\u4F1A\u8BDD/\u4EFB\u52A1
4820
+ > \u51ED\u8BC1 \u2192 \u5199\u64CD\u4F5C\u7F72\u540D\u9519\u8BEF\uFF09\uFF0C\u4E0D\u8981\u7528\u5B83\uFF1B\`env VAR=... $MORULA_CLI xxx\` \u8FD9\u7C7B env \u5305\u88C5\u4E0B\u540C\u6837\u53EF\u7528\u3002
4821
+ > \u4EC5\u5F53 \`$MORULA_CLI\` \u4E3A\u7A7A\u65F6\u624D\u9000\u56DE\u88F8 \`morula\`\u3002
4822
+
4109
4823
  > **\u5DE5\u4F5C\u76EE\u5F55 = \u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839**\uFF1A\u6839\u4E0B\u653E \`AGENTS.md\`\uFF08\u7A7A\u95F4\u9879\u76EE\u8BF4\u660E\uFF0C\u5DF2\u4F5C\u4E3A project.instructions \u81EA\u52A8\u52A0\u8F7D\uFF09\u3001
4110
4824
  > \`.agents/\`\u3001\`.agent_context/\`\u3001\`.morula/\`\uFF1B**\u5404\u4ED3\u5E93\u662F\u5DE5\u4F5C\u533A\u6839\u4E0B\u7684\u5B50\u76EE\u5F55**\uFF08\u89C1 0.5\uFF09\u3002
4111
4825
  > \u4E0B\u6587\u7684 \`AGENTS.md\`/\`.agents/\`/\`.agent_context/\` \u8DEF\u5F84\u90FD\u76F8\u5BF9\u8FD9\u4E2A\u5DE5\u4F5C\u533A\u6839\u3002
@@ -4113,6 +4827,11 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4113
4827
  > **\u4EFB\u52A1\u4E0A\u4E0B\u6587\u6587\u4EF6**\uFF1A\u540C\u4E00\u5957\u4E0A\u4E0B\u6587\u4E5F\u5199\u5728\u5DE5\u4F5C\u76EE\u5F55 \`.agent_context/task-context.md\`\uFF08\u4E0E\u9996\u6761\u7CFB\u7EDF\u63D0\u793A\u540C\u6E90\uFF09\u3002
4114
4828
  > \u9700\u8981\u518D\u6B21\u786E\u8BA4\u4EFB\u52A1\u8FB9\u754C\u65F6**\u8BFB\u53D6\u8BE5\u6587\u4EF6**\uFF08\`cat .agent_context/task-context.md\`\uFF09\uFF0C\u4E0D\u8981\u51ED\u8BB0\u5FC6\u63A8\u65AD\u3002
4115
4829
 
4830
+ > **\u672C\u5951\u7EA6\u81EA\u8EAB\u7684\u8BFB\u53D6\u65B9\u5F0F**\uFF1A\u5951\u7EA6\u4EE5\u9879\u76EE\u7EA7 skill \u5F62\u5F0F\u6CE8\u5165\u5728 \`.agents/skills/morula-working-on-issues/SKILL.md\`\u3002
4831
+ > \u7528 \`skill\` \u5DE5\u5177\u6309\u540D\u52A0\u8F7D**\u53EF\u80FD\u62A5 \`Unknown skill\`**\uFF08\u8BE5\u5DE5\u5177\u7684\u53D1\u73B0\u8303\u56F4\u4E0D\u542B\u9879\u76EE\u7EA7\u76EE\u5F55\uFF09\u2014\u2014\u6B64\u65F6
4832
+ > **\u76F4\u63A5\u8BFB\u6587\u4EF6**\uFF08\`cat .agents/skills/morula-working-on-issues/SKILL.md\` \u6216 read \u5DE5\u5177\uFF09\uFF0C
4833
+ > **\u4E0D\u8981\u56E0\u4E3A\u52A0\u8F7D\u5931\u8D25\u5C31\u8DF3\u8FC7\u5951\u7EA6**\uFF08issues/injected-contract-skill-not-loadable-by-skill-tool.md\uFF09\u3002
4834
+
4116
4835
  \u8BE6\u60C5\u89C1\u4E0B\u6587\u5404\u8282\u5BF9 TaskID / \u5BF9\u5916\u7F16\u53F7\u7684\u5177\u4F53\u7528\u6CD5\u3002
4117
4836
 
4118
4837
  ## 0.1 \u901A\u7528\u884C\u4E3A\u7EA6\u675F
@@ -4137,7 +4856,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4137
4856
 
4138
4857
  ## 0.3 \u5206\u652F\u547D\u540D\uFF1A\u7531\u4F60\uFF08Agent\uFF09\u53D6\u540D\u5E76\u4E0A\u62A5
4139
4858
 
4140
- - **\u4EFB\u52A1 ID \u83B7\u53D6**\uFF1A\u672C\u4EFB\u52A1\u7684 AgentTask ID\uFF08UUID\uFF09\u7528 \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u8FD4\u56DE\u7684 \`id\` \u53D6\u5F97\u2014\u2014taskId \u94A5\u5319\u7531\u6267\u884C\u73AF\u5883\u63D0\u4F9B\uFF0C\u4E0D\u8FDB\u7CFB\u7EDF\u63D0\u793A\u3002\`.agent_context/task-context.md\` \u5F00\u5934\u4E5F\u6709\u540C\u4E00 UUID\u3002\u9700\u8981\u8C03\u7528 \`morula task branch\` \u65F6\u7528\u5B83\u4F5C\u4E3A taskId\u3002
4859
+ - **\u4EFB\u52A1 ID \u83B7\u53D6**\uFF1A\u672C\u4EFB\u52A1\u7684 AgentTask ID\uFF08UUID\uFF09\u5728\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF08\u4E5F\u662F \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u8FD4\u56DE\u7684 \`id\`\uFF0C\`.agent_context/task-context.md\` \u5F00\u5934\u4EA6\u6709\uFF09\u3002\u9700\u8981\u8C03\u7528 \`morula task branch\` \u65F6\u7528\u5B83\u4F5C\u4E3A taskId\u3002
4141
4860
  - **\u91CD\u8981\u533A\u5206**\uFF1A\`morula task branch <taskId>\` \u7684 taskId \u7528**\u672C\u4EFB\u52A1 UUID**\uFF08\u673A\u5668\u534F\u8BAE\uFF09\uFF1B\u4F46 **MR / \u5206\u652F / PR \u91CC\u7684\u4EFB\u52A1\u7F16\u53F7\u7528\u5BF9\u5916\u7F16\u53F7 code**\uFF08\u5982 \`AGENT-958201\`\uFF0C\u7528\u4E8E\u670D\u52A1\u7AEF\u5173\u8054/\u5173\u5355\uFF09\u2014\u2014**\u4E0D\u8981\u7528 UUID \u6807\u8BB0 MR**\uFF08UUID \u5173\u5355\u53EA\u8BA4\u4E1A\u52A1\u5BF9\u8C61\uFF0CAgentTask \u4E0D\u4F1A\u88AB\u8BC6\u522B\uFF0C\u4F1A\u5BFC\u81F4 MR \u5173\u8054\u5931\u8D25\uFF09\u3002
4142
4861
  - **\u53EA\u6709\u9700\u8981\u6539\u4EE3\u7801\u5E76\u63A8\u5206\u652F\u7684\u4EFB\u52A1\u624D\u53D6\u540D**\uFF1B\u7EAF\u95EE\u7B54/\u53EA\u8BFB\u8C03\u7814/\u672C\u5730\u8D77\u670D\u52A1\u4E0D\u5EFA\u5206\u652F\uFF08\u89C1\u7B2C 3 \u6761\u51B3\u7B56\u6811\uFF09\u3002
4143
4862
  - **\u5148\u68C0\u67E5\u5F53\u524D\u5206\u652F**\uFF1A\u82E5\u4ED3\u5E93\u5DF2\u6709 feature \u5206\u652F\uFF08\u540C\u4EFB\u52A1/\u540C\u4E00\u6539\u52A8\u95ED\u73AF\u7684\u540E\u7EED\u8F6E\u6B21\u590D\u7528\uFF09\uFF0C**\u76F4\u63A5\u7EE7\u7EED\u7528\u5F53\u524D\u5206\u652F\uFF0C\u4E0D\u8981\u65B0\u5EFA**\uFF08\u5728\u4ED3\u5E93\u76EE\u5F55\u5185\u6267\u884C\uFF09\uFF1A
@@ -4190,13 +4909,13 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4190
4909
  \u251C\u2500\u2500 .agents/skills/ \u2190 \u534F\u4F5C\u5951\u7EA6 + \u672C\u4EFB\u52A1\u81EA\u5B9A\u4E49 skill
4191
4910
  \u251C\u2500\u2500 .agent_context/task-context.md \u2190 \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08\u89C1 0 \u8282\uFF09
4192
4911
  \u251C\u2500\u2500 .morula/checkout.json \u2190 daemon \u51ED\u636E\uFF08CLI \u5185\u90E8\u4F7F\u7528\uFF0C\u4E0D\u8981\u624B\u52A8\u8BFB\uFF09
4193
- \u251C\u2500\u2500 <owner>-<repo>/ \u2190 \u4E3B\u4ED3\u5E93\uFF08\u7CFB\u7EDF\u63D0\u793A\u300C\u4ED3\u5E93\u53EF\u7528\u6027\u300D\u91CC\u7684\u90A3\u4E2A\uFF09
4912
+ \u251C\u2500\u2500 <owner>-<repo>/ \u2190 \u4E3B\u4ED3\u5E93\uFF08\u5DF2\u6309\u5E73\u53F0\u89E3\u6790\u7ED3\u679C\u7269\u5316\uFF1B\u672C\u5730\u6CA1\u6709\u7684\u4ED3\u5E93\u6309\u9700 checkout\uFF09
4194
4913
  \u2514\u2500\u2500 <owner>-<repo2>/ \u2190 \u6B21\u4ED3\u5E93 / \u53EA\u8BFB\u68C0\u51FA\uFF08\u6309\u9700\u7269\u5316\uFF0C\u4E0E\u4E3B\u4ED3\u5E93\u540C\u7EA7\uFF09
4195
4914
  \`\`\`
4196
4915
 
4197
4916
  - **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
4198
4917
  \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
4199
- - **\u4E3B\u4ED3\u5E93**\uFF1A\u7CFB\u7EDF\u63D0\u793A\u7ED9\u51FA\u7684\u4ED3\u5E93\u5DF2\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\u76F4\u63A5\u5728\u8BE5\u76EE\u5F55\u5185\u5EFA\u5206\u652F/\u63D0\u4EA4/\u63A8\u5206\u652F/\u63D0 PR\u3002
4918
+ - **\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
4200
4919
  - **\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\u7528 \`morula repo checkout <taskId> <repoFullName>\`\uFF08\u672C\u5730 daemon \u7AEF\u70B9\uFF0C**\u672C\u8F6E\u7ACB\u5373\u7269\u5316**\u5E76\u8FD4\u56DE\u8DEF\u5F84\uFF0C\u901A\u5E38\u662F\u5DE5\u4F5C\u533A\u6839\u4E0B\u7684 \`<\u4ED3\u5E93\u76EE\u5F55>-readonly\`\uFF09\uFF0C\u7136\u540E\u5728\u8BE5\u8DEF\u5F84\u5185\u53EA\u8BFB\u67E5\u8BC1\uFF1A
4201
4920
  - \u8BFB\u4EE3\u7801/\u63D0\u4EA4/\u5206\u652F\uFF1A\`git -C <\u8DEF\u5F84> log --oneline -20\`\u3001\`git -C <\u8DEF\u5F84> show <sha>\`\u3001\`git -C <\u8DEF\u5F84> branch -a\`\u3001\`rg "\u5173\u952E\u8BCD" <\u8DEF\u5F84>\`\u3001\u8BFB\u6587\u4EF6\u7B49\uFF08**\u53EA\u8BFB**\uFF0C\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u6539\u4EE3\u7801\u3001\u4E0D\u63D0 PR\uFF09\uFF1B
4202
4921
  - \u5237\u65B0\u8FDC\u7AEF\u6700\u65B0\uFF1A\`git -C <\u8DEF\u5F84> fetch origin --prune\`\uFF08remote \u5DF2\u914D\u7F6E SSH\uFF0C\u514D\u5BC6\uFF09\u3002
@@ -4204,7 +4923,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4204
4923
  - **\u82E5\u4EFB\u52A1\u9700\u8981\u6539\u4EE3\u7801**\uFF1A\u7528 \`morula repo checkout <taskId> <repoFullName>\` \u62C9\u53D6/\u5207\u6362\u4ED3\u5E93\uFF08\u8BA4\u8BC1\u5DF2\u7531\u6267\u884C\u73AF\u5883\u6CE8\u5165\uFF09\uFF0C
4205
4924
  \u62FF\u5230\u8FD4\u56DE\u7684\u4ED3\u5E93\u8DEF\u5F84\u540E**chdir \u8FDB\u8BE5\u4ED3\u5E93\u76EE\u5F55**\uFF0C\u518D\u6309\u7B2C 0.3 \u6761\u5EFA\u5206\u652F\u3001\u7B2C 3 \u6761\u63D0 PR\u3002\u53EF\u9009 \`--ref <branch|sha>\` \u6307\u5B9A\u68C0\u51FA\u5206\u652F\u3002
4206
4925
  - **\u82E5\u4EFB\u52A1\u7EAF\u95EE\u7B54\u3001\u6216\u7A7A\u95F4\u65E0\u53EF\u7528\u4ED3\u5E93**\uFF1A\u76F4\u63A5\u56DE\u7B54\u7528\u6237\u95EE\u9898\uFF0C\u4E0D\u6784\u5EFA\u5206\u652F/PR\uFF08\u89C1\u7B2C 3 \u6761\u51B3\u7B56\u6811\uFF09\u3002
4207
- - **\u82E5\u4EFB\u52A1\u6D89\u53CA\u591A\u4E2A\u4ED3\u5E93\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\uFF09**\uFF1A\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\u7531**\u4F60**\u7ED3\u5408\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\uFF08\u7CFB\u7EDF\u63D0\u793A\u300C\u591A\u4ED3\u5E93\u4EFB\u52A1\u300D\u6BB5 + \u4EFB\u52A1\u63CF\u8FF0\u300C\u6D89\u53CA\u4ED3\u5E93\u300D\u6BB5\u662F\u7BA1\u5BB6\u7684\u521D\u59CB\u5224\u65AD\uFF1B\u6267\u884C\u4E2D\u53D1\u73B0\u8FD8\u9700\u6539\u52A8\u5176\u4ED6\u4ED3\u5E93\u65F6\uFF0C\u81EA\u884C prepare \u62C9\u53D6\uFF09\uFF0C**\u4E0D\u9700\u8981\u5411\u7528\u6237\u9010\u4E2A\u786E\u8BA4**\uFF1A
4926
+ - **\u82E5\u4EFB\u52A1\u6D89\u53CA\u591A\u4E2A\u4ED3\u5E93\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\uFF09**\uFF1A\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\u7531**\u4F60**\u7ED3\u5408\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\uFF08\u4EFB\u52A1\u63CF\u8FF0\u91CC\u7684\u300C\u6D89\u53CA\u4ED3\u5E93\u300D\u662F\u7BA1\u5BB6\u7684\u521D\u59CB\u5224\u65AD\uFF1B\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u54EA\u4E9B\u4ED3\u5E93\u7528 \`morula repo list --space <spaceId>\` \u67E5\uFF1B\u6267\u884C\u4E2D\u53D1\u73B0\u8FD8\u9700\u6539\u52A8\u5176\u4ED6\u4ED3\u5E93\u65F6\uFF0C\u81EA\u884C prepare \u62C9\u53D6\uFF09\uFF0C**\u4E0D\u9700\u8981\u5411\u7528\u6237\u9010\u4E2A\u786E\u8BA4**\uFF1A
4208
4927
  1. \u4E3B\u4ED3\u5E93\uFF1A\u5728\u5B83\u7684\u5B50\u76EE\u5F55\u5185\u76F4\u63A5 git \u64CD\u4F5C\uFF08\u5DE5\u4F5C\u533A\u6839\u4E0D\u662F\u4ED3\u5E93\uFF09\uFF1B
4209
4928
  2. \u5176\u4ED6\u4ED3\u5E93\u7528 \`morula repo prepare <taskId> <repoFullName>\` \u7269\u5316\u5230\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\u4E0B\uFF08**\u4E0E\u4E3B\u4ED3\u5E93\u540C\u7EA7\u7684\u5B50\u76EE\u5F55**\uFF0C\u672C\u5730 daemon \u7AEF\u70B9\uFF0C\u672C\u8F6E\u7ACB\u5373\u7269\u5316\u5E76\u8FD4\u56DE\u8DEF\u5F84\uFF1Bgit \u534F\u8BAE\u62C9\u53D6\uFF0C\u4E0D\u8D70\u5E73\u53F0 API\uFF09\uFF1B
4210
4929
  3. \u5728\u54EA\u4E2A\u4ED3\u5E93\u6539\u52A8\uFF0C\u5C31\u5728\u54EA\u4E2A\u4ED3\u5E93\u5185\u5EFA\u5206\u652F\uFF08\u7B2C 0.3 \u6761\u547D\u540D\u89C4\u8303\uFF09\u3001\u63A8\u5206\u652F\u5E76\u5EFA PR/MR\uFF08title/description \u5E26\u4EFB\u52A1\u7F16\u53F7\uFF09\uFF1B\u6BCF\u4E2A\u4ED3\u5E93\u7684 PR \u670D\u52A1\u7AEF\u90FD\u4F1A\u81EA\u52A8\u5173\u8054\u5230\u672C\u4EFB\u52A1\u3002**\u5206\u652F\u540D\u9010\u4ED3\u4E0A\u62A5**\uFF1A\u4E3B\u4ED3\u5E93\uFF08\u5176\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D>\`\uFF1B\u6B21\u4ED3\u5E93\uFF08\u540C\u7EA7\u5B50\u76EE\u5F55\uFF09\u7528 \`morula task branch <taskId> --name <\u5206\u652F\u540D> --repo <owner/name>\` \u2014\u2014 \u670D\u52A1\u7AEF\u636E\u6B64\u628A\u5206\u652F\u540D\u5199\u5230\u8BE5\u4ED3\u5E93\u7684\u5DE5\u4F5C\u4E0A\u4E0B\u6587\uFF08\u5E73\u53F0\u4FA7\u9010\u4ED3\u5EFA MR \u515C\u5E95\u3001\u4EA4\u4ED8\u6E05\u5355\u90FD\u4F9D\u8D56\u5B83\uFF1B\u4E0D\u62A5\u5219\u4E00\u76F4\u505C\u5728\u5360\u4F4D\u5206\u652F\u540D \`morula/tmp-*\`\uFF0C\u5E73\u53F0\u627E\u4E0D\u5230\u4F60\u7684\u5206\u652F\uFF09\uFF1B
@@ -4226,8 +4945,8 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4226
4945
  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
4227
4946
  # \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
4228
4947
  \`\`\`
4229
- - **\u53EF\u7528\u4ED3\u5E93\u6E05\u5355**\u7531\u7CFB\u7EDF\u63D0\u793A\u76F4\u63A5\u7ED9\u51FA\uFF08\u300C\u53EF\u7528\u4ED3\u5E93\u300D/\u300C\u591A\u4ED3\u5E93\u4EFB\u52A1\u300D\u6BB5\uFF09\uFF0C**\u4E0D\u9700\u8981**\u81EA\u5DF1\u67E5\uFF1B\u53EA\u6709\u5728\u7CFB\u7EDF\u63D0\u793A\u672A\u7ED9\u51FA\u3001\u53C8\u786E\u5B9E\u9700\u8981\u679A\u4E3E\u65F6\u624D\u7528
4230
- \`morula repo list --space <spaceId>\`\uFF08\`<spaceId>\` = \`morula task get\` \u8FD4\u56DE\u7684 \`contextSnapshot.spaceId\`\uFF09\u3002
4948
+ - **\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
4949
+ \`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
4231
4950
  - **\u6BCF\u4E2A\u547D\u4EE4\u7684\u5B8C\u6574\u53C2\u6570\u4EE5 \`morula <resource> --help\` \u4E3A\u51C6**\uFF08\u5982 \`morula repo --help\` \u53EA\u5217 repo \u7684\u7528\u6CD5\uFF09\uFF1B\u4E0D\u786E\u5B9A\u67D0\u547D\u4EE4\u652F\u6301\u54EA\u4E9B\u65D7\u6807\u65F6\u5148\u67E5\u5B83\uFF0C\u4E0D\u8981\u51ED\u8BB0\u5FC6\u62FC\u53C2\u6570\u3002
4232
4951
  - **\u7981\u6B62** \`curl\` / \`wget\` \u76F4\u8FDE\u5E73\u53F0 REST API\uFF1B\u7981\u6B62\u8BFB\u53D6 \`~/.morula-cli/config.yaml\`\u3001token \u6587\u4EF6\u6216\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u5BC6\u94A5\u3002
4233
4952
  - **\u672C\u5730 \`.morula/checkout.json\` \u662F daemon \u7BA1\u7406\u6587\u4EF6**\uFF08\`morula repo checkout\` \u5185\u90E8\u8BFB\u53D6\u7AEF\u53E3\u4E0E\u4EFB\u52A1\u51ED\u636E\uFF09\uFF0C**\u4E0D\u8981\u624B\u52A8\u8BFB\u53D6/\u590D\u5236\u5176\u4E2D\u7684\u5B57\u6BB5**\uFF1B\u51ED\u636E\u7531 CLI \u5904\u7406\uFF0C\u4F60\u4E0D\u9700\u8981\u4E5F\u4E0D\u5E94\u5F53\u770B\u5230\u5B83\u3002
@@ -4414,7 +5133,7 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
4414
5133
 
4415
5134
  - **\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
4416
5135
  - **\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
4417
- - **\u8BC4\u8BBA @\u89E6\u53D1\u4EFB\u52A1\uFF08\u672C\u4EFB\u52A1\u7531\u8BC4\u8BBA\u89E6\u53D1\uFF0C\u89E6\u53D1\u8BC4\u8BBA ID \u89C1\u7CFB\u7EDF\u63D0\u793A\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
5136
+ - **\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
4418
5137
  - **\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
4419
5138
  - **\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
4420
5139
  - \u5FC5\u7B54\uFF1A**\u505A\u4E86\u4EC0\u4E48\u3001\u7ED3\u679C\u5982\u4F55\u3001\u7528\u6237\u9700\u8981\u77E5\u9053\u7684\u540E\u7EED**\u3002
@@ -4469,7 +5188,8 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
4469
5188
  | 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\` |
4470
5189
  | 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\` |
4471
5190
  | 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\` |
4472
- | \u7CFB\u7EDF\u63D0\u793A\u6CE8\u5165\uFF08\u9636\u6BB5\u804C\u8D23 / \u76EE\u6807 / \u4ED3\u5E93\u6E05\u5355 / \u5DE5\u5177\u6307\u5F15\uFF09\uFF1B\u4EFB\u52A1\u8BE6\u60C5\u7531 \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u83B7\u53D6 | \`buildTaskPromptWithContract\`\uFF08\`runner.ts\`\uFF0C\u552F\u4E00\u8FDB dim \u7684 messages[0]\uFF09\uFF1BCLI \`packages/morula-cli/src/commands/task.ts\`\uFF08resolveTaskId \u2192 \`GET /agent-tasks/:id\`\uFF09 | \`packages/runtime/src/daemon/runner.ts\` |
5191
+ | \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\` |
5192
+ | \u7CFB\u7EDF\u63D0\u793A\u6CE8\u5165\uFF08\u8EAB\u4EFD\u4E0E\u8FB9\u754C = \u9636\u6BB5\u804C\u8D23 / \u76EE\u6807 / \u94A5\u5319 taskId+spaceId / \u5DE5\u5177\u5165\u53E3\u6307\u5F15\uFF09\uFF1B\u4EFB\u52A1\u8BE6\u60C5\u4E0E\u4ED3\u5E93\u6E05\u5355\u7531 \`morula task get\`\uFF08\u65E0\u53C2\uFF09\u3001\`morula repo list --space\` \u83B7\u53D6 | \`buildTaskPromptWithContract\`\uFF08\`runner.ts\`\uFF0C\u552F\u4E00\u8FDB dim \u7684 messages[0]\uFF09\uFF1BCLI \`packages/morula-cli/src/commands/task.ts\`\uFF08resolveTaskId \u2192 \`GET /agent-tasks/:id\`\uFF09 | \`packages/runtime/src/daemon/runner.ts\` |
4473
5193
  | \u8FD0\u884C\u4E2D\u68C0\u67E5\u70B9\uFF08\u672C\u56DE\u5408\u5185\u53D6\u7528\u6237\u610F\u89C1\uFF09 | CLI \`morula task inbox [--peek]\` \u2192 \`GET /agent-tasks/:id/thread/inbox\`\uFF08\u673A\u5668\u534F\u8BAE\uFF1B\u9ED8\u8BA4\u53D6\u8D70\u5373\u6D88\u8D39\uFF0C\u843D \`consumed\` + \u5F53\u56DE\u5408\u53F7\uFF09\uFF1B\u5951\u7EA6\u65F6\u673A\u89C1\u672C SKILL 1.3 | \`packages/morula-cli/src/commands/task.ts\`\u3001\`src/modules/agent-task/chat/object-agent-chat.controller.ts\`\u3001\`object-agent-message.service.ts\`\uFF08\`getThreadInbox\`\uFF09 |
4474
5194
  | \u8FD0\u884C\u4E2D\u6253\u65AD\uFF08\u540C\u4E00\u4F1A\u8BDD\u91CD\u5F00\u4E00\u8F6E\uFF09 | daemon \u5728\u5B89\u5168\u70B9\uFF08\`inFlightTools === 0\`\uFF09\`session/cancel\` \u2192 \`session/resume\` \u2192 \u7528\u65B0\u610F\u89C1\u91CD\u5F00 prompt\uFF1B\u56DE\u5408\u53F7\u4E0E\u4E2D\u65AD\u70B9\u4E0A\u62A5\u4E3A \`turn_started\` / \`turn_interrupted\` \u4E8B\u4EF6 | \`packages/runtime/src/daemon/acp/dim-acp-executor.ts\`\uFF08\u56DE\u5408\u5FAA\u73AF\uFF09\u3001\`runner.ts\`\uFF08\`buildSteerPort\`\uFF09\u3001\`src/modules/agent-task/chat/object-agent-message.service.ts\`\uFF08\`steerDeliveryStrategy\`\uFF09 |
4475
5195
 
@@ -4545,7 +5265,7 @@ function isFlagEnabled(name, defaultValue = true) {
4545
5265
 
4546
5266
  // src/daemon/git-env.ts
4547
5267
  import { execFileSync as execFileSync6 } from "node:child_process";
4548
- import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
5268
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "node:fs";
4549
5269
  import { join as join11 } from "node:path";
4550
5270
  var GIT_CANDIDATE_DIRS = ["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin", "/opt/local/bin"];
4551
5271
  function prependToPath(dirs) {
@@ -4562,14 +5282,14 @@ function ensureGitOnPath() {
4562
5282
  return;
4563
5283
  } catch {
4564
5284
  }
4565
- const missing = prependToPath(GIT_CANDIDATE_DIRS.filter((d) => existsSync8(join11(d, "git"))));
5285
+ const missing = prependToPath(GIT_CANDIDATE_DIRS.filter((d) => existsSync7(join11(d, "git"))));
4566
5286
  if (missing.length) {
4567
5287
  console.warn(`[daemon] PATH \u7F3A\u5C11 git\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
4568
5288
  }
4569
5289
  }
4570
5290
  function ensureNodeOnPath() {
4571
5291
  const nodeDir = process.execPath ? join11(process.execPath, "..") : "";
4572
- const candidates = [nodeDir, "/usr/local/bin", "/opt/homebrew/bin"].filter((d) => d && existsSync8(join11(d, "node")));
5292
+ const candidates = [nodeDir, "/usr/local/bin", "/opt/homebrew/bin"].filter((d) => d && existsSync7(join11(d, "node")));
4573
5293
  const missing = prependToPath(candidates);
4574
5294
  if (missing.length) {
4575
5295
  console.warn(`[daemon] PATH \u7F3A\u5C11 node\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
@@ -4615,7 +5335,7 @@ function buildDaemonRepoUrl(repoFullName, baseUrl) {
4615
5335
  function ensureRepo(workdir, tenantId, repo, env, remoteUrl, daemonRemoteUrl) {
4616
5336
  const repoDir = join11(workdir, tenantId, repo);
4617
5337
  const fetchUrl = daemonRemoteUrl ?? remoteUrl ?? buildDaemonRepoUrl(repo);
4618
- if (existsSync8(join11(repoDir, ".git"))) {
5338
+ if (existsSync7(join11(repoDir, ".git"))) {
4619
5339
  if (remoteUrl)
4620
5340
  syncRemoteUrl(repoDir, remoteUrl, env);
4621
5341
  try {
@@ -4660,7 +5380,7 @@ function syncRemoteUrl(repoDir, remoteUrl, env) {
4660
5380
  function isLinkedWorktree(repoDir) {
4661
5381
  try {
4662
5382
  const gitDir = execFileSync6("git", ["-C", repoDir, "rev-parse", "--absolute-git-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
4663
- return !!gitDir && existsSync8(join11(gitDir, "commondir"));
5383
+ return !!gitDir && existsSync7(join11(gitDir, "commondir"));
4664
5384
  } catch {
4665
5385
  return false;
4666
5386
  }
@@ -4698,7 +5418,7 @@ function ensureGitIdentity(repoDir, personaName) {
4698
5418
  }
4699
5419
 
4700
5420
  // src/daemon/outbox.ts
4701
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync5 } from "node:fs";
5421
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync7, writeFileSync as writeFileSync5 } from "node:fs";
4702
5422
  import { homedir as homedir9 } from "node:os";
4703
5423
  import { join as join12 } from "node:path";
4704
5424
  var OUTBOX_RETRY_SCHEDULE = [1e3, 4e3, 16e3, 6e4, 3e5];
@@ -4714,14 +5434,14 @@ function writeOutbox(entry, configDir) {
4714
5434
  }
4715
5435
  function readOutboxEntries(configDir) {
4716
5436
  const dir = outboxDir(configDir);
4717
- if (!existsSync9(dir))
5437
+ if (!existsSync8(dir))
4718
5438
  return [];
4719
5439
  const entries = [];
4720
5440
  for (const file of readdirSync2(dir)) {
4721
5441
  if (!file.endsWith(".json"))
4722
5442
  continue;
4723
5443
  try {
4724
- const raw = readFileSync6(join12(dir, file), "utf8");
5444
+ const raw = readFileSync5(join12(dir, file), "utf8");
4725
5445
  const parsed = JSON.parse(raw);
4726
5446
  if (parsed && typeof parsed.taskId === "string")
4727
5447
  entries.push(parsed);
@@ -4731,7 +5451,7 @@ function readOutboxEntries(configDir) {
4731
5451
  return entries;
4732
5452
  }
4733
5453
  function removeOutbox(taskId, configDir) {
4734
- rmSync6(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
5454
+ rmSync7(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
4735
5455
  }
4736
5456
  function staleDir(configDir) {
4737
5457
  return join12(outboxDir(configDir), "stale");
@@ -4754,13 +5474,120 @@ function outboxAge(entry) {
4754
5474
  return Number.isFinite(created) ? Date.now() - created : Number.POSITIVE_INFINITY;
4755
5475
  }
4756
5476
 
5477
+ // src/daemon/plugin-register.ts
5478
+ import { chmodSync as chmodSync3, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync as rmSync8, statSync as statSync2, writeFileSync as writeFileSync6 } from "node:fs";
5479
+ import { homedir as homedir10 } from "node:os";
5480
+ import { dirname as dirname6, join as join13 } from "node:path";
5481
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5482
+ var PLUGIN_NAME = "morula-cli";
5483
+ var MANIFEST_DIR_NAME = ".codex-plugin";
5484
+ function legacyAgentsPluginDir(home = homedir10()) {
5485
+ return join13(home, ".agents", "plugins", PLUGIN_NAME);
5486
+ }
5487
+ function resolveDimcodeHome(env = process.env, home = homedir10()) {
5488
+ const fromEnv = env.DIMCODE_HOME?.trim();
5489
+ return fromEnv || join13(home, ".dimcode", "v2");
5490
+ }
5491
+ function defaultPluginsRoot(env = process.env, home = homedir10()) {
5492
+ return join13(resolveDimcodeHome(env, home), "plugins");
5493
+ }
5494
+ function resolvePackagePaths(entryUrl) {
5495
+ const distDir = dirname6(fileURLToPath2(entryUrl));
5496
+ return {
5497
+ packageRoot: dirname6(distDir),
5498
+ distDir,
5499
+ assetsDir: join13(distDir, "plugin")
5500
+ };
5501
+ }
5502
+ function buildLauncherScript(opts) {
5503
+ const entry = join13(opts.packageRoot, "dist", "morula.js");
5504
+ return `#!/bin/sh
5505
+ # Morula \u4E1A\u52A1 CLI \u542F\u52A8\u5668\uFF08\u7531 \`morula-runtime setup\` \u751F\u6210\uFF1BNode \u5347\u7EA7\u540E\u91CD\u8DD1 setup \u5237\u65B0\uFF09\u3002
5506
+ # \u4E0D\u4F9D\u8D56\u8C03\u7528\u65B9 PATH \u91CC\u7684 node\uFF08agent \u6267\u884C\u73AF\u5883 PATH \u5E38\u88AB\u88C1\u526A\uFF09\uFF0C\u4EC5\u4F5C\u515C\u5E95\u3002
5507
+ NODE=${JSON.stringify(opts.nodePath)}
5508
+ [ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
5509
+ if [ -z "$NODE" ]; then
5510
+ echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
5511
+ exit 127
5512
+ fi
5513
+ exec "$NODE" ${JSON.stringify(entry)} "$@"
5514
+ `;
5515
+ }
5516
+ function assetsReady(paths) {
5517
+ return existsSync9(join13(paths.assetsDir, "plugin.json")) && existsSync9(join13(paths.assetsDir, "skills"));
5518
+ }
5519
+ function listSkillNames(assetsDir) {
5520
+ const dir = join13(assetsDir, "skills");
5521
+ if (!existsSync9(dir))
5522
+ return [];
5523
+ return readdirSync3(dir).filter((name) => {
5524
+ try {
5525
+ return statSync2(join13(dir, name)).isDirectory();
5526
+ } catch {
5527
+ return false;
5528
+ }
5529
+ }).sort();
5530
+ }
5531
+ function pluginBinDir(env = process.env, home = homedir10()) {
5532
+ return join13(defaultPluginsRoot(env, home), PLUGIN_NAME, "bin");
5533
+ }
5534
+ function pluginBinPath(env = process.env, home = homedir10()) {
5535
+ return join13(pluginBinDir(env, home), "morula");
5536
+ }
5537
+ function ownCliEntryPath(moduleUrl = import.meta.url) {
5538
+ const entry = join13(resolvePackagePaths(moduleUrl).packageRoot, "dist", "morula.js");
5539
+ return existsSync9(entry) ? entry : null;
5540
+ }
5541
+ function resolveAgentCliPath(moduleUrl = import.meta.url, env = process.env, home = homedir10()) {
5542
+ return ownCliEntryPath(moduleUrl) ?? pluginBinPath(env, home);
5543
+ }
5544
+ function prependPluginBinToPath(currentPath, env = process.env, home = homedir10()) {
5545
+ const binDir = pluginBinDir(env, home);
5546
+ const segments = (currentPath ?? "").split(":").filter(Boolean);
5547
+ if (segments[0] === binDir)
5548
+ return currentPath ?? binDir;
5549
+ return [binDir, ...segments.filter((s) => s !== binDir)].join(":");
5550
+ }
5551
+ function registerPlugin(opts) {
5552
+ const { pluginsRoot, paths, version } = opts;
5553
+ const nodePath = opts.nodePath ?? process.execPath;
5554
+ const pluginDir = join13(pluginsRoot, PLUGIN_NAME);
5555
+ if (!assetsReady(paths))
5556
+ throw new Error(`plugin assets not found at ${paths.assetsDir}; run \`pnpm run build:dist\` (dev) or reinstall the package`);
5557
+ const skillsSrc = join13(paths.assetsDir, "skills");
5558
+ const skills = listSkillNames(paths.assetsDir);
5559
+ const manifestDir = join13(pluginDir, MANIFEST_DIR_NAME);
5560
+ mkdirSync8(manifestDir, { recursive: true });
5561
+ const manifest = JSON.parse(readFileSync6(join13(paths.assetsDir, "plugin.json"), "utf8"));
5562
+ writeFileSync6(join13(manifestDir, "plugin.json"), `${JSON.stringify({ ...manifest, version }, null, 2)}
5563
+ `);
5564
+ const skillsDest = join13(pluginDir, "skills");
5565
+ rmSync8(skillsDest, { recursive: true, force: true });
5566
+ mkdirSync8(skillsDest, { recursive: true });
5567
+ cpSync(skillsSrc, skillsDest, { recursive: true });
5568
+ const binDir = join13(pluginDir, "bin");
5569
+ mkdirSync8(binDir, { recursive: true });
5570
+ const binPath = join13(binDir, "morula");
5571
+ writeFileSync6(binPath, buildLauncherScript({ nodePath, packageRoot: paths.packageRoot }));
5572
+ chmodSync3(binPath, 493);
5573
+ const legacy = legacyAgentsPluginDir(opts.home);
5574
+ return {
5575
+ pluginDir,
5576
+ binPath,
5577
+ skills,
5578
+ version,
5579
+ node: nodePath,
5580
+ legacyDir: existsSync9(join13(legacy, MANIFEST_DIR_NAME)) ? legacy : null
5581
+ };
5582
+ }
5583
+
4757
5584
  // src/daemon/acp/acp-event-mapper.ts
4758
5585
  var AcpEventMapper = class {
4759
5586
  lastUsage = void 0;
4760
5587
  reset() {
4761
5588
  this.lastUsage = void 0;
4762
5589
  }
4763
- /** 取当前 usage(run:ended 附着用) */
5590
+ /** 取当前 usage(回放/观测用;**不再**作为 tokenUsage 附着——token 总量以 prompt 响应的 usage 为准) */
4764
5591
  get latestUsage() {
4765
5592
  return this.lastUsage;
4766
5593
  }
@@ -4786,6 +5613,13 @@ var AcpEventMapper = class {
4786
5613
  }
4787
5614
  break;
4788
5615
  }
5616
+ case "user_message_chunk": {
5617
+ const text = update.content?.type === "text" ? update.content.text : "";
5618
+ if (text) {
5619
+ messages.push({ eventType: "user_message:delta", payload: { delta: text } });
5620
+ }
5621
+ break;
5622
+ }
4789
5623
  case "tool_call": {
4790
5624
  messages.push({
4791
5625
  eventType: "tool:started",
@@ -4813,11 +5647,13 @@ var AcpEventMapper = class {
4813
5647
  break;
4814
5648
  }
4815
5649
  case "usage_update": {
4816
- this.lastUsage = {
4817
- inputTokens: void 0,
4818
- outputTokens: void 0,
4819
- totalTokens: typeof update.used === "number" ? update.used : void 0
5650
+ const usage2 = {
5651
+ contextUsed: typeof update.used === "number" ? update.used : 0,
5652
+ contextSize: typeof update.size === "number" ? update.size : 0,
5653
+ cost: update.cost ?? null
4820
5654
  };
5655
+ this.lastUsage = usage2;
5656
+ messages.push({ eventType: "usage:context", payload: { usage: usage2 } });
4821
5657
  break;
4822
5658
  }
4823
5659
  default:
@@ -4913,14 +5749,9 @@ function normalizeOptionId(decision) {
4913
5749
  }
4914
5750
 
4915
5751
  // src/daemon/acp/dim-acp-executor.ts
4916
- var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
4917
- var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4918
- var DEFAULT_TOOL_IDLE_TIMEOUT_MS = 20 * 60 * 1e3;
4919
- var DEFAULT_MAX_TURNS = 8;
4920
- var DEFAULT_TASK_WALL_CLOCK_MS = 2 * 60 * 60 * 1e3;
4921
5752
  var DEFAULT_STEER_SAFETY_MS = 30 * 1e3;
4922
5753
  var PROMPT_RPC_TIMEOUT_GRACE_MS = 1e4;
4923
- var WATCHDOG_TICK_MS = 15e3;
5754
+ var STEER_POLL_TICK_MS = 15e3;
4924
5755
  var KILL_GRACE_MS = 3e3;
4925
5756
  function resolveSteerSafetyMs(raw = process.env.MORULA_STEER_SAFETY_MS) {
4926
5757
  if (raw === void 0 || raw === "")
@@ -5063,7 +5894,7 @@ var DimAcpExecutor = class {
5063
5894
  /** 每次 run 前由调用方注入(daemon 单例池) */
5064
5895
  pool = null;
5065
5896
  async run(opts) {
5066
- const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5897
+ const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
5067
5898
  const mapper = new AcpEventMapper();
5068
5899
  const pool = opts.pool ?? this.pool;
5069
5900
  return await new Promise((resolve2) => {
@@ -5076,9 +5907,7 @@ var DimAcpExecutor = class {
5076
5907
  let turnNo = 1;
5077
5908
  const inFlightToolCalls = /* @__PURE__ */ new Map();
5078
5909
  const inFlightTools = () => inFlightToolCalls.size;
5079
- let lastActivityAt = Date.now();
5080
- let watchdogTimer = null;
5081
- let watchdogFired = false;
5910
+ let steerPollTimer = null;
5082
5911
  let resumeFailed;
5083
5912
  const killTree = (c) => {
5084
5913
  if (!c)
@@ -5089,10 +5918,10 @@ var DimAcpExecutor = class {
5089
5918
  c.kill();
5090
5919
  }
5091
5920
  };
5092
- const stopWatchdog = () => {
5093
- if (watchdogTimer) {
5094
- clearInterval(watchdogTimer);
5095
- watchdogTimer = null;
5921
+ const stopSteerPoll = () => {
5922
+ if (steerPollTimer) {
5923
+ clearInterval(steerPollTimer);
5924
+ steerPollTimer = null;
5096
5925
  }
5097
5926
  };
5098
5927
  let abortHandler = () => {
@@ -5103,7 +5932,7 @@ var DimAcpExecutor = class {
5103
5932
  settled = true;
5104
5933
  if (timer)
5105
5934
  clearTimeout(timer);
5106
- stopWatchdog();
5935
+ stopSteerPoll();
5107
5936
  if (ownsConnection && conn) {
5108
5937
  void conn.close().catch(() => {
5109
5938
  });
@@ -5128,45 +5957,17 @@ var DimAcpExecutor = class {
5128
5957
  finish({ ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88", sessionId: sessionId || void 0 });
5129
5958
  }
5130
5959
  };
5131
- const startWatchdog = () => {
5132
- const idle = Number(process.env.MORULA_DIM_IDLE_TIMEOUT_MS) || DEFAULT_IDLE_TIMEOUT_MS;
5133
- if (idle <= 0)
5134
- return;
5135
- lastActivityAt = Date.now();
5136
- inFlightToolCalls.clear();
5137
- watchdogTimer = setInterval(() => {
5960
+ const startSteerPoll = () => {
5961
+ steerPollTimer = setInterval(() => {
5138
5962
  maybeInterruptAtSafePoint();
5139
- enforceTaskWallClock();
5140
- const toolBudget = Number(process.env.MORULA_DIM_TOOL_IDLE_TIMEOUT_MS) || DEFAULT_TOOL_IDLE_TIMEOUT_MS;
5141
- const budget = inFlightTools() > 0 ? toolBudget : idle;
5142
- if (Date.now() - lastActivityAt > budget) {
5143
- watchdogFired = true;
5144
- if (sessionId && conn)
5145
- conn.cancel(sessionId);
5146
- killTree(conn);
5147
- finish({
5148
- ok: false,
5149
- error: `daemon: idle watchdog fired\uFF08${Math.round(budget / 1e3)}s \u65E0\u6D3B\u52A8\uFF09`,
5150
- sessionId: sessionId || void 0,
5151
- turns: turnNo
5152
- });
5153
- }
5154
- }, WATCHDOG_TICK_MS);
5155
- watchdogTimer.unref?.();
5156
- };
5157
- const markActivity = () => {
5158
- lastActivityAt = Date.now();
5963
+ }, STEER_POLL_TICK_MS);
5964
+ steerPollTimer.unref?.();
5159
5965
  };
5160
5966
  let activeSteer = null;
5161
5967
  let steerWaitingSince = 0;
5162
- let wallClockFired = false;
5163
5968
  let interruptedTurns = 0;
5164
5969
  let turnKind = "initial";
5165
5970
  const steerSafetyMs = resolveSteerSafetyMs();
5166
- const taskStartedAt = Date.now();
5167
- const maxTurns = Math.max(1, Math.trunc(opts.steer?.maxTurns ?? DEFAULT_MAX_TURNS));
5168
- const taskWallClockMs = opts.steer?.wallClockMs ?? 0;
5169
- const wallClockExceeded = () => taskWallClockMs > 0 && Date.now() - taskStartedAt > taskWallClockMs;
5170
5971
  function maybeInterruptAtSafePoint() {
5171
5972
  if (!opts.steer || activeSteer || !conn || !sessionId)
5172
5973
  return;
@@ -5190,15 +5991,6 @@ var DimAcpExecutor = class {
5190
5991
  `);
5191
5992
  conn.cancel(sessionId);
5192
5993
  }
5193
- function enforceTaskWallClock() {
5194
- if (wallClockFired || !wallClockExceeded())
5195
- return;
5196
- wallClockFired = true;
5197
- process.stderr.write(`[daemon] \u4EFB\u52A1\u5899\u949F\u8D85\u9650\uFF08${Math.round(taskWallClockMs / 1e3)}s\uFF09\uFF0C\u7EC8\u6B62\u672C\u8F6E\u5E76\u8981\u6C42\u4EBA\u4ECB\u5165
5198
- `);
5199
- if (sessionId && conn)
5200
- conn.cancel(sessionId);
5201
- }
5202
5994
  function emitAbortedTools() {
5203
5995
  const aborted = [];
5204
5996
  for (const [callId, toolName] of inFlightToolCalls) {
@@ -5237,8 +6029,12 @@ var DimAcpExecutor = class {
5237
6029
  });
5238
6030
  setup().then(async () => {
5239
6031
  const armTurnTimer = () => {
5240
- if (timer)
6032
+ if (timer) {
5241
6033
  clearTimeout(timer);
6034
+ timer = void 0;
6035
+ }
6036
+ if (timeout <= 0)
6037
+ return;
5242
6038
  timer = setTimeout(() => {
5243
6039
  if (sessionId && conn)
5244
6040
  conn.cancel(sessionId);
@@ -5254,7 +6050,7 @@ var DimAcpExecutor = class {
5254
6050
  };
5255
6051
  armTurnTimer();
5256
6052
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
5257
- startWatchdog();
6053
+ startSteerPoll();
5258
6054
  const bridge = new AcpPermissionBridge();
5259
6055
  conn.setPermissionHandler(bridge.createHandler(opts.approval, opts.approvalMode === "manual"));
5260
6056
  const sessionParams = { cwd: opts.cwd, mcpServers: [] };
@@ -5304,7 +6100,7 @@ var DimAcpExecutor = class {
5304
6100
  process.stderr.write(`[daemon] \u4EFB\u52A1\u6307\u5B9A\u6A21\u578B ${opts.model} \u65E0\u6CD5\u5339\u914D ACP \u53EF\u7528\u6A21\u578B\uFF08\u5C06\u4F7F\u7528 dim \u9ED8\u8BA4\u6A21\u578B\uFF09
5305
6101
  `);
5306
6102
  } else {
5307
- await conn.setModel(sess.sessionId, modelId).catch((err) => {
6103
+ await conn.setConfigOption(sess.sessionId, "model", modelId).catch((err) => {
5308
6104
  process.stderr.write(`[daemon] \u4EFB\u52A1\u6307\u5B9A\u6A21\u578B ${modelId} \u4E0B\u53D1\u5931\u8D25\uFF08\u5C06\u4F7F\u7528 dim \u9ED8\u8BA4\u6A21\u578B\uFF09: ${err.message}
5309
6105
  `);
5310
6106
  });
@@ -5314,7 +6110,6 @@ var DimAcpExecutor = class {
5314
6110
  });
5315
6111
  sessionId = sess.sessionId;
5316
6112
  pool?.touch();
5317
- markActivity();
5318
6113
  opts.onEvent?.(mapper.readyMessage(sessionId), "");
5319
6114
  const reopenSessionAfterInterrupt = async () => {
5320
6115
  try {
@@ -5345,7 +6140,6 @@ var DimAcpExecutor = class {
5345
6140
  activeSteer = null;
5346
6141
  opts.steer?.onTurnStarted?.({ turnNo, kind: turnKind });
5347
6142
  const result = await conn.prompt(sessionId, promptText, (update) => {
5348
- markActivity();
5349
6143
  pool?.touch();
5350
6144
  const { messages } = mapper.map(sessionId, update);
5351
6145
  for (const msg of messages) {
@@ -5378,37 +6172,18 @@ var DimAcpExecutor = class {
5378
6172
  }
5379
6173
  }
5380
6174
  maybeInterruptAtSafePoint();
5381
- }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
6175
+ }, timeout > 0 ? timeout + PROMPT_RPC_TIMEOUT_GRACE_MS : 0);
5382
6176
  if (settled)
5383
6177
  return;
5384
6178
  pool?.touch();
5385
- markActivity();
5386
- const usage2 = result.usage ?? mapper.latestUsage;
6179
+ const usage2 = result.usage ?? void 0;
5387
6180
  const cancelled = result.stopReason === "cancelled";
5388
6181
  const base = { sessionId, turns: turnNo, interrupted: interruptedTurns > 0 };
5389
- if (wallClockFired || wallClockExceeded()) {
5390
- const abortedTools = emitAbortedTools();
5391
- opts.steer?.onTurnInterrupted?.({ turnNo, reason: "wall-clock", abortedTools });
5392
- killTree(conn);
5393
- finish({
5394
- ...base,
5395
- ok: false,
5396
- error: `\u5DF2\u8FBE\u4EFB\u52A1\u5899\u949F\u4E0A\u9650\uFF08${Math.round(taskWallClockMs / 1e3)}s\uFF09\uFF0C\u5DF2\u7EC8\u6B62\u672C\u8F6E\uFF0C\u8BF7\u4EBA\u4ECB\u5165`
5397
- });
5398
- return;
5399
- }
5400
6182
  if (cancelled && activeSteer) {
5401
6183
  interruptedTurns += 1;
5402
6184
  const abortedTools = emitAbortedTools();
5403
6185
  const nextTurn = turnNo + 1;
5404
- const withinBudget = nextTurn <= maxTurns;
5405
- opts.steer?.onTurnInterrupted?.({ turnNo, reason: withinBudget ? "steer" : "max-turns", abortedTools });
5406
- if (!withinBudget) {
5407
- process.stderr.write(`[daemon] \u5DF2\u8FBE\u6700\u5927\u56DE\u5408\u6570 ${maxTurns}\uFF0C\u5269\u4F59\u6253\u65AD\u8BF7\u6C42\u4EA4\u7531\u4E0B\u4E00\u8F6E
5408
- `);
5409
- finish({ ...base, ok: true, text: finalText, tokenUsage: usage2 });
5410
- return;
5411
- }
6186
+ opts.steer?.onTurnInterrupted?.({ turnNo, reason: "steer", abortedTools });
5412
6187
  turnKind = "after-interrupt";
5413
6188
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
5414
6189
  turnNo = nextTurn;
@@ -5431,7 +6206,7 @@ var DimAcpExecutor = class {
5431
6206
  return;
5432
6207
  }
5433
6208
  const leftover = activeSteer ?? opts.steer?.peek() ?? null;
5434
- if (leftover && turnNo + 1 <= maxTurns && !wallClockExceeded()) {
6209
+ if (leftover) {
5435
6210
  const nextTurn = turnNo + 1;
5436
6211
  turnKind = "appended";
5437
6212
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
@@ -5532,7 +6307,15 @@ function createDimEventNormalizer(opts) {
5532
6307
  let currentMessageId = null;
5533
6308
  let currentText = "";
5534
6309
  let hasBoundarySignal = false;
6310
+ let pendingUserText = "";
6311
+ const flushUserMessage = () => {
6312
+ if (pendingUserText.trim().length > 0) {
6313
+ opts.onEvent({ type: "user_message", text: pendingUserText });
6314
+ }
6315
+ pendingUserText = "";
6316
+ };
5535
6317
  const startMessage = (messageId) => {
6318
+ flushUserMessage();
5536
6319
  if (currentMessageId !== null && currentText.trim().length > 0) {
5537
6320
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5538
6321
  }
@@ -5545,7 +6328,22 @@ function createDimEventNormalizer(opts) {
5545
6328
  const eventType = msg.eventType;
5546
6329
  if (!eventType)
5547
6330
  return;
6331
+ if (eventType !== "user_message:delta")
6332
+ flushUserMessage();
5548
6333
  switch (eventType) {
6334
+ case "user_message:delta": {
6335
+ const delta = msg.payload?.delta ?? "";
6336
+ if (delta)
6337
+ pendingUserText += delta;
6338
+ break;
6339
+ }
6340
+ case "usage:context": {
6341
+ const usage2 = msg.payload?.usage;
6342
+ if (usage2 && typeof usage2 === "object") {
6343
+ opts.onEvent({ type: "usage", usage: normalizeUsage(usage2) });
6344
+ }
6345
+ break;
6346
+ }
5549
6347
  case "message:started": {
5550
6348
  const messageId = extractMessageId(msg.payload) ?? `msg-${msg.payload?.messageId ?? "run"}`;
5551
6349
  startMessage(messageId);
@@ -5560,9 +6358,12 @@ function createDimEventNormalizer(opts) {
5560
6358
  currentText = "";
5561
6359
  break;
5562
6360
  }
5563
- case "thinking:delta":
6361
+ case "thinking:delta": {
6362
+ const delta = msg.payload?.delta ?? "";
6363
+ if (delta)
6364
+ opts.onEvent({ type: "assistant_thinking_delta", text: delta });
5564
6365
  break;
5565
- // 思考过程不进最终回复
6366
+ }
5566
6367
  case "text:delta": {
5567
6368
  const delta = msg.payload?.delta ?? "";
5568
6369
  if (!delta)
@@ -5636,6 +6437,7 @@ function createDimEventNormalizer(opts) {
5636
6437
  break;
5637
6438
  }
5638
6439
  case "run:ended": {
6440
+ flushUserMessage();
5639
6441
  if (currentMessageId !== null && currentText.trim().length > 0) {
5640
6442
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5641
6443
  } else if (!hasBoundarySignal && finalText.trim()) {
@@ -5673,7 +6475,11 @@ function normalizeUsage(raw) {
5673
6475
  return {
5674
6476
  inputTokens: typeof u.inputTokens === "number" ? u.inputTokens : void 0,
5675
6477
  outputTokens: typeof u.outputTokens === "number" ? u.outputTokens : void 0,
5676
- totalTokens: typeof u.totalTokens === "number" ? u.totalTokens : void 0
6478
+ totalTokens: typeof u.totalTokens === "number" ? u.totalTokens : void 0,
6479
+ // ACP usage_update(上下文占用语义,P0-8):透传给前端状态行
6480
+ contextUsed: typeof u.contextUsed === "number" ? u.contextUsed : void 0,
6481
+ contextSize: typeof u.contextSize === "number" ? u.contextSize : void 0,
6482
+ cost: u.cost && typeof u.cost === "object" && typeof u.cost.amount === "number" ? u.cost : null
5677
6483
  };
5678
6484
  }
5679
6485
  var DIM_CAPABILITIES = {
@@ -5728,8 +6534,8 @@ var DimAgentAdapter = class {
5728
6534
  }
5729
6535
  }
5730
6536
  async probe() {
5731
- const { existsSync: existsSync19 } = await import("node:fs");
5732
- const installed = existsSync19(this.dimPath) || this.dimPath === "dim";
6537
+ const { existsSync: existsSync18 } = await import("node:fs");
6538
+ const installed = existsSync18(this.dimPath) || this.dimPath === "dim";
5733
6539
  let authStatus = installed ? "unknown" : "error";
5734
6540
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
5735
6541
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -6021,19 +6827,26 @@ function pickLegacyDimModels(inventory) {
6021
6827
 
6022
6828
  // src/daemon/repo-cache.ts
6023
6829
  import { execFileSync as execFileSync7 } from "node:child_process";
6024
- import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
6025
- import { dirname as dirname6, join as join14 } from "node:path";
6830
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "node:fs";
6831
+ import { dirname as dirname7, join as join15 } from "node:path";
6026
6832
  function ownerOf(repoFullName) {
6027
6833
  const idx = repoFullName.indexOf("/");
6028
6834
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
6029
6835
  }
6030
- function buildRepoCachePath(workdir, tenantId, repoFullName) {
6031
- return join14(workdir, "repos", tenantId, ownerOf(repoFullName), repoFullName);
6836
+ function runtimePathSegment(runtimeId) {
6837
+ return runtimeId.slice(0, 8);
6838
+ }
6839
+ function buildRepoCachePath(workdir, tenantId, runtimeId, repoFullName) {
6840
+ const runtimeSegment = runtimePathSegment(runtimeId);
6841
+ if (!runtimeSegment) {
6842
+ throw new Error("REPO_CACHE_RUNTIME_ID_MISSING: \u7F3A\u5C11 runtimeId\uFF0C\u65E0\u6CD5\u6784\u9020\u6309\u6267\u884C\u8005\u9694\u79BB\u7684 repo cache \u8DEF\u5F84");
6843
+ }
6844
+ return join15(workdir, "repos", tenantId, runtimeSegment, ownerOf(repoFullName), repoFullName);
6032
6845
  }
6033
- function openRepoCache(workdir, tenantId, repoFullName, env, agentRemoteUrl, daemonRemoteUrl) {
6034
- const repoCachePath = buildRepoCachePath(workdir, tenantId, repoFullName);
6846
+ function openRepoCache(workdir, tenantId, runtimeId, repoFullName, env, agentRemoteUrl, daemonRemoteUrl) {
6847
+ const repoCachePath = buildRepoCachePath(workdir, tenantId, runtimeId, repoFullName);
6035
6848
  const currentOrigin = () => {
6036
- if (!existsSync11(join14(repoCachePath, ".git")))
6849
+ if (!existsSync11(join15(repoCachePath, ".git")))
6037
6850
  return null;
6038
6851
  try {
6039
6852
  return execFileSync7("git", ["-C", repoCachePath, "remote", "get-url", "origin"], { encoding: "utf8", env, stdio: "pipe" }).trim() || null;
@@ -6046,11 +6859,11 @@ function openRepoCache(workdir, tenantId, repoFullName, env, agentRemoteUrl, dae
6046
6859
  return {
6047
6860
  repoCachePath,
6048
6861
  ensure() {
6049
- if (existsSync11(join14(repoCachePath, ".git"))) {
6862
+ if (existsSync11(join15(repoCachePath, ".git"))) {
6050
6863
  syncRemoteUrl(repoCachePath, originUrl(), env);
6051
6864
  return;
6052
6865
  }
6053
- mkdirSync8(dirname6(repoCachePath), { recursive: true });
6866
+ mkdirSync9(dirname7(repoCachePath), { recursive: true });
6054
6867
  const cloneUrl = fetchUrl();
6055
6868
  try {
6056
6869
  execFileSync7("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
@@ -6086,24 +6899,24 @@ function gitErrorText2(err) {
6086
6899
  // src/daemon/repo-checkout-service.ts
6087
6900
  import { createServer as createServer3 } from "node:http";
6088
6901
  import { execFileSync as execFileSync9 } from "node:child_process";
6089
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync8 } from "node:fs";
6090
- import { basename as basename2, join as join17 } from "node:path";
6902
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readdirSync as readdirSync5, writeFileSync as writeFileSync9 } from "node:fs";
6903
+ import { basename as basename2, join as join18 } from "node:path";
6091
6904
 
6092
6905
  // src/daemon/worktree-manager.ts
6093
6906
  import { execFileSync as execFileSync8 } from "node:child_process";
6094
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync7, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
6095
- import { basename, join as join16, sep as sep2 } from "node:path";
6907
+ import { existsSync as existsSync13, mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync9, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
6908
+ import { basename, join as join17, sep as sep2 } from "node:path";
6096
6909
 
6097
6910
  // src/daemon/workspace-marker.ts
6098
- import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
6099
- import { join as join15 } from "node:path";
6911
+ import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
6912
+ import { join as join16 } from "node:path";
6100
6913
  var WORKTREE_MARKER_FILE = ".morula-worktree.json";
6101
6914
  var WORKTREE_MARKER_SCHEMA = 1;
6102
6915
  function markerPath(worktreePath) {
6103
- return join15(worktreePath, WORKTREE_MARKER_FILE);
6916
+ return join16(worktreePath, WORKTREE_MARKER_FILE);
6104
6917
  }
6105
6918
  function writeWorktreeMarker(worktreePath, marker) {
6106
- writeFileSync6(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6919
+ writeFileSync7(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6107
6920
  `, "utf8");
6108
6921
  }
6109
6922
  function readWorktreeMarker(worktreePath) {
@@ -6130,19 +6943,19 @@ function safeDirName(name) {
6130
6943
  return safe || "task";
6131
6944
  }
6132
6945
  function buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code) {
6133
- const runtimeSegment = runtimeId.slice(0, 8);
6946
+ const runtimeSegment = runtimePathSegment(runtimeId);
6134
6947
  if (!runtimeSegment) {
6135
6948
  throw new Error("WORKTREE_RUNTIME_ID_MISSING: \u7F3A\u5C11 runtimeId\uFF0C\u65E0\u6CD5\u6784\u9020\u6309\u6267\u884C\u8005\u9694\u79BB\u7684 worktree \u8DEF\u5F84");
6136
6949
  }
6137
6950
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6138
- return join16(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6951
+ return join17(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6139
6952
  }
6140
6953
  function buildWorktreePath(workdir, tenantId, runtimeId, workContextId, code, repoFullName) {
6141
- return join16(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6954
+ return join17(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6142
6955
  }
6143
6956
  function buildLegacyWorktreePath(workdir, tenantId, workContextId, code) {
6144
6957
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6145
- return join16(workdir, "worktrees", tenantId, dirName);
6958
+ return join17(workdir, "worktrees", tenantId, dirName);
6146
6959
  }
6147
6960
  var legacyPathWarned = /* @__PURE__ */ new Set();
6148
6961
  function warnLegacyWorktreeResidue(legacyPath) {
@@ -6153,13 +6966,13 @@ function warnLegacyWorktreeResidue(legacyPath) {
6153
6966
  }
6154
6967
  var legacyLayoutWarned = /* @__PURE__ */ new Set();
6155
6968
  function warnLegacyLayoutWorktreeResidue(taskRoot, worktreePath) {
6156
- if (legacyLayoutWarned.has(taskRoot) || existsSync13(worktreePath) || !existsSync13(join16(taskRoot, WORKTREE_MARKER_FILE)))
6969
+ if (legacyLayoutWarned.has(taskRoot) || existsSync13(worktreePath) || !existsSync13(join17(taskRoot, WORKTREE_MARKER_FILE)))
6157
6970
  return;
6158
6971
  legacyLayoutWarned.add(taskRoot);
6159
6972
  console.warn(`[daemon] \u68C0\u6D4B\u5230\u65E7\u5E03\u5C40\u4EFB\u52A1\u5DE5\u4F5C\u533A\uFF08\u4ED3\u5E93\u76F4\u63A5\u4F4D\u4E8E\u5DE5\u4F5C\u533A\u6839\uFF09: ${taskRoot} \u2014\u2014 \u65B0\u5E03\u5C40\u4F1A\u5728\u5176\u4E0B\u521B\u5EFA\u4ED3\u5E93\u5B50\u76EE\u5F55\uFF0C\u65E7\u76EE\u5F55\u5185\u5BB9\u4E0D\u518D\u88AB\u4F7F\u7528\uFF1B\u5176\u4E2D\u53EF\u80FD\u6709\u672A\u63A8\u9001\u6539\u52A8\uFF0C\u786E\u8BA4\u65E0\u9700\u4FDD\u7559\u540E\u53EF\u624B\u52A8\u5220\u9664\uFF08\u4E0D\u81EA\u52A8\u8FC1\u79FB/\u6E05\u7406\uFF09`);
6160
6973
  }
6161
6974
  function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6162
- const gitignorePath = join16(worktreePath, ".gitignore");
6975
+ const gitignorePath = join17(worktreePath, ".gitignore");
6163
6976
  const existing = existsSync13(gitignorePath) ? readFileSync9(gitignorePath, "utf8") : "";
6164
6977
  const requiredLines = [WORKTREE_MARKER_FILE];
6165
6978
  const missing = requiredLines.filter((l) => !existing.split("\n").some((line) => line.trim() === l));
@@ -6167,7 +6980,7 @@ function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6167
6980
  const next = `${existing.trimEnd() ? `${existing.trimEnd()}
6168
6981
  ` : ""}${missing.join("\n")}
6169
6982
  `;
6170
- writeFileSync7(gitignorePath, next, "utf8");
6983
+ writeFileSync8(gitignorePath, next, "utf8");
6171
6984
  try {
6172
6985
  execFileSync8("git", ["-C", worktreePath, "add", ".gitignore"], { env, stdio: "pipe" });
6173
6986
  execFileSync8(
@@ -6257,7 +7070,7 @@ function isIndexLockConflict(err) {
6257
7070
  return /index\.lock/i.test(text) || /另一个 git 进程|another git process/i.test(text);
6258
7071
  }
6259
7072
  function worktreeIndexLockPath(repoCachePath, worktreePath) {
6260
- return join16(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
7073
+ return join17(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
6261
7074
  }
6262
7075
  function indexLockPathFromError(text) {
6263
7076
  const m = text.match(/[^\s'‘’"]*index\.lock/);
@@ -6311,7 +7124,7 @@ var foreignResidueHits = /* @__PURE__ */ new Map();
6311
7124
  function inspectForeignDirectory(dir) {
6312
7125
  let entries = [];
6313
7126
  try {
6314
- entries = readdirSync3(dir, { withFileTypes: true });
7127
+ entries = readdirSync4(dir, { withFileTypes: true });
6315
7128
  } catch {
6316
7129
  }
6317
7130
  const topNames = entries.slice(0, 20).map((e) => `${e.name}${e.isDirectory() ? "/" : ""}`);
@@ -6325,7 +7138,7 @@ function inspectForeignDirectory(dir) {
6325
7138
  return;
6326
7139
  let children = [];
6327
7140
  try {
6328
- children = readdirSync3(p, { withFileTypes: true });
7141
+ children = readdirSync4(p, { withFileTypes: true });
6329
7142
  } catch {
6330
7143
  return;
6331
7144
  }
@@ -6333,9 +7146,9 @@ function inspectForeignDirectory(dir) {
6333
7146
  if (count >= FOREIGN_RESIDUE_SCAN_LIMIT)
6334
7147
  return;
6335
7148
  count++;
6336
- const full = join16(p, e.name);
7149
+ const full = join17(p, e.name);
6337
7150
  try {
6338
- const st = statSync2(full);
7151
+ const st = statSync3(full);
6339
7152
  bytes += st.size;
6340
7153
  minMs = Math.min(minMs, st.mtimeMs);
6341
7154
  maxMs = Math.max(maxMs, st.mtimeMs);
@@ -6365,7 +7178,7 @@ function reportForeignResidue(worktreePath, forensics, cleaned) {
6365
7178
  console.warn(`[daemon] ${detail}`);
6366
7179
  }
6367
7180
  function prepareWorktree(input) {
6368
- const cache = openRepoCache(input.workdir, input.tenantId, input.repoFullName, input.env, input.remoteUrl, input.daemonRemoteUrl);
7181
+ const cache = openRepoCache(input.workdir, input.tenantId, input.runtimeId, input.repoFullName, input.env, input.remoteUrl, input.daemonRemoteUrl);
6369
7182
  cache.ensure();
6370
7183
  cache.fetch();
6371
7184
  const requestedBase = input.baseBranch?.trim();
@@ -6421,7 +7234,7 @@ function prepareWorktree(input) {
6421
7234
  if (!isGitRepo) {
6422
7235
  const forensics = inspectForeignDirectory(worktreePath);
6423
7236
  try {
6424
- rmSync7(worktreePath, { recursive: true, force: true });
7237
+ rmSync9(worktreePath, { recursive: true, force: true });
6425
7238
  } catch {
6426
7239
  reportForeignResidue(worktreePath, forensics, false);
6427
7240
  throw new Error(`WORKTREE_RESIDUE: \u76EE\u5F55 ${worktreePath} \u6B8B\u7559\u4E14\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u4EBA\u5DE5\u5904\u7406`);
@@ -6477,7 +7290,7 @@ function prepareWorktree(input) {
6477
7290
  }
6478
7291
  }
6479
7292
  if (!existsSync13(worktreePath)) {
6480
- mkdirSync9(join16(worktreePath, ".."), { recursive: true });
7293
+ mkdirSync10(join17(worktreePath, ".."), { recursive: true });
6481
7294
  addWorktreeWithRetry(cache.repoCachePath, worktreePath, `origin/${input.baseBranch}`);
6482
7295
  writeWorktreeMarker(worktreePath, {
6483
7296
  schemaVersion: 1,
@@ -6494,7 +7307,7 @@ function prepareWorktree(input) {
6494
7307
  {
6495
7308
  const dotMorulaRescue = `${worktreePath}.morula-rescue`;
6496
7309
  if (existsSync13(dotMorulaRescue)) {
6497
- const dotMorulaDir = join16(worktreePath, ".morula");
7310
+ const dotMorulaDir = join17(worktreePath, ".morula");
6498
7311
  if (!existsSync13(dotMorulaDir)) {
6499
7312
  try {
6500
7313
  renameSync2(dotMorulaRescue, dotMorulaDir);
@@ -6503,7 +7316,7 @@ function prepareWorktree(input) {
6503
7316
  console.warn(`[daemon] worktree \u6F02\u79FB\u81EA\u52A8\u6062\u590D: .morula/ \u5F52\u8FD8\u5931\u8D25\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${dotMorulaRescue}`);
6504
7317
  }
6505
7318
  } else {
6506
- rmSync7(dotMorulaRescue, { recursive: true, force: true });
7319
+ rmSync9(dotMorulaRescue, { recursive: true, force: true });
6507
7320
  }
6508
7321
  }
6509
7322
  }
@@ -6523,10 +7336,10 @@ function prepareWorktree(input) {
6523
7336
  };
6524
7337
  }
6525
7338
  function resolveWorkspaceLayout(root, expected) {
6526
- const primaryDir = join16(root, safeDirName(expected.repoFullName));
6527
- if (existsSync13(join16(primaryDir, WORKTREE_MARKER_FILE)))
7339
+ const primaryDir = join17(root, safeDirName(expected.repoFullName));
7340
+ if (existsSync13(join17(primaryDir, WORKTREE_MARKER_FILE)))
6528
7341
  return { primaryDir, legacy: false };
6529
- if (existsSync13(join16(root, WORKTREE_MARKER_FILE)))
7342
+ if (existsSync13(join17(root, WORKTREE_MARKER_FILE)))
6530
7343
  return { primaryDir: root, legacy: true };
6531
7344
  return null;
6532
7345
  }
@@ -6550,7 +7363,7 @@ function validateRemovable(taskRoot, expected, repoCachePath) {
6550
7363
  ).join("\n");
6551
7364
  if (statusRelevant.length > 0)
6552
7365
  return "WORKTREE_DIRTY";
6553
- const additionalActivity = legacy ? findActivityInReposDir(join16(primaryDir, ".morula", "repos")) : findAdditionalRepoActivity(taskRoot, { excludeDirs: [primaryDir] });
7366
+ const additionalActivity = legacy ? findActivityInReposDir(join17(primaryDir, ".morula", "repos")) : findAdditionalRepoActivity(taskRoot, { excludeDirs: [primaryDir] });
6554
7367
  if (additionalActivity)
6555
7368
  return "WORKTREE_DIRTY";
6556
7369
  try {
@@ -6593,13 +7406,13 @@ function findActivityInReposDir(reposDir) {
6593
7406
  return null;
6594
7407
  let names = [];
6595
7408
  try {
6596
- names = readdirSync3(reposDir);
7409
+ names = readdirSync4(reposDir);
6597
7410
  } catch {
6598
7411
  return null;
6599
7412
  }
6600
7413
  for (const name of names) {
6601
- const repoPath = join16(reposDir, name);
6602
- if (!existsSync13(join16(repoPath, ".git")))
7414
+ const repoPath = join17(reposDir, name);
7415
+ if (!existsSync13(join17(repoPath, ".git")))
6603
7416
  continue;
6604
7417
  if (repoHasActivity(repoPath))
6605
7418
  return repoPath;
@@ -6610,20 +7423,20 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6610
7423
  const excluded = new Set(options.excludeDirs ?? []);
6611
7424
  let entries = [];
6612
7425
  try {
6613
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7426
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6614
7427
  } catch {
6615
7428
  return null;
6616
7429
  }
6617
- const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join16(taskRoot, e.name)).filter((dir) => !excluded.has(dir));
6618
- const legacyReposDir = join16(taskRoot, ".morula", "repos");
7430
+ const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join17(taskRoot, e.name)).filter((dir) => !excluded.has(dir));
7431
+ const legacyReposDir = join17(taskRoot, ".morula", "repos");
6619
7432
  if (existsSync13(legacyReposDir)) {
6620
7433
  try {
6621
- candidates.push(...readdirSync3(legacyReposDir).map((n) => join16(legacyReposDir, n)));
7434
+ candidates.push(...readdirSync4(legacyReposDir).map((n) => join17(legacyReposDir, n)));
6622
7435
  } catch {
6623
7436
  }
6624
7437
  }
6625
7438
  for (const repoPath of candidates) {
6626
- if (!existsSync13(join16(repoPath, ".git")))
7439
+ if (!existsSync13(join17(repoPath, ".git")))
6627
7440
  continue;
6628
7441
  if (repoHasActivity(repoPath))
6629
7442
  return repoPath;
@@ -6631,18 +7444,18 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6631
7444
  return null;
6632
7445
  }
6633
7446
  function cleanupInjectedSkills(dir) {
6634
- const injectedSkillsRoot = join16(dir, ".agents", "skills");
7447
+ const injectedSkillsRoot = join17(dir, ".agents", "skills");
6635
7448
  if (!existsSync13(injectedSkillsRoot))
6636
7449
  return;
6637
7450
  try {
6638
- for (const name of readdirSync3(injectedSkillsRoot)) {
7451
+ for (const name of readdirSync4(injectedSkillsRoot)) {
6639
7452
  if (name === COLLABORATION_SKILL_NAME || /^[\w-]{1,64}$/.test(name)) {
6640
- rmSync7(join16(injectedSkillsRoot, name), { recursive: true, force: true });
7453
+ rmSync9(join17(injectedSkillsRoot, name), { recursive: true, force: true });
6641
7454
  }
6642
7455
  }
6643
- if (readdirSync3(injectedSkillsRoot).length === 0) {
6644
- rmSync7(join16(dir, ".agents", "skills"), { recursive: true, force: true });
6645
- rmSync7(join16(dir, ".agents"), { recursive: true, force: true });
7456
+ if (readdirSync4(injectedSkillsRoot).length === 0) {
7457
+ rmSync9(join17(dir, ".agents", "skills"), { recursive: true, force: true });
7458
+ rmSync9(join17(dir, ".agents"), { recursive: true, force: true });
6646
7459
  }
6647
7460
  } catch {
6648
7461
  }
@@ -6651,15 +7464,15 @@ function collectSiblingGitCommonDirs(taskRoot, excludeDir) {
6651
7464
  const dirs = /* @__PURE__ */ new Set();
6652
7465
  let entries = [];
6653
7466
  try {
6654
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7467
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6655
7468
  } catch {
6656
7469
  return [];
6657
7470
  }
6658
7471
  for (const e of entries) {
6659
7472
  if (!e.isDirectory() || e.name.startsWith("."))
6660
7473
  continue;
6661
- const repoPath = join16(taskRoot, e.name);
6662
- if (repoPath === excludeDir || !existsSync13(join16(repoPath, ".git")))
7474
+ const repoPath = join17(taskRoot, e.name);
7475
+ if (repoPath === excludeDir || !existsSync13(join17(repoPath, ".git")))
6663
7476
  continue;
6664
7477
  try {
6665
7478
  const commonDir = execFileSync8("git", ["-C", repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
@@ -6676,13 +7489,13 @@ function removeTaskWorkspace(repoCachePath, taskRoot, expected) {
6676
7489
  throw new Error("MARKER_MISMATCH");
6677
7490
  const { primaryDir, legacy } = layout;
6678
7491
  if (!legacy) {
6679
- if (primaryDir !== join16(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
7492
+ if (primaryDir !== join17(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
6680
7493
  throw new Error("GIT_REMOVE_FAILED");
6681
7494
  }
6682
7495
  cleanupInjectedSkills(legacy ? primaryDir : taskRoot);
6683
7496
  const siblingCommonDirs = legacy ? [] : collectSiblingGitCommonDirs(taskRoot, primaryDir);
6684
7497
  try {
6685
- rmSync7(taskRoot, { recursive: true, force: true });
7498
+ rmSync9(taskRoot, { recursive: true, force: true });
6686
7499
  } catch {
6687
7500
  throw new Error("GIT_REMOVE_FAILED");
6688
7501
  }
@@ -6706,7 +7519,7 @@ function removeWorktree(repoCachePath, worktreePath) {
6706
7519
  }
6707
7520
 
6708
7521
  // src/daemon/repo-checkout-service.ts
6709
- var CHECKOUT_JSON_FILE = join17(".morula", "checkout.json");
7522
+ var CHECKOUT_JSON_FILE = join18(".morula", "checkout.json");
6710
7523
  var LocalRepoCheckoutService = class {
6711
7524
  constructor(opts) {
6712
7525
  this.opts = opts;
@@ -6794,9 +7607,9 @@ var LocalRepoCheckoutService = class {
6794
7607
  return;
6795
7608
  this.tasks.set(taskId, { workdir, checkToken, primaryRepoDir });
6796
7609
  try {
6797
- const dir = join17(workdir, ".morula");
6798
- mkdirSync10(dir, { recursive: true, mode: 448 });
6799
- writeFileSync8(join17(dir, "checkout.json"), JSON.stringify({
7610
+ const dir = join18(workdir, ".morula");
7611
+ mkdirSync11(dir, { recursive: true, mode: 448 });
7612
+ writeFileSync9(join18(dir, "checkout.json"), JSON.stringify({
6800
7613
  taskId,
6801
7614
  port: this.boundPort,
6802
7615
  checkToken
@@ -6848,6 +7661,7 @@ var LocalRepoCheckoutService = class {
6848
7661
  const cache = openRepoCache(
6849
7662
  this.opts.cfg.workdir,
6850
7663
  this.opts.tenantId,
7664
+ this.runtimeId,
6851
7665
  repoFullName,
6852
7666
  env,
6853
7667
  baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0,
@@ -6912,6 +7726,7 @@ var LocalRepoCheckoutService = class {
6912
7726
  const cache = openRepoCache(
6913
7727
  this.opts.cfg.workdir,
6914
7728
  this.opts.tenantId,
7729
+ this.runtimeId,
6915
7730
  repoFullName,
6916
7731
  env,
6917
7732
  baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0,
@@ -6960,11 +7775,11 @@ function isReadOnlyRepoDir(dirName) {
6960
7775
  return READ_ONLY_DIR_RE.test(dirName) || dirName.startsWith(LEGACY_READ_ONLY_DIR_PREFIX);
6961
7776
  }
6962
7777
  function resolveRepoTargetDir(taskRoot, base) {
6963
- const direct = join17(taskRoot, base);
6964
- if (!existsSync14(direct) || existsSync14(join17(direct, ".git")))
7778
+ const direct = join18(taskRoot, base);
7779
+ if (!existsSync14(direct) || existsSync14(join18(direct, ".git")))
6965
7780
  return direct;
6966
7781
  for (let i = 2; i < 100; i++) {
6967
- const candidate = join17(taskRoot, `${base}-${i}`);
7782
+ const candidate = join18(taskRoot, `${base}-${i}`);
6968
7783
  if (!existsSync14(candidate))
6969
7784
  return candidate;
6970
7785
  }
@@ -6973,7 +7788,7 @@ function resolveRepoTargetDir(taskRoot, base) {
6973
7788
  function materializeReadWorktree(input) {
6974
7789
  const worktreePath = resolveRepoTargetDir(input.taskRoot, `${safeRepoDirName(input.repoFullName)}${READ_ONLY_REPO_DIR_SUFFIX}`);
6975
7790
  const targetRef = resolveReadRef(input.repoCachePath, input.env, input.ref, input.baseBranch);
6976
- if (existsSync14(join17(worktreePath, ".git"))) {
7791
+ if (existsSync14(join18(worktreePath, ".git"))) {
6977
7792
  const current = currentBranchName(worktreePath);
6978
7793
  if (current)
6979
7794
  throw new Error(`READ_WORKTREE_DIRTY: \u53EA\u8BFB\u68C0\u51FA\u5DF2\u5207\u5230\u5206\u652F ${current}\uFF0C\u62D2\u7EDD\u81EA\u52A8\u66F4\u65B0\uFF08\u8BF7\u6E05\u7406\u8BE5\u76EE\u5F55\u540E\u91CD\u8BD5\uFF09`);
@@ -6981,7 +7796,7 @@ function materializeReadWorktree(input) {
6981
7796
  throw new Error("READ_WORKTREE_DIRTY: \u53EA\u8BFB\u68C0\u51FA\u5B58\u5728\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u62D2\u7EDD\u81EA\u52A8\u66F4\u65B0\uFF08\u8BF7\u6E05\u7406\u8BE5\u76EE\u5F55\u540E\u91CD\u8BD5\uFF09");
6982
7797
  execFileSync9("git", ["-C", worktreePath, "checkout", "--detach", targetRef], { env: input.env, stdio: "pipe" });
6983
7798
  } else {
6984
- mkdirSync10(join17(worktreePath, ".."), { recursive: true });
7799
+ mkdirSync11(join18(worktreePath, ".."), { recursive: true });
6985
7800
  execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", worktreePath, targetRef], {
6986
7801
  env: input.env,
6987
7802
  stdio: "pipe"
@@ -7049,11 +7864,11 @@ function listMaterializedRepoDirs(taskRoot, options = {}) {
7049
7864
  const excluded = new Set(options.excludeDirs ?? []);
7050
7865
  let entries;
7051
7866
  try {
7052
- entries = readdirSync4(taskRoot, { withFileTypes: true });
7867
+ entries = readdirSync5(taskRoot, { withFileTypes: true });
7053
7868
  } catch {
7054
7869
  return [];
7055
7870
  }
7056
- return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join17(taskRoot, e.name)).filter((dir) => !excluded.has(dir) && !isReadOnlyRepoDir(basename2(dir))).filter((dir) => existsSync14(join17(dir, ".git"))).sort();
7871
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join18(taskRoot, e.name)).filter((dir) => !excluded.has(dir) && !isReadOnlyRepoDir(basename2(dir))).filter((dir) => existsSync14(join18(dir, ".git"))).sort();
7057
7872
  }
7058
7873
  function parseRepoFullNameFromRemoteUrl(url) {
7059
7874
  const raw = url?.trim();
@@ -7075,7 +7890,7 @@ function parseRepoFullNameFromRemoteUrl(url) {
7075
7890
  }
7076
7891
  function collectMaterializedRepoDeliveryTargets(input) {
7077
7892
  const excludedNames = new Set(input.excludeRepoFullNames?.filter(Boolean) ?? []);
7078
- const excludedDirs = [...excludedNames].map((name) => join17(input.taskRoot, safeRepoDirName(name)));
7893
+ const excludedDirs = [...excludedNames].map((name) => join18(input.taskRoot, safeRepoDirName(name)));
7079
7894
  const recordsByDir = /* @__PURE__ */ new Map();
7080
7895
  const recordsByDirName = /* @__PURE__ */ new Map();
7081
7896
  for (const [repoFullName, record] of input.prepared ?? []) {
@@ -7138,7 +7953,7 @@ function syncGitIdentity(primaryDir, targetDir, env) {
7138
7953
  }
7139
7954
  }
7140
7955
  function materializeAdditionalRepo(input) {
7141
- if (existsSync14(join17(input.targetPath, ".git"))) {
7956
+ if (existsSync14(join18(input.targetPath, ".git"))) {
7142
7957
  const current = currentBranchName(input.targetPath);
7143
7958
  if (!current && !hasLocalChanges2(input.targetPath, input.env)) {
7144
7959
  const targetRef = resolveReadRef(input.repoCachePath, input.env, void 0, input.baseBranch);
@@ -7158,7 +7973,7 @@ function materializeAdditionalRepo(input) {
7158
7973
  }
7159
7974
  if (!baseRef)
7160
7975
  throw new Error("BASE_REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u8FDC\u7AEF\u5F15\u7528\uFF08origin/<base> / origin/main / origin/master\uFF09");
7161
- mkdirSync10(join17(input.targetPath, ".."), { recursive: true });
7976
+ mkdirSync11(join18(input.targetPath, ".."), { recursive: true });
7162
7977
  execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", input.targetPath, baseRef], {
7163
7978
  env: input.env,
7164
7979
  stdio: "pipe"
@@ -7166,94 +7981,814 @@ function materializeAdditionalRepo(input) {
7166
7981
  return input.targetPath;
7167
7982
  }
7168
7983
 
7169
- // src/daemon/task-messages.ts
7170
- var TASK_MESSAGE_QUEUE_MAX = 200;
7171
- function asRecord(value) {
7172
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
7173
- }
7174
- function asNonEmptyString(value) {
7175
- return typeof value === "string" && value.length > 0 ? value : null;
7176
- }
7177
- function normalizeAgentMessage(value) {
7178
- const row = asRecord(value);
7179
- if (!row)
7180
- return null;
7181
- const id = asNonEmptyString(row.id);
7182
- if (!id)
7183
- return null;
7184
- const seq = row.seq;
7185
- if (typeof seq !== "number" || !Number.isFinite(seq))
7186
- return null;
7187
- return {
7188
- id,
7189
- seq,
7190
- authorType: typeof row.authorType === "string" ? row.authorType : "unknown",
7191
- authorPrincipalId: asNonEmptyString(row.authorPrincipalId),
7192
- authorName: asNonEmptyString(row.authorName),
7193
- content: typeof row.content === "string" ? row.content : "",
7194
- mode: typeof row.mode === "string" ? row.mode : "queue",
7195
- deliveryState: typeof row.deliveryState === "string" ? row.deliveryState : "pending",
7196
- deliveryError: asNonEmptyString(row.deliveryError),
7197
- targetTaskId: asNonEmptyString(row.targetTaskId),
7198
- anchorEventSeq: typeof row.anchorEventSeq === "number" ? row.anchorEventSeq : null,
7199
- turnNo: typeof row.turnNo === "number" ? row.turnNo : null,
7200
- createdAt: typeof row.createdAt === "string" ? row.createdAt : ""
7201
- };
7202
- }
7203
- function parseTaskMessageFrame(value) {
7204
- const frame = asRecord(value);
7205
- if (!frame)
7206
- return null;
7207
- const taskId = asNonEmptyString(frame.taskId);
7208
- if (!taskId || !Array.isArray(frame.messages))
7209
- return null;
7210
- const messages = frame.messages.map(normalizeAgentMessage).filter((message) => message !== null);
7211
- if (messages.length === 0)
7212
- return null;
7213
- return {
7214
- type: "agent-task:message",
7215
- taskId,
7216
- objectId: asNonEmptyString(frame.objectId),
7217
- anchorKey: typeof frame.anchorKey === "string" ? frame.anchorKey : "",
7218
- messages
7219
- };
7220
- }
7221
- function upsertTaskMessages(existing, incoming, max = TASK_MESSAGE_QUEUE_MAX) {
7222
- const byId = /* @__PURE__ */ new Map();
7223
- for (const message of existing)
7224
- byId.set(message.id, message);
7225
- for (const message of incoming)
7226
- byId.set(message.id, message);
7227
- const merged = [...byId.values()].sort((a, b) => a.seq - b.seq || a.id.localeCompare(b.id));
7228
- const dropped = Math.max(0, merged.length - max);
7229
- return { messages: dropped > 0 ? merged.slice(dropped) : merged, dropped };
7230
- }
7231
- function selectConsumedMessages(messages, taskId) {
7232
- return messages.filter((message) => message.targetTaskId === taskId);
7233
- }
7234
- function selectSteerMessages(messages, taskId) {
7235
- return messages.filter((message) => message.mode === "steer").filter((message) => message.deliveryState !== "consumed" && message.deliveryState !== "withdrawn").filter((message) => message.targetTaskId === taskId).sort((a, b) => a.seq - b.seq);
7236
- }
7237
- function renderMessageList(messages) {
7238
- return messages.map((message) => `${message.seq}. \u3010${message.authorType === "user" ? "\u4EBA" : message.authorType === "system" ? "\u5E73\u53F0" : "\u8FD0\u884C\u65F6"}\u3011${message.content}`).join("\n\n");
7984
+ // src/daemon/session-host.ts
7985
+ init_dist2();
7986
+ init_acp_connection();
7987
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "node:fs";
7988
+ import { join as join19 } from "node:path";
7989
+
7990
+ // src/daemon/steward-instructions.ts
7991
+ var STEWARD_SYSTEM_INSTRUCTIONS = `\u4F60\u662F\u300C\u7A7A\u95F4\u7BA1\u5BB6\u667A\u80FD\u4F53\u300D\uFF08Project Steward\uFF09\u3002
7992
+
7993
+ \u4F60\u7684\u76EE\u6807\uFF1A\u505A\u4E09\u4EF6\u4E8B\u2014\u2014\u5BF9\u7528\u6237\u63D0\u51FA\u7684\u9700\u6C42/\u95EE\u9898\u505A**\u65B9\u5411\u7EA7\u88C1\u5B9A**\uFF08\u8BE5\u4E0D\u8BE5\u505A\uFF09\uFF1B\u9700\u8981\u505A\u65F6**\u6D3E\u5355**\u7ED9\u300C\u5F85\u5206\u6790\u300D\u8FD0\u884C\u65F6\uFF0C\u5E76\u5728\u5F00\u53D1/\u8BC4\u5BA1\u8FC7\u7A0B\u4E2D**\u8DDF\u8E2A\u8FDB\u5C55**\uFF1B\u4EE5\u53CA**\u7BA1\u7406\u672C\u7A7A\u95F4\u7684\u4EFB\u52A1**\uFF08\u6539\u5C5E\u6027\u4E0E\u8D1F\u8D23\u4EBA\u3001\u63A8\u8FDB\u72B6\u6001\u3001\u5728\u4EFB\u52A1\u91CC @ \u4EBA\u50AC\u529E\u3001\u505C\u6389\u6216\u91CD\u8DD1\u5361\u4F4F\u7684\u6267\u884C\uFF09\u3002
7994
+ \u4F60\u7684\u8FB9\u754C\uFF1A\u4E0D\u5199\u4EE3\u7801\u3001\u4E0D\u505A\u6587\u4EF6\u7EA7\u65B9\u6848\u8BBE\u8BA1\u3001\u4E0D\u76F4\u63A5\u5408\u5E76\u4EE3\u7801\uFF1B\u6700\u7EC8\u5408\u5E76/\u53D1\u5E03\u7531\u4EBA\u51B3\u5B9A\u3002
7995
+
7996
+ ## \u4F60\u7684\u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177
7997
+
7998
+ - \u7A7A\u95F4\u8BF4\u660E\uFF08\u7A7A\u95F4\u4FE1\u606F\u3001\u4ED3\u5E93\u6E05\u5355\u3001\u7EA6\u5B9A\u4E0E\u8FB9\u754C\u3001\u5B8C\u6210\u6807\u51C6\uFF09\uFF1A\u5DE5\u4F5C\u533A\u6839 \`AGENTS.md\`\uFF0C\u5DF2\u81EA\u52A8\u52A0\u8F7D\uFF1B
7999
+ - \u4F1A\u8BDD\u4E8B\u5B9E\uFF08\u7A7A\u95F4 ID\u3001\u79DF\u6237\u3001\u5F53\u524D\u7528\u6237\u3001\u7ED1\u5B9A\u4ED3\u5E93\uFF09\uFF1A\`.agent_context/steward-context.md\`\uFF1B
8000
+ - \u884C\u4E3A\u5951\u7EA6\u4E0E\u547D\u4EE4\u7528\u6CD5\uFF1A\`steward-management\` skill\uFF08\u5DF2\u6CE8\u5165 \`.agents/skills/\`\uFF09\uFF0C**\u52A8\u624B\u524D\u5148\u8BFB\u5B83**\uFF1B
8001
+ \u4E0E\u5E73\u53F0\u4EA4\u4E92\u53EA\u80FD\u8D70 \`morula\` CLI\uFF08\u672C\u4F1A\u8BDD\u5DE5\u4F5C\u533A\u5DF2\u6CE8\u5165 PATH \u4E0E\u51ED\u8BC1\uFF09\uFF0C\u6240\u6709\u67E5\u8BE2\u663E\u5F0F\u5E26 \`--space <spaceId>\`\u3002`;
8002
+ function buildStewardContextMd(facts) {
8003
+ const lines = [
8004
+ "# \u7BA1\u5BB6\u4F1A\u8BDD\u4E0A\u4E0B\u6587",
8005
+ "",
8006
+ "| \u4E8B\u5B9E | \u503C |",
8007
+ "|------|-----|",
8008
+ `| \u7A7A\u95F4 ID\uFF08\`--space\` \u7528\u5B83\uFF09 | \`${facts.projectId}\` |`,
8009
+ `| \u7A7A\u95F4\u540D\u79F0 | ${facts.projectName?.trim() || "\uFF08\u672A\u547D\u540D\uFF09"} |`,
8010
+ `| \u79DF\u6237 | \`${facts.tenantId}\` |`,
8011
+ `| \u5F53\u524D\u7528\u6237 | ${facts.userName?.trim() || "\uFF08\u672A\u77E5\uFF09"} |`
8012
+ ];
8013
+ if (facts.stewardPrincipalId)
8014
+ lines.push(`| \u7BA1\u5BB6\u7F72\u540D\u8EAB\u4EFD | \`${facts.stewardPrincipalId}\` |`);
8015
+ lines.push("");
8016
+ if (facts.repos?.length) {
8017
+ lines.push("## \u672C\u7A7A\u95F4\u7ED1\u5B9A\u4ED3\u5E93", "");
8018
+ for (const repo of facts.repos)
8019
+ lines.push(`- \`${repo}\``);
8020
+ lines.push("", "\u591A\u4ED3\u5E93\u7A7A\u95F4\u6D3E\u5355\u65F6\u7528 `--repo <owner/name>` \u6307\u660E\u6D89\u53CA\u4ED3\u5E93\uFF1B\u4E0D\u786E\u5B9A\u5C31\u7559\u7A7A\u3002");
8021
+ } else {
8022
+ lines.push("## \u672C\u7A7A\u95F4\u7ED1\u5B9A\u4ED3\u5E93", "", "\uFF08\u672A\u7ED1\u5B9A\u4ED3\u5E93\uFF09");
8023
+ }
8024
+ lines.push("", "> \u547D\u4EE4\u7528\u6CD5\u89C1 `.agents/skills/steward-management/SKILL.md`\uFF1B\u7A7A\u95F4\u8BF4\u660E\u89C1\u5DE5\u4F5C\u533A\u6839 `AGENTS.md`\u3002");
8025
+ return `${lines.join("\n")}
8026
+ `;
7239
8027
  }
7240
- function buildSteerPrompt(input) {
7241
- const opinions = input.messages.length > 0 ? renderMessageList(input.messages) : "\uFF08\u6253\u65AD\u8BF7\u6C42\u672A\u88AB\u53D6\u5230\uFF0C\u8BF7\u6309\u539F\u59CB\u9700\u6C42\u7EE7\u7EED\uFF09";
7242
- const resetNote = input.sessionReset ? "\n- \u26A0\uFE0F \u4F1A\u8BDD\u5DF2\u91CD\u5EFA\uFF1A\u4E0A\u4E00\u56DE\u5408\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587**\u672A\u4FDD\u7559**\uFF0C\u8BF7\u4EE5\u672C\u56DE\u5408\u7ED9\u51FA\u7684\u4FE1\u606F\u4E3A\u51C6\uFF08\u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E git \u72B6\u6001\u4ECD\u5728\uFF09\u3002" : "";
7243
- const originalPrompt = input.taskPrompt?.trim() ? `
7244
8028
 
7245
- \u3010\u539F\u59CB\u4EFB\u52A1\u9700\u6C42\uFF08\u672C\u8F6E\u662F\u540C\u4E00\u6B21\u5DE5\u4F5C\u7684\u5EF6\u7EED\uFF09\u3011
7246
- ${input.taskPrompt.trim()}` : "";
7247
- return `\u3010\u8FD0\u884C\u4E2D\u6253\u65AD \xB7 \u8BF7\u5148\u6309\u8FD9\u4E9B\u610F\u89C1\u8C03\u6574\uFF0C\u518D\u7EE7\u7EED\u3011
7248
- \u4F60\u6B63\u5728\u6267\u884C\u7B2C ${input.turnNo - 1} \u56DE\u5408\uFF0C\u4EBA\u5728\u8FD0\u884C\u8FC7\u7A0B\u4E2D\u63D0\u51FA\u4E86\u4EE5\u4E0B\u610F\u89C1\uFF08\u6309\u63D0\u51FA\u987A\u5E8F\uFF09\uFF1A
8029
+ // src/daemon/steward-skill.ts
8030
+ var STEWARD_SKILL_NAME = "steward-management";
8031
+ var STEWARD_SKILL_FRONTMATTER = `---
8032
+ name: steward-management
8033
+ description: "Use when acting as a project steward in a Morula steward session \u2014 to operate the platform through the morula CLI with explicit parameters (project/task queries, dispatch, dashboards), and to keep the verdict output format the product renders."
8034
+ user-invocable: false
8035
+ allowed-tools: Bash(morula *)
8036
+ ---
8037
+ `;
8038
+ var STEWARD_SKILL_BODY = `# \u7A7A\u95F4\u7BA1\u5BB6\u5951\u7EA6\uFF08Morula\uFF09
7249
8039
 
7250
- ${opinions}
8040
+ \u4F60\u662F**\u7A7A\u95F4\u7BA1\u5BB6**\uFF1A\u505A\u65B9\u5411\u7EA7\u88C1\u5B9A\u3001\u6D3E\u5355\u3001\u8DDF\u8E2A\u8FDB\u5C55\uFF0C\u5E76**\u7BA1\u7406\u672C\u7A7A\u95F4\u7684\u4EFB\u52A1**\u3002\u672C\u6587\u4EF6\u662F\u4F60\u884C\u4E3A\u89C4\u8303\u7684**\u552F\u4E00\u6743\u5A01\u6B63\u6587**
8041
+ \uFF08\u9996\u6761 prompt \u53EA\u7ED9\u8EAB\u4EFD\u4E0E\u5165\u53E3\uFF0C\u89C4\u8303\u4E00\u5F8B\u5728\u6B64\uFF0C\u89C1 issues/archived/steward-first-prompt-context-overload.md\uFF09\u3002
7251
8042
 
7252
- \u8981\u6C42\uFF1A
7253
- 1. \u5148\u9010\u6761\u5904\u7406\u4E0A\u8FF0\u610F\u89C1\uFF0C\u518D\u7EE7\u7EED\u5B8C\u6210\u672C\u4EFB\u52A1\uFF1B\u610F\u89C1\u4E0E\u539F\u59CB\u9700\u6C42\u51B2\u7A81\u65F6**\u4EE5\u610F\u89C1\u4E3A\u51C6**\uFF0C\u5E76\u5728\u6700\u7EC8\u603B\u7ED3\u91CC\u8BF4\u660E\u5DEE\u5F02\u3002
7254
- 2. \u4E0D\u8981\u63A8\u7FFB\u4E0A\u8FF0\u610F\u89C1\u672A\u6D89\u53CA\u7684\u90E8\u5206\uFF1B\u5DF2\u7ECF\u5B8C\u6210\u4E14\u4E0D\u5728\u610F\u89C1\u8303\u56F4\u5185\u7684\u6539\u52A8\u4FDD\u7559\u3002
7255
- 3. \u4E0A\u4E00\u56DE\u5408\u5728\u5B89\u5168\u70B9\u88AB\u4E2D\u65AD\uFF0C\u5176\u4E2D\u7684\u5DE5\u5177\u8FDB\u5EA6\u5DF2\u4E22\u5F03\u3001\u88AB\u4E2D\u65AD\u7684\u8C03\u7528**\u6CA1\u6709\u7ED3\u679C**\uFF08\u65F6\u95F4\u7EBF\u4E0A\u6807\u4E3A\u5DF2\u4E2D\u65AD\uFF09\u2014\u2014\u9700\u8981\u65F6\u91CD\u65B0\u6267\u884C\uFF0C\u4E0D\u8981\u5047\u5B9A\u5B83\u5DF2\u6210\u529F\u3002${resetNote}${originalPrompt}`;
7256
- }
8043
+ ## 0. \u804C\u8D23\u8FB9\u754C\u4E0E\u786C\u7EA6\u675F
8044
+
8045
+ ### 0.1 \u804C\u8D23\u8FB9\u754C
8046
+
8047
+ - **\u4F60\u505A\u4E09\u4EF6\u4E8B**\uFF1A**\u88C1\u5B9A**\uFF08\u8BE5\u4E0D\u8BE5\u505A\uFF0C\u65B9\u5411\u7EA7\uFF09\u2192 **\u6D3E\u5355**\uFF08\u628A\u9700\u6C42\u4EA4\u7ED9\u300C\u5F85\u5206\u6790\u300D\u8FD0\u884C\u65F6\uFF09\u2192 **\u7BA1\u7406**\uFF08\u6539\u4EFB\u52A1\u5C5E\u6027\u4E0E\u8D1F\u8D23\u4EBA\u3001\u63A8\u8FDB\u72B6\u6001\u3001\u5728\u4EFB\u52A1\u91CC @ \u4EBA\u50AC\u529E\u3001\u505C\u6389\u6216\u91CD\u8DD1\u5361\u4F4F\u7684\u6267\u884C\uFF09\u3002
8048
+ - **\u4F60\u4E0D\u505A\u5177\u4F53\u4EFB\u52A1\u7684\u9700\u6C42\u5206\u6790**\uFF1A\u4E0D\u8BFB\u4ED3\u5E93\u6E90\u7801\u4EA7\u51FA\u6587\u4EF6\u7EA7\u6539\u52A8\u6E05\u5355\u3001\u4E0D\u66FF\u4EFB\u52A1\u8BBE\u8BA1\u5B9E\u73B0\u65B9\u6848\u2014\u2014\u8FD9\u4E9B\u7531\u300C\u5F85\u5206\u6790\u8FD0\u884C\u65F6\u300D\u5728\u4EFB\u52A1\u5185\u5B8C\u6210\u3002\u4F60\u8BFB\u4ED3\u5E93\u53EA\u7528\u4E8E\u4E86\u89E3\u7A7A\u95F4\u73B0\u72B6\u3001\u6838\u67E5\u4E8B\u5B9E\u3001\u56DE\u7B54\u300C\u8FD9\u4E2A\u4ED3\u5E93\u662F\u600E\u4E48\u5B9E\u73B0\u7684\u300D\u7C7B\u95EE\u9898\u3002
8049
+ - **\u4F60\u4E0D\u76F4\u63A5\u5199\u4EE3\u7801\u3001\u4E0D\u6267\u884C\u6784\u5EFA/\u6D4B\u8BD5\u3001\u4E0D\u5408\u5E76\u4EE3\u7801**\uFF08\u8FD9\u4E9B\u7531\u5F00\u53D1/\u8BC4\u5BA1\u8FD0\u884C\u65F6\u4E0E\u4EBA\u5B8C\u6210\uFF09\u3002
8050
+ - **\u5408\u5E76 / \u53D1\u5E03 / \u89E6\u53D1\u90E8\u7F72 / \u7A7A\u95F4\u6210\u5458\u4E0E\u6743\u9650 / \u7A7A\u95F4\u5220\u9664**\uFF1A\u4F60\u6CA1\u6709\u5165\u53E3\uFF0C\u4E5F\u4E0D\u8981\u5C1D\u8BD5\uFF1B\u6700\u7EC8\u5408\u5E76\u4E0E\u53D1\u5E03\u59CB\u7EC8\u7531\u4EBA\u51B3\u5B9A\u3002
8051
+ - \u7A7A\u95F4\u4E0A\u4E0B\u6587\u6765\u81EA**\u5DE5\u4F5C\u533A\u6839 \`AGENTS.md\`**\uFF08\u7A7A\u95F4\u4FE1\u606F\u3001\u4ED3\u5E93\u6E05\u5355\u3001\u7EA6\u5B9A\u4E0E\u8FB9\u754C\u3001\u5B8C\u6210\u6807\u51C6\uFF09\uFF0C\u5B83\u5DF2\u81EA\u52A8\u52A0\u8F7D\uFF1B\u9700\u8981\u786E\u8BA4\u7A7A\u95F4\u7EA6\u5B9A\u65F6\u4EE5\u5B83\u4E3A\u51C6\uFF0C\u4E0D\u8981\u58F0\u79F0\u81EA\u5DF1\u770B\u4E0D\u5230\u7A7A\u95F4\u8BF4\u660E\u3002
8052
+
8053
+ ### 0.2 \u786C\u7EA6\u675F
8054
+
8055
+ - **\u4E0E\u5E73\u53F0\u4EA4\u4E92\u53EA\u80FD\u8D70\u5E73\u53F0 CLI**\uFF1A\u4F18\u5148\u7528 \`$MORULA_CLI\`\uFF08daemon \u6CE8\u5165\u7684**\u7EDD\u5BF9\u8DEF\u5F84**\uFF0C\u7248\u672C\u4E0E\u4F1A\u8BDD\u540C\u6E90\uFF09\uFF1B\u53EA\u6709\u5728 \`$MORULA_CLI\` \u4E3A\u7A7A\u65F6\u624D\u7528 PATH \u4E0A\u7684 \`morula\`\u2014\u2014PATH \u4E0A\u90A3\u4EFD\u53EF\u80FD\u662F\u5BBF\u4E3B\u81EA\u5E26\u7684\u65E7\u7248\u672C\uFF08\u7F3A\u547D\u4EE4\u3001\u4E14\u4E0D\u8BA4\u4F1A\u8BDD\u51ED\u8BC1\uFF0C\u4F1A\u8BA9\u5199\u64CD\u4F5C\u7F72\u540D\u9519\u8BEF\uFF09\u3002\u4E0D\u8981\u7528 \`curl\` \u76F4\u8FDE REST\u3001\u4E0D\u8981\u8BD5\u56FE\u7528 MCP \u5DE5\u5177\u3002
8056
+ - **\u4E0D\u8981\u7528\u4F9D\u8D56\u4EFB\u52A1\u4E0A\u4E0B\u6587\u7684\u547D\u4EE4**\uFF1A\u672C\u4F1A\u8BDD**\u6CA1\u6709 \`MORULA_TASK_ID\`**\u3002
8057
+ \u4E0B\u5217\u547D\u4EE4\u5728\u7BA1\u5BB6\u4F1A\u8BDD\u91CC\u5FC5\u7136\u5931\u8D25\u6216\u8BED\u4E49\u9519\u8BEF\uFF0C\u7981\u6B62\u4F7F\u7528\uFF1A
8058
+ \`morula task get\`\uFF08\u65E0\u53C2\u5F62\u5F0F\uFF09\u3001\`morula task branch\`\u3001\`morula task state\`\u3001
8059
+ \`morula task analysis-verdict\`\u3001\`morula task inbox\`\u3001\`morula review submit\`\u3001
8060
+ \`morula repo checkout\`\u3001\`morula repo prepare\`\u3001\`morula repo mr *\`\u3001\`morula attachment upload\`\u3002
8061
+ \u540C\u7406\uFF0C**CI \u547D\u4EE4\u4E00\u5F8B\u7528 \`--space\` \u5F62\u5F0F**\uFF08\`morula ci status --space <id>\`\uFF09\uFF0C
8062
+ \u4E0D\u8981\u7528\u4F4D\u7F6E\u53C2\u6570\u5F62\u5F0F\uFF08\`morula ci status <taskId>\` \u662F\u4EFB\u52A1\u9762\u547D\u4EE4\uFF0C\u672C\u4F1A\u8BDD\u6CA1\u6709 taskId\uFF09\u3002
8063
+ - **\u4E0D\u8981 @ \u8FD0\u884C\u65F6\uFF08\u667A\u80FD\u4F53\uFF09**\uFF1A\u8BC4\u8BBA\u91CC @ \u8FD0\u884C\u65F6\u662F\u300C\u89E6\u53D1\u67D0\u4E2A\u9636\u6BB5\u6267\u884C\u300D\u7684\u901A\u9053\uFF0C\u5C5E\u6D41\u7A0B\u63A7\u5236\uFF1B\u6D41\u7A0B\u63A7\u5236\u4E00\u5F8B\u8D70\u6D3E\u5355\u4E0E\u72B6\u6001\u63A8\u8FDB\uFF08\u672C\u4F1A\u8BDD\u672A\u6388\u6743\u8BE5\u7528\u6CD5\uFF0C\u89C1 features/steward-task-management.md \xA73.4\uFF09\u3002
8064
+ - **\u6240\u6709\u67E5\u8BE2\u90FD\u8981\u663E\u5F0F\u5E26 \`--space\`**\uFF08= \u5F53\u524D\u7A7A\u95F4 ID\uFF0C\u89C1 \`.agent_context/steward-context.md\`\uFF09\u3002
8065
+ - \u65B0\u9700\u6C42**\u4E00\u5F8B\u8D70\u6D3E\u5355**\uFF0C\u4E0D\u8981\u81EA\u5DF1\u52A8\u624B\u5B9E\u73B0\uFF0C\u4E5F\u4E0D\u8981\u7528\u300C\u59D4\u6258\u5B50\u4EE3\u7406\u300D\u7C7B\u624B\u6BB5\u7ED5\u5F00\u6D3E\u5355\u94FE\u8DEF\u3002
8066
+ - **\u7834\u574F\u6027\u52A8\u4F5C\u5FC5\u987B\u5148\u8981\u4EBA\u786E\u8BA4**\uFF1A\u5220\u9664\u4EFB\u52A1\u3001\u53D6\u6D88\u6B63\u5728\u8DD1\u7684\u6267\u884C\u3001\u91CD\u8DD1\u5931\u8D25\u4EFB\u52A1\u3001**\u91CD\u8DD1\u5931\u8D25\u7684 CI \u6D41\u6C34\u7EBF / job**\u3001\u628A\u72B6\u6001\u63A8\u8FDB\u5230\u300C\u5DF2\u5408\u5E76 / \u5DF2\u53D1\u5E03 / \u5DF2\u5173\u95ED\u300D\u2014\u2014\u7528**\u7ED3\u6784\u5316\u63D0\u95EE\u5DE5\u5177**\u53D1\u786E\u8BA4\u8868\u5355\uFF0C\u62FF\u5230\u7528\u6237\u7B54\u590D\u540E\u518D\u6267\u884C\u3002\u53EF\u9006\u7684\u8BB0\u5F55\u4E0E\u6D41\u8F6C\uFF08\u6539\u5C5E\u6027\u3001\u6539\u8D1F\u8D23\u4EBA\u3001\`backlog \u21C4 todo\` \u63A8\u8FDB\uFF09\u4E0D\u9700\u8981\u786E\u8BA4\u3002
8067
+ - **\u63A2\u67E5\u547D\u4EE4\u9762\u53EA\u7528 \`--help\`\uFF0C\u4E0D\u8981\u62FF\u771F\u5B9E\u5199\u547D\u4EE4\u8BD5\u8DD1**\uFF1A\u60F3\u786E\u8BA4\u67D0\u6761\u547D\u4EE4\u6216\u53C2\u6570\u5B58\u4E0D\u5B58\u5728\u3001\u5F62\u6001\u5BF9\u4E0D\u5BF9\uFF0C\u5C31\u770B \`$MORULA_CLI <cmd> --help\`\uFF08\u4F8B\u5982 \`$MORULA_CLI ci retry --help\`\uFF09\u3002\u5199\u547D\u4EE4**\u6267\u884C\u5373\u751F\u6548**\uFF08\`morula ci retry\` \u6267\u884C\u5373\u91CD\u8DD1\u3001\`morula task delete\` \u6267\u884C\u5373\u5220\u9664\uFF09\uFF0C\u6CA1\u6709\u300C\u53EA\u8BFB\u63A2\u67E5\u300D\u5F62\u6001\u2014\u2014\u4E3A\u300C\u770B\u770B\u8FD9\u6761\u547D\u4EE4\u5728\u4E0D\u5728\u300D\u800C\u771F\u8DD1\u4E00\u6B21\u5199\u547D\u4EE4\uFF0C\u5C31\u662F\u4E00\u6B21\u672A\u7ECF\u786E\u8BA4\u7684\u4E0D\u53EF\u9006\u64CD\u4F5C\uFF08\u771F\u673A\u4E8B\u6545\uFF1A\u7BA1\u5BB6\u786E\u8BA4 \`ci retry\` \u662F\u5426\u5B58\u5728\u65F6\u771F\u7684\u91CD\u8DD1\u4E86\u4E00\u6761\u6D41\u6C34\u7EBF\uFF09\u3002
8068
+
8069
+ ## 1. \u5E38\u7528\u547D\u4EE4\uFF08\u5168\u90E8\u663E\u5F0F\u53C2\u6570\uFF09
8070
+
8071
+ ### \u770B\u7A7A\u95F4\u4E0E\u4EFB\u52A1
8072
+ \`\`\`bash
8073
+ morula steward get --space <spaceId> # \u7BA1\u5BB6\u72B6\u6001 / \u7A7A\u95F4\u8BF4\u660E\uFF08AGENTS.md\uFF09/ \u9636\u6BB5\u6267\u884C\u7ED1\u5B9A
8074
+ morula task list --space <spaceId> [--state <code>] [--assignee <principalId>] [--search <text>]
8075
+ morula task get <objectId> # \u5355\u4E2A\u4E1A\u52A1\u4EFB\u52A1\u8BE6\u60C5\uFF08\u5FC5\u987B\u5E26 id\uFF09
8076
+ morula task agent-tasks --object <objectId> [--status <code>] # \u8BE5\u4EFB\u52A1\u4E0B\u7684\u6267\u884C\uFF08AgentTask\uFF09\uFF1B\u505C/\u91CD\u8DD1\u8981\u5148\u62FF id
8077
+ morula comment list --object <objectId> # \u4EFB\u52A1\u8BC4\u8BBA\uFF08\u5206\u6790\u7ED3\u8BBA / \u8BC4\u5BA1\u610F\u89C1\u90FD\u5728\u8FD9\u91CC\uFF09
8078
+ morula space list # \u53EF\u89C1\u7A7A\u95F4
8079
+ morula space-member list --space <spaceId> # \u7A7A\u95F4\u6210\u5458\uFF08\u627E\u8D1F\u8D23\u4EBA principalId\uFF09
8080
+ morula dashboard get --space <spaceId> # \u6982\u89C8\uFF1A\u4EFB\u52A1\u603B\u6570/\u5B8C\u6210\u7387/\u72B6\u6001\u5206\u5E03/\u6210\u5458\u8D1F\u8377
8081
+ morula dashboard statistics --space <spaceId> # \u6309\u4F18\u5148\u7EA7\u7EDF\u8BA1
8082
+ morula dashboard workload --space <spaceId> # \u6210\u5458\u8D1F\u8377\u660E\u7EC6
8083
+ \`\`\`
8084
+
8085
+ ### \u770B\u6D41\u6C34\u7EBF\uFF08CI/CD\uFF09
8086
+ \`\`\`bash
8087
+ morula ci status --space <spaceId> [--ref <branch>] [--failed] [--limit <n>] # \u672C\u7A7A\u95F4\u4ED3\u5E93\u7684\u6D41\u6C34\u7EBF\u8FD0\u884C\uFF08--failed \u53EA\u770B\u5931\u8D25\u7684\uFF09
8088
+ morula ci deployments --space <spaceId> # \u90E8\u7F72\u8BB0\u5F55\uFF08\u53EA\u8BFB\u5C55\u793A\uFF09
8089
+ morula ci log --space <spaceId> --job <jobId> [--tail <n>] # \u67D0\u4E2A job \u7684\u65E5\u5FD7\uFF08\u670D\u52A1\u7AEF\u5C3E\u90E8\u622A\u65AD\uFF1Btruncated=true \u8868\u793A\u53EA\u7ED9\u4E86\u672B\u5C3E\uFF09
8090
+ morula ci test-report --space <spaceId> [--run <runId>] # \u5931\u8D25\u7528\u4F8B\u6E05\u5355\uFF08\u5E73\u53F0\u4E0D\u652F\u6301\u65F6\u660E\u786E\u62A5\u9519\uFF09
8091
+ morula ci retry --space <spaceId> [--run <runId>] [--job <jobId>] # \u91CD\u8DD1\u5931\u8D25\u6D41\u6C34\u7EBF\uFF08\u7F3A\u7701 = \u6700\u65B0\u4E00\u6761\u5931\u8D25\u8FD0\u884C\uFF09\uFF1B**\u5FC5\u987B\u5148\u53D1\u786E\u8BA4\u5361**
8092
+ \`\`\`
8093
+
8094
+ - **\u53EA\u8BFB + \u91CD\u8DD1**\uFF1A\u4F60\u80FD\u770B\u6D41\u6C34\u7EBF\u4E8B\u5B9E\uFF08\u72B6\u6001 / \u9636\u6BB5 / job / \u8017\u65F6 / \u5931\u8D25\u539F\u56E0 / \u65E5\u5FD7\uFF09\uFF0C\u4E5F\u80FD**\u91CD\u8DD1\u5931\u8D25\u7684\u6D41\u6C34\u7EBF\u6216\u5355\u4E2A job**\uFF08\u8D70\u786E\u8BA4\u5361\uFF09\uFF1B
8095
+ **\u4E0D\u80FD\u89E6\u53D1\u90E8\u7F72**\uFF08\u89E6\u53D1\u624B\u52A8 job \u6C38\u8FDC\u7531\u4EBA\u505A\uFF1A\u4F60\u6CA1\u6709\u8BE5\u547D\u4EE4\uFF0C\u4E5F\u4E0D\u8981\u8BD5\u56FE\u7ED5\u8FC7\uFF09\u3002
8096
+ - **\u91CD\u8DD1\u5FC5\u987B\u5148\u8981\u4EBA\u786E\u8BA4**\uFF1A\u7528**\u7ED3\u6784\u5316\u63D0\u95EE\u5DE5\u5177**\u53D1\u786E\u8BA4\u5361\uFF08\u5199\u6E05\u54EA\u6761\u6D41\u6C34\u7EBF / \u54EA\u4E2A job\u3001\u5931\u8D25\u539F\u56E0\u3001\u4E3A\u4EC0\u4E48\u5EFA\u8BAE\u91CD\u8DD1\uFF09\uFF0C
8097
+ \u62FF\u5230\u7528\u6237\u7B54\u590D\u540E\u518D\u6267\u884C \`morula ci retry\`\u3002\u5B83\u4E0E \`morula task rerun\` \u540C\u6863\uFF08\u300C\u9700\u4EBA\u786E\u8BA4\u300D\uFF09\u3002
8098
+ - **\u786E\u8BA4\u547D\u4EE4\u9762\u7528 \`$MORULA_CLI ci --help\`\uFF08\u6216 \`ci retry --help\`\uFF09\uFF0C\u4E0D\u8981\u7528\u771F\u5B9E\u5199\u547D\u4EE4\u8BD5\u8DD1**\uFF1A
8099
+ \`morula ci retry\` \u6CA1\u6709\u53EA\u8BFB\u9884\u6F14\u5F62\u6001\uFF0C\u6267\u884C\u5C31\u662F\u4E00\u6B21\u771F\u5B9E\u91CD\u8DD1\uFF1B\u53C2\u6570\u5F62\u6001\u4E0D\u786E\u5B9A\u5C31\u770B \`--help\` \u8F93\u51FA\uFF0C\u4E0D\u8981\u62FF\u771F\u5B9E\u8FD0\u884C\u53BB\u8BD5\u3002
8100
+ - \u91CD\u8DD1\u53EA\u5BF9**\u5931\u8D25\u6216\u5DF2\u53D6\u6D88**\u7684\u8FD0\u884C / job \u6709\u6548\uFF1B\u72B6\u6001\u4E0D\u7B26\u4F1A\u660E\u786E\u62A5\u9519\uFF08\`CI_RETRY_NOT_ALLOWED\`\uFF09\uFF0C\u4E0D\u8981\u53CD\u590D\u91CD\u8BD5\u540C\u6837\u7684\u8C03\u7528\u3002
8101
+ - \u91CD\u8DD1\u540E**\u56DE\u8BFB\u4E00\u6B21**\uFF08\`morula ci status --space <id>\`\uFF09\u518D\u6C47\u62A5\u2014\u2014\u300C\u5DF2\u91CD\u8DD1\u300D\u8981\u7528\u72B6\u6001\u8BF4\u8BDD\uFF0C\u4E0D\u80FD\u53EA\u770B\u9000\u51FA\u7801\u3002
8102
+ - \u6D41\u6C34\u7EBF\u72B6\u6001\u662F**\u5E73\u53F0\u4E8B\u5B9E**\uFF08\u6765\u81EA VCS \u5E73\u53F0\u4E8B\u4EF6\uFF09\uFF0C\u4E0D\u662F\u67D0\u4E2A agent \u7684\u81EA\u8FF0\uFF1B\u88C1\u5B9A\u4E0E\u6C47\u62A5\u4EE5\u5B83\u4E3A\u51C6\u3002
8103
+ - \u300C\u5DF2\u53D1\u5E03\u300D\u7531**\u751F\u4EA7\u5206\u652F\u6D41\u6C34\u7EBF\u8DD1\u6210\u529F**\u9A71\u52A8\uFF0C\u90E8\u7F72\u8BB0\u5F55\u53EA\u4F5C\u5C55\u793A\u3001\u4E0D\u9A71\u52A8\u6D41\u8F6C\u3002
8104
+ - \u7A7A\u95F4\u6CA1\u7ED1\u4ED3\u5E93\u3001\u6216\u4ED3\u5E93\u6CA1\u6709 CI \u914D\u7F6E\u65F6\uFF0C\u547D\u4EE4\u8FD4\u56DE\u7A7A\u7ED3\u679C\u5E76\u63D0\u793A\u539F\u56E0\uFF08\`CI_NOT_CONFIGURED\`\uFF09\u2014\u2014\u8FD9\u4E0D\u662F\u6545\u969C\uFF0C\u5982\u5B9E\u8F6C\u8FF0\u3002
8105
+
8106
+ ### \u6D3E\u5355
8107
+ \`\`\`bash
8108
+ morula steward dispatch --space <spaceId> --title "<\u4E00\u53E5\u8BDD\u76EE\u6807>" \\
8109
+ [--description "<\u80CC\u666F/\u52A8\u673A/\u76EE\u6807/\u7EA6\u675F>"] \\
8110
+ [--state backlog|todo|in_progress] \\
8111
+ [--assignee <principalId>] \\
8112
+ [--repo <owner/name>]
8113
+ \`\`\`
8114
+
8115
+ - \`--state todo\`\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u5EFA\u4EFB\u52A1\u5E76\u6D3E\u7ED9\u300C\u5F85\u5206\u6790\u300D\u8FD0\u884C\u65F6\u505A\u9700\u6C42\u5206\u6790\uFF1B\u5206\u6790\u5224\u5B9A\u53EF\u505A\u65F6\u5E73\u53F0**\u81EA\u52A8**\u8FDB\u5165\u5F00\u53D1\u3002
8116
+ - \`--state backlog\`\uFF1A\u53EA\u843D\u300C\u5F85\u89C4\u5212\u300D\uFF0C**\u4E0D\u6D3E\u53D1\u6267\u884C**\uFF1B\u7B49\u4EBA\u6216\u4F60\u540E\u7EED\u7528
8117
+ \`morula task update <objectId> --state todo\` \u63A8\u8FDB\u3002
8118
+ - \`--state in_progress\`\uFF1A\u76F4\u63A5\u8FDB\u300C\u5F00\u53D1\u4E2D\u300D\u5E76\u6D3E\u5F00\u53D1\u8FD0\u884C\u65F6\uFF08\u4EC5\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u8DF3\u8FC7\u5206\u6790\u65F6\u4F7F\u7528\uFF09\u3002
8119
+ - \`--repo\`\uFF1A\u591A\u4ED3\u5E93\u7A7A\u95F4\u91CC\u5FC5\u987B\u5199\u6E05\u6D89\u53CA\u4ED3\u5E93\uFF08\`owner/name\`\uFF09\uFF1B\u5224\u65AD\u4E0D\u51FA\u552F\u4E00\u4ED3\u5E93\u65F6\u7559\u7A7A\u3002
8120
+ - \`--title\` \u7528\u4E00\u53E5\u8BDD\u6982\u62EC**\u9700\u6C42\u76EE\u6807**\uFF08\u4E0D\u662F\u5B9E\u73B0\u52A8\u4F5C\uFF09\uFF1B\`--description\` \u53EA\u5199\u9700\u6C42\u4E0A\u4E0B\u6587
8121
+ \uFF08\u80CC\u666F/\u76EE\u6807/\u7EA6\u675F/\u5173\u8054\u4E8B\u9879\uFF09\uFF0C**\u4E0D\u8981\u5199\u6587\u4EF6\u8DEF\u5F84\u3001\u6539\u52A8\u70B9\u3001\u5B9E\u73B0\u6307\u4EE4**\u2014\u2014\u90A3\u662F\u5206\u6790\u9636\u6BB5\u7684\u4EA7\u51FA\u3002
8122
+
8123
+ ### \u6D3E\u5355\u6D41\u7A0B\uFF08\u5206\u6790\u81EA\u52A8\u8FDB\u5F00\u53D1\uFF09
8124
+
8125
+ 1. \u7528\u6237\u786E\u8BA4\u540E\u6D3E\u5355\u5206\u6790\uFF08\u547D\u4EE4\u89C1\u4E0A\uFF09\uFF0C\u4EFB\u52A1\u4F1A\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\u9636\u6BB5\u3002
8126
+ 2. \u5206\u6790\u5B8C\u6210\u540E**\u5E73\u53F0\u6309\u5206\u6790\u7ED3\u8BBA\u81EA\u52A8\u63A8\u8FDB**\uFF08\u4E0D\u7ECF\u8FC7\u4F60\u3001\u4E5F\u6CA1\u6709\u6309\u94AE\uFF09\uFF1A\u5224\u5B9A\u53EF\u505A \u2192 \u4EFB\u52A1\u81EA\u52A8\u8FDB\u5165\u300C\u5F00\u53D1\u4E2D\u300D\u5E76\u6D3E\u53D1\u5F00\u53D1\u8FD0\u884C\u65F6\uFF1B\u5224\u5B9A\u4E0D\u53EF\u505A/\u9700\u6F84\u6E05 \u2192 \u4EFB\u52A1\u505C\u5728\u300C\u5F85\u5206\u6790\u300D\uFF0C\u7406\u7531\u5199\u5728\u4EFB\u52A1\u8BC4\u8BBA\u91CC\u3002\u4F60**\u4E0D\u9700\u8981**\u5F15\u5BFC\u7528\u6237\u70B9\u4EFB\u4F55\u6309\u94AE\uFF0C\u4E5F\u4E0D\u8981\u66FF\u7528\u6237\u505A\u8FD9\u4E2A\u51B3\u5B9A\u3002
8127
+ 3. \u6D3E\u5355\u524D\u82E5\u9700\u8981\u7528\u6237\u786E\u8BA4\u6216\u8865\u5145\u4FE1\u606F\uFF08\u6807\u9898/\u63CF\u8FF0/\u76EE\u6807\u72B6\u6001/\u4ED3\u5E93\uFF09\uFF0C**\u7528\u7ED3\u6784\u5316\u63D0\u95EE\u5DE5\u5177**\u5411\u7528\u6237\u8981\u8868\u5355\uFF0C\u7528\u6237\u586B\u5B8C\u4F60\u62FF\u5230\u7ED3\u679C\u540E\u518D\u6267\u884C\u6D3E\u5355\uFF1B**\u4E0D\u8981**\u53EA\u5728\u6B63\u6587\u91CC\u95EE\u4E00\u53E5\u5C31\u5F53\u4F5C\u5DF2\u786E\u8BA4\u3002
8128
+
8129
+ ### \u6D3E\u5355\u63CF\u8FF0\u89C4\u8303\uFF08\u4EFB\u52A1\u63CF\u8FF0\u53EA\u5199\u300C\u9700\u6C42\u4E0A\u4E0B\u6587\u300D\uFF0C\u4E0D\u5199\u300C\u600E\u4E48\u505A\u300D\uFF09
8130
+
8131
+ - **\u5199**\uFF1A\u80CC\u666F/\u52A8\u673A\uFF08\u4E3A\u4EC0\u4E48\u505A\uFF09\u3001\u76EE\u6807\u4E0E\u9884\u671F\u6548\u679C\u3001\u7EA6\u675F\u4E0E\u8FB9\u754C\uFF08\u7528\u6237\u660E\u786E\u63D0\u5230\u7684\u9650\u5236\u3001\u5173\u8054\u4E8B\u9879\uFF09\u3001\u4E0A\u4E0B\u6587\u8981\u70B9\uFF08\u76F8\u5173\u5BF9\u8BDD\u7ED3\u8BBA/\u5386\u53F2\uFF09\u3002
8132
+ - **\u4E0D\u5199**\uFF1A\u6587\u4EF6\u8DEF\u5F84\u3001\u6539\u52A8\u70B9\u3001\u300C\u628A X \u6539\u6210 Y\u300D\u300C\u5728 Z \u8FFD\u52A0 W\u300D\u7B49\u5B9E\u73B0\u6307\u4EE4\u4E0E\u65B9\u6848\u2014\u2014\u65B9\u6848\u62C6\u89E3\u3001\u6587\u4EF6\u7EA7\u6539\u52A8\u3001\u9A8C\u6536\u70B9\u7531\u300C\u5F85\u5206\u6790\u8FD0\u884C\u65F6\u300D\u5728\u5206\u6790\u4EFB\u52A1\u4E2D\u4EA7\u51FA\u3002
8133
+ - \u7528\u6237\u539F\u6587\u82E5\u542B\u5B9E\u73B0\u6027\u8868\u8FF0\uFF08\u5982\u300C\u5728 README \u672B\u5C3E\u52A0\u4E00\u884C\u300D\uFF09\uFF1A\u63D0\u70BC\u5176**\u610F\u56FE\u4E0E\u76EE\u6807**\u8FDB\u63CF\u8FF0\uFF08\u5982\u300C\u8865\u5145\u9879\u76EE README \u7684\u672C\u5730\u5F00\u53D1\u6307\u5F15\uFF0C\u8BA9\u65B0\u7528\u6237\u80FD\u5FEB\u901F\u8DD1\u8D77\u6765\u300D\uFF09\uFF0C\u5177\u4F53\u600E\u4E48\u843D\u6587\u4EF6\u7559\u7ED9\u5206\u6790\u3002
8134
+ - **\u6807\u9898\u7528\u4E00\u53E5\u8BDD\u6982\u62EC\u9700\u6C42\u76EE\u6807\uFF08\u4E0D\u662F\u5B9E\u73B0\u52A8\u4F5C\uFF09**\uFF0C\u5982\u300C\u8865\u5145 README \u672C\u5730\u5F00\u53D1\u6307\u5F15\u300D\u800C\u975E\u300C\u5728 README.md \u672B\u5C3E\u8FFD\u52A0\u4E00\u884C\u300D\u3002
8135
+
8136
+ ### \u591A\u4ED3\u5E93\u7A7A\u95F4\uFF08\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u591A\u4E2A\u4ED3\u5E93\u65F6\uFF09
8137
+
8138
+ - \u88C1\u5B9A\u65F6\u53EF\u7ED3\u5408\u4ED3\u5E93\u6E05\u5355\u7ED9\u51FA**\u65B9\u5411\u7EA7**\u5224\u65AD\uFF08\u8FD9\u4E2A\u9700\u6C42\u5927\u6982\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\uFF09\uFF0C\u4F46\u300C\u6BCF\u4E2A\u4ED3\u5E93\u5177\u4F53\u6539\u4EC0\u4E48\u300D\u7531\u5F85\u5206\u6790\u8FD0\u884C\u65F6\u5728\u5206\u6790\u4EFB\u52A1\u4E2D\u4EA7\u51FA\uFF1B\u4E0D\u786E\u5B9A\u5C31\u5199\u300C\u5177\u4F53\u6D89\u53CA\u4ED3\u5E93\u4EE5\u5206\u6790\u7ED3\u8BBA\u4E3A\u51C6\u300D\uFF0C\u4E0D\u8981\u51ED\u7A7A\u65AD\u8A00\u3002
8139
+ - **\u6D3E\u5355\u65F6\u5FC5\u987B\u628A\u6D89\u53CA\u4ED3\u5E93\u4F20\u7ED9 \`--repo\`**\uFF08\u5199\u4ED3\u5E93\u540D\uFF0C\u5982 \`<owner>/<repo>\`\uFF1B**\u540D\u5B57\u53EA\u80FD\u53D6\u81EA\u672C\u7A7A\u95F4\u4ED3\u5E93\u6E05\u5355\u91CC\u5B9E\u9645\u51FA\u73B0\u8FC7\u7684\u4ED3\u5E93\uFF0C\u4E0D\u5F97\u7167\u6284\u793A\u4F8B**\uFF09\uFF1A\u591A\u4ED3\u5E93\u7A7A\u95F4\u91CC\u5B83\u662F\u4EFB\u52A1\u300C\u5173\u8054\u4ED3\u5E93\u300D\u7684\u552F\u4E00\u786E\u5B9A\u6027\u6765\u6E90\uFF0C\u7F3A\u5931\u4F1A\u5BFC\u81F4\u8BE5\u4EFB\u52A1\u4E0D\u521B\u5EFA\u5BF9\u5E94 Issue\u3001\u4E5F\u65E0\u6CD5\u5728\u4EFB\u52A1\u91CC\u770B\u51FA\u8BE5\u6539\u54EA\u4E2A\u4ED3\u5E93\u3002\u5224\u65AD\u4E0D\u51FA\u552F\u4E00\u4ED3\u5E93\u65F6**\u7559\u7A7A**\uFF0C\u4E0D\u8981\u778E\u586B\u3002
8140
+
8141
+ ### \u7BA1\u7406\u4EFB\u52A1\uFF08\u6539\u5C5E\u6027 / \u63A8\u8FDB\u72B6\u6001 / \u5EFA\u5355 / \u5220\u5355\uFF09
8142
+ \`\`\`bash
8143
+ morula task create --space <spaceId> --title "<\u6807\u9898>" [--description "<\u63CF\u8FF0>"] \\
8144
+ [--priority <low|medium|high|urgent>] [--state backlog|todo]
8145
+ morula task update <objectId> [--title <t>] [--description <d>] [--priority <p>] [--assignee <principalId>] [--state <code>]
8146
+ morula task delete <objectId> # \u5220\u9664\u4EFB\u52A1\uFF08\u7834\u574F\u6027\uFF1A\u5148\u8981\u786E\u8BA4\uFF09
8147
+ \`\`\`
8148
+
8149
+ - \`--state\` \u7528\u72B6\u6001 code\uFF08\`backlog\` / \`todo\` / \`in_progress\` / \`review\` / \`merged\` / \`released\` / \`closed\`\uFF09\uFF0C\u4E0D\u8981\u4F20 UUID\u3002
8150
+ - **\u63A8\u8FDB\u5230\u300C\u5F85\u5206\u6790\u300D\u4F1A\u81EA\u52A8\u5F00\u5DE5**\uFF1A\`backlog \u2192 todo\` \u7531\u5E73\u53F0\u81EA\u52A8\u6D3E\u53D1\u5206\u6790\u8FD0\u884C\u65F6\uFF0C\u4E0D\u9700\u8981\u4F60\u518D\u6D3E\u5355\u3002
8151
+ - **\u6539\u8D1F\u8D23\u4EBA\u53EF\u4EE5\u76F4\u63A5\u505A**\uFF08\u4E0D\u662F\u7834\u574F\u6027\u52A8\u4F5C\uFF09\uFF1A\u9636\u6BB5\u72B6\u6001\uFF08\u5F85\u5206\u6790 / \u5F00\u53D1 / \u5F85\u8BC4\u5BA1\uFF09\u7684\u8D1F\u8D23\u4EBA\u53EA\u5E94\u662F\u8BE5\u9636\u6BB5\u7ED1\u5B9A\u7684\u8FD0\u884C\u65F6\uFF1B\u5176\u4F59\u72B6\u6001\u53EA\u6307\u6D3E\u7ED9\u7A7A\u95F4\u771F\u4EBA\u6210\u5458\u3002
8152
+ - \u7528\u6237\u53E3\u8FF0\u7684\u591A\u6761\u9700\u6C42 \u2192 \u5148\u843D \`--state backlog\`\uFF08\u4E0D\u6D3E\u53D1\u6267\u884C\uFF09\uFF0C\u9700\u8981\u5F00\u5DE5\u65F6\u518D\u63A8\u5230 \`todo\`\u3002
8153
+ - \u53EA\u6539\u7528\u6237\u660E\u786E\u8981\u6539\u7684\u5B57\u6BB5\uFF1B**\u6539\u5B8C\u56DE\u8BFB\u4E00\u6B21**\uFF08\`morula task get <objectId>\`\uFF09\u786E\u8BA4\u751F\u6548\u518D\u6C47\u62A5\u3002
8154
+
8155
+ ### \u8BC4\u8BBA\u4E0E @ \u4EBA\uFF08\u50AC\u529E\uFF09
8156
+ \`\`\`bash
8157
+ morula comment add --object <objectId> --content "<\u5185\u5BB9>" [--format markdown] [--parent <commentId>] [--mention <principalId>]...
8158
+ morula comment update <commentId> --content "<\u65B0\u5185\u5BB9>"
8159
+ morula comment delete <commentId>
8160
+ \`\`\`
8161
+
8162
+ - \`--mention <principalId>\` **\u53EA\u7528\u4E8E @ \u4EBA**\uFF08\u50AC\u529E / \u8BF7\u4EBA\u786E\u8BA4\uFF09\uFF0C\u88AB @ \u7684\u4EBA\u4F1A\u6536\u5230\u901A\u77E5\uFF1B\u591A\u4E2A\u53EF\u4EE5**\u91CD\u590D\u4F20**\uFF08\`--mention a --mention b\`\uFF09\u6216**\u9017\u53F7\u5206\u9694**\uFF08\`--mention a,b\`\uFF09\uFF0C\u91CD\u590D\u9879\u81EA\u52A8\u53BB\u91CD\u3002
8163
+ - **\u4E0D\u8981 @ \u8FD0\u884C\u65F6**\uFF08\u89C1 \xA70.2\uFF09\u3002
8164
+ - \u8BC4\u8BBA\u662F\u4EFB\u52A1\u91CC\u7684\u516C\u5F00\u8BB0\u5F55\uFF1A\u5199\u7ED3\u8BBA\u4E0E\u4E0B\u4E00\u6B65\uFF0C\u4E0D\u8981\u5199\u5185\u90E8\u63A2\u7D22\u8FC7\u7A0B\u3002
8165
+
8166
+ ### \u8FD0\u884C\u5E72\u9884\uFF08\u505C\u6389\u5361\u4F4F\u7684\u6267\u884C / \u91CD\u8DD1\u5931\u8D25\u7684\uFF09
8167
+ \`\`\`bash
8168
+ morula task agent-tasks --object <objectId> --status running # \u5148\u627E\u5230\u5728\u8DD1\u7684 AgentTask id
8169
+ morula task cancel <agentTaskId> # \u53D6\u6D88\uFF08\u7834\u574F\u6027\uFF1A\u5148\u8981\u786E\u8BA4\uFF09
8170
+ morula task rerun <agentTaskId> # \u91CD\u8DD1\uFF08\u4EC5 failed / cancelled \u53EF\u91CD\u8DD1\uFF1B\u5148\u8981\u786E\u8BA4\uFF09
8171
+ \`\`\`
8172
+
8173
+ - \u5224\u65AD\u300C\u5361\u4F4F\u300D\u8981\u5148\u770B\u8BC1\u636E\uFF08\`task agent-tasks\` \u7684\u72B6\u6001 + \u4EFB\u52A1\u8BC4\u8BBA\u91CC\u7684\u6700\u8FD1\u8FDB\u5C55\uFF09\uFF0C\u518D\u51B3\u5B9A\u505C\u6216\u91CD\u8DD1\uFF1B\u4E0D\u8981\u51ED\u611F\u89C9\u505C\u522B\u4EBA\u7684\u6D3B\u3002
8174
+ - **\u5931\u8D25\u4F1A\u660E\u786E\u62A5\u9519**\uFF082026-09-20 \u8D77\uFF09\uFF1A\u4EFB\u52A1\u4E0D\u5B58\u5728 \u2192 \u9000\u51FA\u7801 3 / \`NOT_FOUND\`\uFF1B\u72B6\u6001\u4E0D\u5141\u8BB8\uFF08\u5982\u5DF2\u5B8C\u6210\uFF09\u2192 \u9000\u51FA\u7801 6 / \`CONFLICT\`\u3002\u770B\u5230\u975E\u96F6\u9000\u51FA\u5C31\u662F**\u6CA1\u6267\u884C\u6210\u529F**\uFF0C\u4E0D\u8981\u5411\u7528\u6237\u6C47\u62A5\u300C\u5DF2\u505C\u6389/\u5DF2\u91CD\u8DD1\u300D\u3002
8175
+ - \u505C/\u91CD\u8DD1\u540E\u56DE\u8BFB\u4E00\u6B21\u72B6\u6001\u786E\u8BA4\u7ED3\u679C\uFF0C\u518D\u5411\u7528\u6237\u6C47\u62A5\u3002
8176
+
8177
+ ## 2. \u88C1\u5B9A\u8F93\u51FA\u683C\u5F0F\uFF08\u524D\u7AEF\u6309\u6B64\u6E32\u67D3\uFF0C\u5FC5\u987B\u9075\u5B88\uFF09
8178
+
8179
+ \u56DE\u590D\u7528\u6237\u65F6\uFF0C\u7ED3\u8BBA\u884C\u5FC5\u987B\u662F\u4E0B\u5217\u56DB\u8005\u4E4B\u4E00\uFF0C\u5355\u72EC\u6210\u884C\uFF1A
8180
+
8181
+ - \`\u7ED3\u8BBA\uFF1A\u2705 \u5EFA\u8BAE\u505A\`
8182
+ - \`\u7ED3\u8BBA\uFF1A\u274C \u4E0D\u5EFA\u8BAE\u505A\`
8183
+ - \`\u7ED3\u8BBA\uFF1A\u23F8 \u6682\u7F13\`
8184
+ - \`\u7ED3\u8BBA\uFF1A\u2753 \u9700\u6F84\u6E05\`
8185
+
8186
+ \u968F\u540E\u7ED9\u300C\u7406\u7531\u300D\uFF08\u65B9\u5411\u7EA7\uFF1A\u7A7A\u95F4\u73B0\u72B6 / \u4EFB\u52A1\u73B0\u72B6 / \u6210\u672C\u6536\u76CA\uFF09\uFF0C\u300C\u5EFA\u8BAE\u505A\u300D\u65F6\u518D\u7ED9\u65B9\u5411\u7EA7\u8303\u56F4
8187
+ \uFF08\u5927\u81F4\u6D89\u53CA\u4EC0\u4E48\u3001\u9884\u671F\u6548\u679C\uFF09\uFF0C**\u4E0D\u8981**\u7ED9\u6587\u4EF6\u7EA7\u6539\u52A8\u6E05\u5355\u3002
8188
+
8189
+ ## 3. \u4E8B\u5B9E\u7EAA\u5F8B\u4E0E\u8FDB\u5C55\u540C\u6B65
8190
+
8191
+ - **\u4EFB\u52A1\u7F16\u53F7\uFF08\u5982 SPW299-28\uFF09\u53EA\u80FD\u53D6\u81EA\u547D\u4EE4\u8FD4\u56DE\u503C**\uFF08\`morula steward dispatch\` / \`morula task list\` \u8FD4\u56DE\u7684 \`code\`\uFF09\u3002
8192
+ \u8FD4\u56DE\u503C\u91CC\u6CA1\u6709\u7F16\u53F7\u5C31\u4E0D\u8981\u5199\u7F16\u53F7\uFF0C**\u4E0D\u5F97\u81EA\u884C\u7F16\u9020**\u3002
8193
+ - \u62A5\u8FDB\u5C55\u524D\u5148\u67E5\uFF08\`morula task list\` / \`morula task get\` / \`morula comment list\`\uFF09\uFF0C\u4E0D\u8981\u51ED\u8BB0\u5FC6\u65AD\u8A00\u3002
8194
+ - **\u5199\u64CD\u4F5C\u540E\u56DE\u8BFB**\uFF1A\u6539\u5C5E\u6027 / \u63A8\u8FDB\u72B6\u6001 / \u505C\u91CD\u8DD1\u4E4B\u540E\uFF0C\u7528\u67E5\u8BE2\u547D\u4EE4\u786E\u8BA4\u7ED3\u679C\uFF0C\u518D\u5411\u7528\u6237\u6C47\u62A5\u300C\u5DF2\u751F\u6548\u300D\u3002
8195
+ - \u547D\u4EE4\u8F93\u51FA\u662F stdout \u4E0A\u7684 JSON\uFF1B\u51FA\u9519\u65F6\u770B stderr \u7684 \`code\` / \`message\` \u5B57\u6BB5\u5E76\u636E\u6B64\u8C03\u6574\uFF0C\u4E0D\u8981\u91CD\u8BD5\u540C\u6837\u7684\u8C03\u7528\u3002
8196
+ \uFF08\u5199\u64CD\u4F5C\u62A5 \`FORBIDDEN\` \u4E14\u6587\u6848\u662F\u300C\u4F1A\u8BDD\u51ED\u8BC1\u4E0D\u80FD\u6267\u884C\u8BE5\u5199\u64CD\u4F5C\u300D= \u8FD9\u6761\u80FD\u529B\u4E0D\u5728\u7BA1\u5BB6\u6388\u6743\u8303\u56F4\u5185\uFF08\u4F1A\u8BDD\u4F5C\u7528\u57DF\u9650\u5236\uFF09\uFF1A**\u4E0D\u8981\u7ED5\u9053\u91CD\u8BD5**\uFF0C\u76F4\u63A5\u544A\u8BC9\u7528\u6237\u8BE5\u80FD\u529B\u672A\u5F00\u653E\u3002\uFF09
8197
+ - \u5E73\u53F0**\u4E0D\u518D**\u5411\u4F1A\u8BDD\u81EA\u52A8\u63A8\u9001\u6C47\u62A5\u6D88\u606F\u3002\u7528\u6237\u95EE\u8FDB\u5C55\u65F6\uFF0C\u7528 CLI \u67E5\u8BE2\u540E\u56DE\u7B54\uFF1A\u4EFB\u52A1\u72B6\u6001\u770B\u677F\u5B9E\u65F6\u53EF\u89C1\uFF0C
8198
+ \u5206\u6790\u7ED3\u8BBA\u4E0E\u8BC4\u5BA1\u610F\u89C1\u5728\u4EFB\u52A1\u8BC4\u8BBA\u91CC\uFF0C\u4EA4\u4ED8\u7ED3\u679C\u5728\u4EFB\u52A1\u7684\u300C\u5173\u8054 PR / Commit\u300D\u533A\u3002
8199
+ `;
8200
+ function buildStewardSkillFile() {
8201
+ return `${STEWARD_SKILL_FRONTMATTER}
8202
+ ${STEWARD_SKILL_BODY}`;
8203
+ }
8204
+
8205
+ // src/daemon/session-host.ts
8206
+ var SESSION_IDLE_DEFAULT_MS = 30 * 60 * 1e3;
8207
+ var ELICITATION_DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
8208
+ var ATTACHMENT_FETCH_TIMEOUT_MS = 3e4;
8209
+ function safeAttachmentName(fileName) {
8210
+ const base = (fileName || "attachment").split(/[\\/]/).pop() ?? "attachment";
8211
+ const cleaned = base.replace(/[\u0000-\u001F<>:"|?*]/g, "_").replace(/^\.+/, "_").trim();
8212
+ return cleaned.slice(0, 120) || "attachment";
8213
+ }
8214
+ function resolveSessionIdleMs(env = process.env) {
8215
+ const raw = Number(env.MORULA_SESSION_IDLE_MS);
8216
+ return Number.isFinite(raw) && raw > 0 ? raw : SESSION_IDLE_DEFAULT_MS;
8217
+ }
8218
+ function resolveElicitationTimeoutMs(env = process.env) {
8219
+ const raw = Number(env.MORULA_DIM_APPROVAL_TIMEOUT_MS);
8220
+ return Number.isFinite(raw) && raw > 0 ? raw : ELICITATION_DEFAULT_TIMEOUT_MS;
8221
+ }
8222
+ var ELICITATION_POLL_WAIT_MS = 15e3;
8223
+ var SessionHost = class {
8224
+ constructor(options) {
8225
+ this.options = options;
8226
+ this.log = options.log ?? ((line) => console.log(line));
8227
+ this.idleMs = resolveSessionIdleMs();
8228
+ }
8229
+ options;
8230
+ sessions = /* @__PURE__ */ new Map();
8231
+ log;
8232
+ idleMs;
8233
+ /** 当前打开的会话(morula AgentSession.id 列表;诊断/测试用) */
8234
+ get openSessionIds() {
8235
+ return [...this.sessions.keys()];
8236
+ }
8237
+ /**
8238
+ * 打开会话:建/复用工作区 → 起**独立** dim acp 进程 → session/new(或 resume 续跑)
8239
+ * → 应用配置 → 指针回传。工作区保留(close 不清理)。
8240
+ *
8241
+ * **幂等**:已打开且进程存活 → 只刷新凭证/注入物与变化过的配置,**不重建进程**
8242
+ * (服务端每轮 prompt 前都会补发 open 做兜底:daemon 可能重启或空闲回收过,
8243
+ * 用同一条命令覆盖「首次打开」与「兜底确认」两种语义,避免两端各维护一套「是否已打开」状态)。
8244
+ */
8245
+ async open(cmd) {
8246
+ if (!cmd?.sessionId || !cmd?.tenantId || !cmd?.projectId)
8247
+ return { ok: false, error: "SESSION_OPEN_INVALID_ARGS" };
8248
+ const workspaceDir = join19(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8249
+ mkdirSync12(workspaceDir, { recursive: true });
8250
+ this.writeInjections(workspaceDir, cmd);
8251
+ const existing = this.sessions.get(cmd.sessionId);
8252
+ if (existing?.conn.isAlive) {
8253
+ if (cmd.sessionToken) {
8254
+ this.options.api.setSessionToken(cmd.sessionId, cmd.sessionToken);
8255
+ existing.env.MORULA_SESSION_TOKEN = cmd.sessionToken;
8256
+ }
8257
+ this.touch(existing);
8258
+ const changed = this.diffConfig(existing.appliedConfig, this.configFrom(cmd));
8259
+ for (const [configId, value] of Object.entries(changed)) {
8260
+ await existing.conn.setConfigOptionFull(existing.dimSessionId, configId, value).then((raw) => {
8261
+ const full = normalizeAcpConfigOptions(raw) ?? [];
8262
+ if (full.length > 0) {
8263
+ void this.options.api.reportSessionBinding(cmd.sessionId, { configOptions: full }).catch(() => {
8264
+ });
8265
+ }
8266
+ }).catch((err) => this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u914D\u7F6E ${configId} \u589E\u91CF\u4E0B\u53D1\u5931\u8D25: ${err.message}`));
8267
+ }
8268
+ existing.appliedConfig = { ...existing.appliedConfig, ...changed };
8269
+ return { ok: true, dimSessionId: existing.dimSessionId, workspaceDir: existing.workspaceDir, alreadyOpen: true };
8270
+ }
8271
+ if (existing)
8272
+ await this.close({ sessionId: cmd.sessionId }, "reopen");
8273
+ if (cmd.sessionToken)
8274
+ this.options.api.setSessionToken(cmd.sessionId, cmd.sessionToken);
8275
+ const env = {
8276
+ ...cmd.sessionToken ? { MORULA_SESSION_TOKEN: cmd.sessionToken } : {},
8277
+ ...this.options.baseUrl ? { MORULA_API_URL: this.options.baseUrl } : {},
8278
+ // 会话上下文(对齐任务链路的 MORULA_TASK_ID):让 `morula` CLI 知道「我在哪个会话里」,
8279
+ // 派单幂等与 elicitation 回传需要它;管家会话**没有** MORULA_TASK_ID(任务专属命令会因此拒绝执行)
8280
+ MORULA_SESSION_ID: cmd.sessionId,
8281
+ MORULA_SPACE_ID: cmd.projectId,
8282
+ // 插件 bin 前置 + 显式 CLI 路径:否则 agent 会解析到宿主/桌面应用自带的旧 `morula`——
8283
+ // 新命令不存在,且旧 CLI 不认 MORULA_SESSION_TOKEN(回落用户凭证)→ 写操作署名错、
8284
+ // 写面作用域校验被绕过。PATH 只是第一道保险(dim 会重写 agent shell 的 PATH),
8285
+ // 可靠的是 `MORULA_CLI` 绝对路径(契约要求 agent 用它)。
8286
+ // MORULA_CLI 优先本包自带入口(与当前会话同源),产物缺失才回退插件启动器。
8287
+ // 见 issues/steward-cli-resolves-stale-bundled-morula.md、issues/agent-cli-stale-build-missing-steward-commands.md。
8288
+ PATH: prependPluginBinToPath(process.env.PATH),
8289
+ MORULA_CLI: resolveAgentCliPath(),
8290
+ ...this.options.isolatedDimEnv ?? {}
8291
+ };
8292
+ const onExit = (code) => {
8293
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u7684 dim acp \u8FDB\u7A0B\u9000\u51FA\uFF08code=${code}\uFF09`);
8294
+ const managed2 = this.sessions.get(cmd.sessionId);
8295
+ if (managed2 && managed2.conn === conn) {
8296
+ this.sessions.delete(cmd.sessionId);
8297
+ this.options.api.clearSessionToken(cmd.sessionId);
8298
+ }
8299
+ };
8300
+ const connOptions = {
8301
+ dimPath: this.options.dimPath,
8302
+ cwd: workspaceDir,
8303
+ // process.env 值域含 undefined(ProcessEnv),显式收敛为 string 索引(spawn env 语义)
8304
+ env: { ...process.env, ...env },
8305
+ // P1-7:审批默认全自动放行(管家人在环收敛到 elicitation 一处,见 §4.7 取舍)。
8306
+ // 注意:dim 的权限引擎决定 allow/deny/ask,client 无法强制——`permission` 只选 preset。
8307
+ onPermissionRequest: async () => ({ outcome: { outcome: "selected", optionId: "allow" } }),
8308
+ onElicitation: async (request) => this.handleElicitation(cmd.sessionId, request),
8309
+ onExit
8310
+ };
8311
+ const conn = this.options.acpConnectionFactory ? this.options.acpConnectionFactory(connOptions) : new AcpConnection(connOptions);
8312
+ try {
8313
+ await conn.start();
8314
+ } catch (err) {
8315
+ return { ok: false, error: `dim acp \u542F\u52A8\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}` };
8316
+ }
8317
+ const sessionParams = { cwd: workspaceDir, mcpServers: [] };
8318
+ let dimSessionId;
8319
+ let resumed = false;
8320
+ let resumeFailed = null;
8321
+ let configOptions;
8322
+ if (cmd.dimSessionId) {
8323
+ try {
8324
+ const r = await conn.resumeSession({ ...sessionParams, sessionId: cmd.dimSessionId });
8325
+ configOptions = normalizeAcpConfigOptions(r.configOptions) ?? void 0;
8326
+ dimSessionId = cmd.dimSessionId;
8327
+ resumed = true;
8328
+ } catch {
8329
+ try {
8330
+ const r = await conn.loadSession({ ...sessionParams, sessionId: cmd.dimSessionId });
8331
+ configOptions = normalizeAcpConfigOptions(r.configOptions) ?? void 0;
8332
+ dimSessionId = cmd.dimSessionId;
8333
+ resumed = true;
8334
+ } catch (err) {
8335
+ resumeFailed = err instanceof Error ? err.message : String(err);
8336
+ const created = await conn.newSession(sessionParams);
8337
+ configOptions = normalizeAcpConfigOptions(created.configOptions) ?? void 0;
8338
+ dimSessionId = created.sessionId;
8339
+ }
8340
+ }
8341
+ } else {
8342
+ const created = await conn.newSession(sessionParams);
8343
+ configOptions = normalizeAcpConfigOptions(created.configOptions) ?? void 0;
8344
+ dimSessionId = created.sessionId;
8345
+ }
8346
+ const appliedConfig = this.configFrom(cmd);
8347
+ const permission = appliedConfig.permission || "workspace-write";
8348
+ const applyConfig = async (configId, value) => {
8349
+ try {
8350
+ const full = normalizeAcpConfigOptions(await conn.setConfigOptionFull(dimSessionId, configId, value)) ?? [];
8351
+ if (full.length > 0)
8352
+ configOptions = full;
8353
+ } catch (err) {
8354
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u914D\u7F6E ${configId}=${value} \u4E0B\u53D1\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8355
+ }
8356
+ };
8357
+ await applyConfig("permission", permission);
8358
+ if (appliedConfig.modelId)
8359
+ await applyConfig("model", appliedConfig.modelId);
8360
+ if (appliedConfig.thoughtLevel)
8361
+ await applyConfig("thought_level", appliedConfig.thoughtLevel);
8362
+ if (appliedConfig.agentMode)
8363
+ await applyConfig("mode", appliedConfig.agentMode);
8364
+ void this.options.api.reportSessionBinding(cmd.sessionId, {
8365
+ dimSessionId,
8366
+ workspaceDir,
8367
+ ...cmd.runtimeId ? { runtimeId: cmd.runtimeId } : {},
8368
+ ...cmd.providerKey ? { providerKey: cmd.providerKey } : {},
8369
+ ...configOptions !== void 0 ? { configOptions } : {}
8370
+ }).catch((err) => {
8371
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u6307\u9488\u56DE\u4F20\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8372
+ });
8373
+ const managed = {
8374
+ sessionId: cmd.sessionId,
8375
+ conn,
8376
+ dimSessionId,
8377
+ workspaceDir,
8378
+ env,
8379
+ turnNo: 0,
8380
+ promptInFlight: null,
8381
+ cancelRequested: false,
8382
+ // resume 成功 → dim 会话里已有上一进程注入的管家指令,不重复前置
8383
+ instructed: resumed,
8384
+ appliedConfig: { ...appliedConfig, permission },
8385
+ idleTimer: setTimeout(() => {
8386
+ void this.close({ sessionId: cmd.sessionId }, "idle");
8387
+ }, this.idleMs),
8388
+ lastActiveAt: Date.now()
8389
+ };
8390
+ managed.idleTimer.unref?.();
8391
+ this.sessions.set(cmd.sessionId, managed);
8392
+ this.log(`[session-host] \u4F1A\u8BDD\u5DF2\u6253\u5F00 ${cmd.sessionId}\uFF08dim=${dimSessionId}\uFF0Cworkspace=${workspaceDir}${resumeFailed ? `\uFF0C\u7EED\u8DD1\u5931\u8D25\u5DF2\u65B0\u5EFA\uFF1A${resumeFailed}` : ""}\uFF09`);
8393
+ return { ok: true, dimSessionId, workspaceDir };
8394
+ }
8395
+ /**
8396
+ * 送一轮 prompt(同一会话单活跃——ACP 硬约束;忙时返回 SESSION_BUSY 不排队,
8397
+ * 排队语义由服务端消息投递状态机承载,见 §4.2)。
8398
+ * 事件映射复用任务链路同款 mapper + normalizer(含 P0-8 修正:user_message / usage 上下文语义)。
8399
+ *
8400
+ * **管家指令前置**(§4.4):新建 dim 会话的**首条** prompt 前置 `STEWARD_SYSTEM_INSTRUCTIONS`
8401
+ * (ACP 无 system prompt 通道)。resume 的会话里已有,不重复注入。
8402
+ * 该常量只含身份/边界/目标/工作区与工具入口(提示词极简,见 steward-instructions.ts 文件头);
8403
+ * 行为规范与命令用法在 `.agents/skills/steward-management/SKILL.md`,由 agent 按需读取。
8404
+ */
8405
+ async prompt(cmd) {
8406
+ const managed = this.sessions.get(cmd.sessionId);
8407
+ if (!managed)
8408
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8409
+ if (managed.promptInFlight)
8410
+ return { ok: false, error: "SESSION_BUSY" };
8411
+ if (!cmd.prompt?.trim())
8412
+ return { ok: false, error: "SESSION_PROMPT_EMPTY" };
8413
+ managed.turnNo += 1;
8414
+ managed.cancelRequested = false;
8415
+ this.touch(managed);
8416
+ const turnNo = managed.turnNo;
8417
+ const mapper = new AcpEventMapper();
8418
+ let seqHint = 0;
8419
+ const nextSeq = () => ++seqHint;
8420
+ const textStream = new AssistantTextStream({
8421
+ onDelta: (seg, delta) => {
8422
+ void this.options.api.recordSessionStreamChunk(managed.sessionId, {
8423
+ chunkType: "delta",
8424
+ data: { kind: "text", seg, text: delta }
8425
+ }).catch(() => {
8426
+ });
8427
+ },
8428
+ onParagraph: (seg, text2) => this.options.api.recordSessionEvent(managed.sessionId, {
8429
+ type: "agent_message_delta",
8430
+ output: { text: redactValue(text2), seg },
8431
+ turnNo,
8432
+ seqHint: nextSeq()
8433
+ }).then(() => void 0).catch((err) => {
8434
+ this.log(`[session-host] \u4F1A\u8BDD ${managed.sessionId} \u6BB5\u843D\u4E8B\u4EF6\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8435
+ })
8436
+ }, { log: this.log });
8437
+ const thinkingStream = new AssistantTextStream({
8438
+ onDelta: (seg, delta) => {
8439
+ void this.options.api.recordSessionStreamChunk(managed.sessionId, {
8440
+ chunkType: "delta",
8441
+ data: { kind: "thinking", seg, text: delta }
8442
+ }).catch(() => {
8443
+ });
8444
+ },
8445
+ onParagraph: (seg, text2) => this.options.api.recordSessionEvent(managed.sessionId, {
8446
+ type: "agent_thought_delta",
8447
+ output: { text: redactValue(text2), seg },
8448
+ turnNo,
8449
+ seqHint: nextSeq()
8450
+ }).then(() => void 0).catch((err) => {
8451
+ this.log(`[session-host] \u4F1A\u8BDD ${managed.sessionId} \u601D\u8003\u6BB5\u843D\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8452
+ })
8453
+ }, { log: this.log });
8454
+ let textChain = Promise.resolve();
8455
+ let thinkingPending = false;
8456
+ const gateThinking = () => {
8457
+ if (thinkingPending) {
8458
+ thinkingPending = false;
8459
+ textChain = textChain.then(() => thinkingStream.flushParagraph());
8460
+ }
8461
+ return textChain;
8462
+ };
8463
+ const onEvent = (event) => {
8464
+ this.touch(managed);
8465
+ if (event.type === "assistant_thinking_delta") {
8466
+ thinkingPending = true;
8467
+ thinkingStream.push(event.text);
8468
+ } else if (event.type === "assistant_text_delta") {
8469
+ void gateThinking().then(() => textStream.push(event.text));
8470
+ } else if (event.type === "tool_call_started") {
8471
+ void gateThinking().then(() => textStream.flushParagraph()).then(() => this.options.api.recordSessionEvent(managed.sessionId, {
8472
+ type: "tool_call_started",
8473
+ toolName: event.toolName,
8474
+ callId: event.callId,
8475
+ input: event.input !== void 0 ? redactInputSummary(event.input) : void 0,
8476
+ output: void 0,
8477
+ turnNo,
8478
+ seqHint: nextSeq()
8479
+ })).catch(() => {
8480
+ });
8481
+ } else if (event.type === "tool_call_completed") {
8482
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8483
+ type: "tool_call_completed",
8484
+ callId: event.callId,
8485
+ toolName: event.toolName,
8486
+ output: {
8487
+ callId: event.callId,
8488
+ toolName: event.toolName,
8489
+ status: event.aborted ? "aborted" : "completed",
8490
+ ...event.outputSummary !== void 0 ? { summary: String(redactValue(String(event.outputSummary).slice(0, 200))) } : {}
8491
+ },
8492
+ turnNo,
8493
+ seqHint: nextSeq()
8494
+ }).catch(() => {
8495
+ });
8496
+ } else if (event.type === "user_message") {
8497
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8498
+ type: "user_message",
8499
+ output: { text: event.text },
8500
+ turnNo
8501
+ }).catch(() => {
8502
+ });
8503
+ } else if (event.type === "usage") {
8504
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8505
+ type: "usage",
8506
+ output: event.usage,
8507
+ turnNo
8508
+ }).catch(() => {
8509
+ });
8510
+ } else if (event.type === "session" && event.providerSessionId) {
8511
+ void this.options.api.reportSessionBinding(managed.sessionId, { dimSessionId: event.providerSessionId }).catch(() => {
8512
+ });
8513
+ }
8514
+ };
8515
+ const normalizer = createDimEventNormalizer({ onEvent });
8516
+ const updateHandler = (update) => {
8517
+ const { messages } = mapper.map(managed.dimSessionId, update);
8518
+ for (const message of messages)
8519
+ normalizer.handle(message, "");
8520
+ };
8521
+ const text = managed.instructed ? cmd.prompt : `${STEWARD_SYSTEM_INSTRUCTIONS}
8522
+
8523
+ ---
8524
+
8525
+ ${cmd.prompt}`;
8526
+ const run3 = (async () => {
8527
+ try {
8528
+ const { blocks, fileRefs } = await this.materializeAttachments(managed.workspaceDir, cmd.attachments ?? []);
8529
+ const promptBlocks = [{ type: "text", text: fileRefs.length > 0 ? `${text}
8530
+
8531
+ ---
8532
+
8533
+ \u672C\u8F6E\u9644\u4EF6\u5DF2\u4E0B\u8F7D\u5230\u4F1A\u8BDD\u5DE5\u4F5C\u533A\uFF08\u7528 read/exec \u5DE5\u5177\u6309\u8DEF\u5F84\u8BFB\u53D6\uFF0C\u4E0D\u8981\u91CD\u590D\u4E0B\u8F7D\uFF09\uFF1A
8534
+ ${fileRefs.join("\n")}` : text }];
8535
+ promptBlocks.push(...blocks);
8536
+ const pending = managed.conn.prompt(managed.dimSessionId, promptBlocks, updateHandler);
8537
+ managed.instructed = true;
8538
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8539
+ type: "turn_started",
8540
+ output: { ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8541
+ turnNo
8542
+ }).catch(() => {
8543
+ });
8544
+ const result = await pending;
8545
+ await gateThinking();
8546
+ await textStream.flushParagraph();
8547
+ normalizer.finish({ eventType: "run:ended", payload: { stopReason: result.stopReason } }, "");
8548
+ await this.options.api.recordSessionEvent(managed.sessionId, {
8549
+ type: "turn_completed",
8550
+ output: { stopReason: result.stopReason, ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8551
+ turnNo
8552
+ }).catch(() => {
8553
+ });
8554
+ return { ok: true, stopReason: result.stopReason };
8555
+ } catch (err) {
8556
+ await gateThinking();
8557
+ await textStream.flushParagraph();
8558
+ const message = err instanceof Error ? err.message : String(err);
8559
+ await this.options.api.recordSessionEvent(managed.sessionId, {
8560
+ type: "turn_failed",
8561
+ output: { error: message, ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8562
+ turnNo
8563
+ }).catch(() => {
8564
+ });
8565
+ return { ok: false, error: message };
8566
+ } finally {
8567
+ textStream.dispose();
8568
+ thinkingStream.dispose();
8569
+ managed.promptInFlight = null;
8570
+ this.touch(managed);
8571
+ }
8572
+ })();
8573
+ managed.promptInFlight = run3;
8574
+ return run3;
8575
+ }
8576
+ /**
8577
+ * 附件物化(§五 P2-3):
8578
+ * - **图片** → ACP `image` content block(base64 + mimeType),直接进模型上下文;
8579
+ * - **其他文件** → 下载到 `<workspace>/attachments/<安全文件名>`,返回路径引用文本
8580
+ * (doc 明确「文件走工作区文件 + 路径引用」:不进上下文,由 agent 按需读取)。
8581
+ *
8582
+ * 下载失败不抛:记日志并跳过该附件(本轮照常发出)。
8583
+ */
8584
+ async materializeAttachments(workspaceDir, attachments) {
8585
+ const blocks = [];
8586
+ const fileRefs = [];
8587
+ if (attachments.length === 0)
8588
+ return { blocks, fileRefs };
8589
+ const dir = join19(workspaceDir, "attachments");
8590
+ for (const attachment of attachments) {
8591
+ try {
8592
+ const res = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
8593
+ if (!res.ok)
8594
+ throw new Error(`HTTP ${res.status}`);
8595
+ const bytes = Buffer.from(await res.arrayBuffer());
8596
+ if (attachment.isImage) {
8597
+ blocks.push({ type: "image", data: bytes.toString("base64"), mimeType: attachment.fileType || "image/png" });
8598
+ } else {
8599
+ mkdirSync12(dir, { recursive: true });
8600
+ const target = join19(dir, safeAttachmentName(attachment.fileName));
8601
+ writeFileSync10(target, bytes);
8602
+ fileRefs.push(`- ${attachment.fileName} \u2192 ${target}`);
8603
+ }
8604
+ } catch (err) {
8605
+ this.log(`[session-host] \u9644\u4EF6 ${attachment.id}\uFF08${attachment.fileName}\uFF09\u62C9\u53D6\u5931\u8D25\uFF0C\u672C\u8F6E\u5DF2\u8DF3\u8FC7: ${err instanceof Error ? err.message : String(err)}`);
8606
+ }
8607
+ }
8608
+ return { blocks, fileRefs };
8609
+ }
8610
+ /** 取消当前回合(ACP session/cancel;会话保持打开——长驻语义) */
8611
+ cancel(cmd) {
8612
+ const managed = this.sessions.get(cmd.sessionId);
8613
+ if (!managed)
8614
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8615
+ managed.cancelRequested = true;
8616
+ managed.conn.cancel(managed.dimSessionId);
8617
+ this.touch(managed);
8618
+ return { ok: true };
8619
+ }
8620
+ /**
8621
+ * 配置变更(configId ∈ model / thought_level / mode / permission)。
8622
+ * ACP `set_config_option` 全量返回 configOptions——回写由服务端(配置端点)消费,
8623
+ * 这里原样返回给调用方。
8624
+ */
8625
+ async config(cmd) {
8626
+ const managed = this.sessions.get(cmd.sessionId);
8627
+ if (!managed)
8628
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8629
+ this.touch(managed);
8630
+ try {
8631
+ const result = normalizeAcpConfigOptions(await managed.conn.setConfigOptionFull(managed.dimSessionId, cmd.configId, cmd.value)) ?? [];
8632
+ managed.appliedConfig = { ...managed.appliedConfig, [this.configKey(cmd.configId)]: cmd.value };
8633
+ if (result.length > 0) {
8634
+ void this.options.api.reportSessionBinding(cmd.sessionId, { configOptions: result }).catch((err) => this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} configOptions \u56DE\u4F20\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`));
8635
+ }
8636
+ return { ok: true, configOptions: result };
8637
+ } catch (err) {
8638
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
8639
+ }
8640
+ }
8641
+ /**
8642
+ * 关闭会话:ACP session/close → 进程回收 → 服务端上报。
8643
+ * **工作区保留**(steward/<tenant>/<project> 可复用;清理属服务端清理命令域)。
8644
+ *
8645
+ * @param reason `idle` = 空闲回收(服务端回到 idle,保留 dimSessionId 以便 resume);
8646
+ * `command` = 用户/平台显式关闭(服务端终态 closed);
8647
+ * `reopen` = daemon 内部重建(**不上报**,紧接着的 open 会重新绑定)。
8648
+ */
8649
+ async close(cmd, reason = "command") {
8650
+ const managed = this.sessions.get(cmd.sessionId);
8651
+ if (!managed)
8652
+ return { ok: true };
8653
+ this.sessions.delete(cmd.sessionId);
8654
+ clearTimeout(managed.idleTimer);
8655
+ try {
8656
+ await managed.conn.close();
8657
+ } catch {
8658
+ }
8659
+ if (reason === "reopen") {
8660
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u8FDE\u63A5\u91CD\u5EFA\uFF08\u5DE5\u4F5C\u533A\u4FDD\u7559 ${managed.workspaceDir}\uFF09`);
8661
+ return { ok: true };
8662
+ }
8663
+ this.options.api.clearSessionToken(cmd.sessionId);
8664
+ void this.options.api.closeAgentSession(cmd.sessionId, reason).catch((err) => {
8665
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u5173\u95ED\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8666
+ });
8667
+ this.log(`[session-host] \u4F1A\u8BDD\u5DF2\u5173\u95ED ${cmd.sessionId}\uFF08\u539F\u56E0=${reason}\uFF1B\u5DE5\u4F5C\u533A\u4FDD\u7559 ${managed.workspaceDir}\uFF09`);
8668
+ return { ok: true };
8669
+ }
8670
+ /**
8671
+ * 关闭全部会话(daemon 停机时收尾)。
8672
+ * 用 `idle` 而非 `command`:daemon 停机不是用户关闭会话,dim 会话应当可 resume
8673
+ * (否则服务端会把会话打成终态 `closed`,下次上线再也接不回来)。
8674
+ */
8675
+ async closeAll() {
8676
+ for (const sessionId of [...this.sessions.keys()])
8677
+ await this.close({ sessionId }, "idle").catch(() => {
8678
+ });
8679
+ }
8680
+ // ────────────────────────── 内部 ──────────────────────────
8681
+ /** 活跃信号:刷新空闲回收计时(事件/prompt/取消都算活跃)。定时器 unref——回收器不应单独钉住进程(daemon 有 WS/心跳常驻句柄) */
8682
+ touch(managed) {
8683
+ managed.lastActiveAt = Date.now();
8684
+ clearTimeout(managed.idleTimer);
8685
+ managed.idleTimer = setTimeout(() => {
8686
+ void this.close({ sessionId: managed.sessionId }, "idle");
8687
+ }, this.idleMs);
8688
+ managed.idleTimer.unref?.();
8689
+ }
8690
+ /** 工作区注入物(幂等覆盖;daemon 侧组装,P1-6) */
8691
+ writeInjections(workspaceDir, cmd) {
8692
+ writeProjectAgentsMd(workspaceDir, cmd.agentsMd, this.log);
8693
+ const skillDir = join19(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
8694
+ mkdirSync12(skillDir, { recursive: true });
8695
+ writeFileSync10(join19(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
8696
+ const contextDir = join19(workspaceDir, ".agent_context");
8697
+ mkdirSync12(contextDir, { recursive: true });
8698
+ writeFileSync10(
8699
+ join19(contextDir, "steward-context.md"),
8700
+ buildStewardContextMd(cmd.facts ?? { projectId: cmd.projectId, tenantId: cmd.tenantId }),
8701
+ "utf8"
8702
+ );
8703
+ }
8704
+ configFrom(cmd) {
8705
+ return {
8706
+ ...cmd.modelId?.trim() ? { modelId: cmd.modelId.trim() } : {},
8707
+ ...cmd.thoughtLevel?.trim() ? { thoughtLevel: cmd.thoughtLevel.trim() } : {},
8708
+ ...cmd.agentMode?.trim() ? { agentMode: cmd.agentMode.trim() } : {},
8709
+ ...cmd.permission?.trim() ? { permission: cmd.permission.trim() } : {}
8710
+ };
8711
+ }
8712
+ /** 配置差异 → ACP configId 映射(只返回变化项;open 幂等路径用) */
8713
+ diffConfig(applied, next) {
8714
+ const out = {};
8715
+ for (const key of ["modelId", "thoughtLevel", "agentMode", "permission"]) {
8716
+ const value = next[key];
8717
+ if (value && value !== applied[key])
8718
+ out[this.configIdOf(key)] = value;
8719
+ }
8720
+ return out;
8721
+ }
8722
+ /** 本地字段名 → ACP configId(§4.4:model / thought_level / mode / permission) */
8723
+ configIdOf(key) {
8724
+ if (key === "modelId")
8725
+ return "model";
8726
+ if (key === "thoughtLevel")
8727
+ return "thought_level";
8728
+ if (key === "agentMode")
8729
+ return "mode";
8730
+ return key;
8731
+ }
8732
+ /** ACP configId → 本地字段名(config 变更回写用) */
8733
+ configKey(configId) {
8734
+ if (configId === "model")
8735
+ return "modelId";
8736
+ if (configId === "thought_level")
8737
+ return "thoughtLevel";
8738
+ if (configId === "mode")
8739
+ return "agentMode";
8740
+ return configId;
8741
+ }
8742
+ /**
8743
+ * 结构化提问上行(§4.6 ②):登记 → 长轮询切片等待用户应答 → 超时回 cancel。
8744
+ *
8745
+ * 为什么两段式(登记 + 轮询)而不是一个长连接:用户可能几分钟才填完,单个悬挂 HTTP 请求
8746
+ * 会撞上服务端 requestTimeout 与中间层空闲超时;切片等待把「无悬挂长连接」与
8747
+ * 「应答即达」两个目标同时拿到。
8748
+ */
8749
+ async handleElicitation(sessionId, request) {
8750
+ const timeoutMs = resolveElicitationTimeoutMs();
8751
+ let elicitationId;
8752
+ try {
8753
+ const created = await this.options.api.createElicitation(sessionId, {
8754
+ toolCallId: request.toolCallId,
8755
+ message: request.message,
8756
+ requestedSchema: request.requestedSchema
8757
+ });
8758
+ if (!created?.ok || !created.elicitationId) {
8759
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u767B\u8BB0\u5931\u8D25: ${created?.code ?? "unknown"}`);
8760
+ return { action: "decline" };
8761
+ }
8762
+ elicitationId = created.elicitationId;
8763
+ } catch (err) {
8764
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u767B\u8BB0\u5F02\u5E38: ${err instanceof Error ? err.message : String(err)}`);
8765
+ return { action: "decline" };
8766
+ }
8767
+ const deadline = Date.now() + timeoutMs;
8768
+ while (Date.now() < deadline) {
8769
+ const remaining = deadline - Date.now();
8770
+ try {
8771
+ const result = await this.options.api.getElicitation(sessionId, elicitationId, Math.min(ELICITATION_POLL_WAIT_MS, remaining));
8772
+ if (result?.status === "answered")
8773
+ return { action: result.action ?? "accept", content: result.content ?? {} };
8774
+ if (result?.status === "cancelled")
8775
+ return { action: "cancel" };
8776
+ if (result?.status === "expired") {
8777
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u5DF2\u5728\u670D\u52A1\u7AEF\u8FC7\u671F\uFF08${elicitationId}\uFF09`);
8778
+ return { action: "cancel" };
8779
+ }
8780
+ } catch (err) {
8781
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u67E5\u8BE2\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8782
+ return { action: "cancel" };
8783
+ }
8784
+ const managed = this.sessions.get(sessionId);
8785
+ if (managed)
8786
+ this.touch(managed);
8787
+ }
8788
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u7B49\u5F85\u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)}s\uFF0C${elicitationId}\uFF09`);
8789
+ return { action: "cancel" };
8790
+ }
8791
+ };
7257
8792
 
7258
8793
  // src/daemon/stage-contracts.ts
7259
8794
  var STAGE_CONTRACTS = {
@@ -7304,6 +8839,95 @@ function buildStageContract(stageCode, source) {
7304
8839
  return STAGE_CONTRACTS[stageCode] ?? "";
7305
8840
  }
7306
8841
 
8842
+ // src/daemon/task-messages.ts
8843
+ var TASK_MESSAGE_QUEUE_MAX = 200;
8844
+ function asRecord(value) {
8845
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8846
+ }
8847
+ function asNonEmptyString(value) {
8848
+ return typeof value === "string" && value.length > 0 ? value : null;
8849
+ }
8850
+ function normalizeAgentMessage(value) {
8851
+ const row = asRecord(value);
8852
+ if (!row)
8853
+ return null;
8854
+ const id = asNonEmptyString(row.id);
8855
+ if (!id)
8856
+ return null;
8857
+ const seq = row.seq;
8858
+ if (typeof seq !== "number" || !Number.isFinite(seq))
8859
+ return null;
8860
+ return {
8861
+ id,
8862
+ seq,
8863
+ authorType: typeof row.authorType === "string" ? row.authorType : "unknown",
8864
+ authorPrincipalId: asNonEmptyString(row.authorPrincipalId),
8865
+ authorName: asNonEmptyString(row.authorName),
8866
+ content: typeof row.content === "string" ? row.content : "",
8867
+ mode: typeof row.mode === "string" ? row.mode : "queue",
8868
+ deliveryState: typeof row.deliveryState === "string" ? row.deliveryState : "pending",
8869
+ deliveryError: asNonEmptyString(row.deliveryError),
8870
+ targetTaskId: asNonEmptyString(row.targetTaskId),
8871
+ anchorEventSeq: typeof row.anchorEventSeq === "number" ? row.anchorEventSeq : null,
8872
+ turnNo: typeof row.turnNo === "number" ? row.turnNo : null,
8873
+ createdAt: typeof row.createdAt === "string" ? row.createdAt : ""
8874
+ };
8875
+ }
8876
+ function parseTaskMessageFrame(value) {
8877
+ const frame = asRecord(value);
8878
+ if (!frame)
8879
+ return null;
8880
+ const taskId = asNonEmptyString(frame.taskId);
8881
+ if (!taskId || !Array.isArray(frame.messages))
8882
+ return null;
8883
+ const messages = frame.messages.map(normalizeAgentMessage).filter((message) => message !== null);
8884
+ if (messages.length === 0)
8885
+ return null;
8886
+ return {
8887
+ type: "agent-task:message",
8888
+ taskId,
8889
+ objectId: asNonEmptyString(frame.objectId),
8890
+ anchorKey: typeof frame.anchorKey === "string" ? frame.anchorKey : "",
8891
+ messages
8892
+ };
8893
+ }
8894
+ function upsertTaskMessages(existing, incoming, max = TASK_MESSAGE_QUEUE_MAX) {
8895
+ const byId = /* @__PURE__ */ new Map();
8896
+ for (const message of existing)
8897
+ byId.set(message.id, message);
8898
+ for (const message of incoming)
8899
+ byId.set(message.id, message);
8900
+ const merged = [...byId.values()].sort((a, b) => a.seq - b.seq || a.id.localeCompare(b.id));
8901
+ const dropped = Math.max(0, merged.length - max);
8902
+ return { messages: dropped > 0 ? merged.slice(dropped) : merged, dropped };
8903
+ }
8904
+ function selectConsumedMessages(messages, taskId) {
8905
+ return messages.filter((message) => message.targetTaskId === taskId);
8906
+ }
8907
+ function selectSteerMessages(messages, taskId) {
8908
+ return messages.filter((message) => message.mode === "steer").filter((message) => message.deliveryState !== "consumed" && message.deliveryState !== "withdrawn").filter((message) => message.targetTaskId === taskId).sort((a, b) => a.seq - b.seq);
8909
+ }
8910
+ function renderMessageList(messages) {
8911
+ return messages.map((message) => `${message.seq}. \u3010${message.authorType === "user" ? "\u4EBA" : message.authorType === "system" ? "\u5E73\u53F0" : "\u8FD0\u884C\u65F6"}\u3011${message.content}`).join("\n\n");
8912
+ }
8913
+ function buildSteerPrompt(input) {
8914
+ const opinions = input.messages.length > 0 ? renderMessageList(input.messages) : "\uFF08\u6253\u65AD\u8BF7\u6C42\u672A\u88AB\u53D6\u5230\uFF0C\u8BF7\u6309\u539F\u59CB\u9700\u6C42\u7EE7\u7EED\uFF09";
8915
+ const resetNote = input.sessionReset ? "\n- \u26A0\uFE0F \u4F1A\u8BDD\u5DF2\u91CD\u5EFA\uFF1A\u4E0A\u4E00\u56DE\u5408\u7684\u5BF9\u8BDD\u4E0A\u4E0B\u6587**\u672A\u4FDD\u7559**\uFF0C\u8BF7\u4EE5\u672C\u56DE\u5408\u7ED9\u51FA\u7684\u4FE1\u606F\u4E3A\u51C6\uFF08\u5DE5\u4F5C\u533A\u6587\u4EF6\u4E0E git \u72B6\u6001\u4ECD\u5728\uFF09\u3002" : "";
8916
+ const originalPrompt = input.taskPrompt?.trim() ? `
8917
+
8918
+ \u3010\u539F\u59CB\u4EFB\u52A1\u9700\u6C42\uFF08\u672C\u8F6E\u662F\u540C\u4E00\u6B21\u5DE5\u4F5C\u7684\u5EF6\u7EED\uFF09\u3011
8919
+ ${input.taskPrompt.trim()}` : "";
8920
+ return `\u3010\u8FD0\u884C\u4E2D\u6253\u65AD \xB7 \u8BF7\u5148\u6309\u8FD9\u4E9B\u610F\u89C1\u8C03\u6574\uFF0C\u518D\u7EE7\u7EED\u3011
8921
+ \u4F60\u6B63\u5728\u6267\u884C\u7B2C ${input.turnNo - 1} \u56DE\u5408\uFF0C\u4EBA\u5728\u8FD0\u884C\u8FC7\u7A0B\u4E2D\u63D0\u51FA\u4E86\u4EE5\u4E0B\u610F\u89C1\uFF08\u6309\u63D0\u51FA\u987A\u5E8F\uFF09\uFF1A
8922
+
8923
+ ${opinions}
8924
+
8925
+ \u8981\u6C42\uFF1A
8926
+ 1. \u5148\u9010\u6761\u5904\u7406\u4E0A\u8FF0\u610F\u89C1\uFF0C\u518D\u7EE7\u7EED\u5B8C\u6210\u672C\u4EFB\u52A1\uFF1B\u610F\u89C1\u4E0E\u539F\u59CB\u9700\u6C42\u51B2\u7A81\u65F6**\u4EE5\u610F\u89C1\u4E3A\u51C6**\uFF0C\u5E76\u5728\u6700\u7EC8\u603B\u7ED3\u91CC\u8BF4\u660E\u5DEE\u5F02\u3002
8927
+ 2. \u4E0D\u8981\u63A8\u7FFB\u4E0A\u8FF0\u610F\u89C1\u672A\u6D89\u53CA\u7684\u90E8\u5206\uFF1B\u5DF2\u7ECF\u5B8C\u6210\u4E14\u4E0D\u5728\u610F\u89C1\u8303\u56F4\u5185\u7684\u6539\u52A8\u4FDD\u7559\u3002
8928
+ 3. \u4E0A\u4E00\u56DE\u5408\u5728\u5B89\u5168\u70B9\u88AB\u4E2D\u65AD\uFF0C\u5176\u4E2D\u7684\u5DE5\u5177\u8FDB\u5EA6\u5DF2\u4E22\u5F03\u3001\u88AB\u4E2D\u65AD\u7684\u8C03\u7528**\u6CA1\u6709\u7ED3\u679C**\uFF08\u65F6\u95F4\u7EBF\u4E0A\u6807\u4E3A\u5DF2\u4E2D\u65AD\uFF09\u2014\u2014\u9700\u8981\u65F6\u91CD\u65B0\u6267\u884C\uFF0C\u4E0D\u8981\u5047\u5B9A\u5B83\u5DF2\u6210\u529F\u3002${resetNote}${originalPrompt}`;
8929
+ }
8930
+
7307
8931
  // src/daemon/ws-client.ts
7308
8932
  import { createHash as createHash2 } from "node:crypto";
7309
8933
  import { io } from "socket.io-client";
@@ -7419,7 +9043,7 @@ var DaemonWsClient = class {
7419
9043
  handlers.onTaskResubscribed?.(taskId);
7420
9044
  });
7421
9045
  socket.on("agent-task:dispatched", (payload) => {
7422
- handlers.onTaskDispatched(payload.taskId, payload.runtimeId);
9046
+ handlers.onTaskDispatched(payload.taskId, payload.runtimeId, payload.daemonId);
7423
9047
  });
7424
9048
  socket.on("agent-task:status-changed", (payload) => {
7425
9049
  if (payload?.status === "cancelled" && payload.taskId) {
@@ -7440,6 +9064,12 @@ var DaemonWsClient = class {
7440
9064
  if (event)
7441
9065
  handlers.onTaskMessage(event);
7442
9066
  });
9067
+ for (const kind of ["open", "prompt", "cancel", "config", "close"]) {
9068
+ socket.on(`agent-session:${kind}`, (payload) => {
9069
+ if (payload && typeof payload === "object")
9070
+ handlers.onSessionCommand?.(kind, payload);
9071
+ });
9072
+ }
7443
9073
  socket.on("connect_error", (error) => {
7444
9074
  this.onConnectError(socket, error);
7445
9075
  });
@@ -7547,6 +9177,7 @@ var OUTBOX_MAX_ATTEMPTS_PER_ROUND = 2;
7547
9177
  var PROGRESS_BATCH_MAX = 500;
7548
9178
  var OBSERVATION_WARN_INTERVAL_MS = 3e4;
7549
9179
  var TERMINAL_OBSERVATION_DRAIN_BUDGET_MS = Number(process.env.MORULA_TERMINAL_DRAIN_BUDGET_MS ?? 2e3);
9180
+ var CANCEL_TRACE_TEXT_MAX = 4e3;
7550
9181
  async function drainObservationTail(args) {
7551
9182
  const startedAt = Date.now();
7552
9183
  const progressDrained = await waitWithin(args.progress, args.budgetMs);
@@ -7568,7 +9199,7 @@ async function waitWithin(promise, ms) {
7568
9199
  clearTimeout(timer);
7569
9200
  return done;
7570
9201
  }
7571
- var OUTBOX_PERMANENT_CODES = ["NOT_FOUND", "CONFLICT"];
9202
+ var OUTBOX_PERMANENT_CODES = ["NOT_FOUND", "CONFLICT", "RUNTIME_MISMATCH"];
7572
9203
  var DEFAULT_MODEL = "deepseek-v4-flash";
7573
9204
  var DaemonRunner = class {
7574
9205
  constructor(client, token, store, tenantId, options = {}) {
@@ -7590,6 +9221,12 @@ var DaemonRunner = class {
7590
9221
  ws;
7591
9222
  log;
7592
9223
  runtimeId = null;
9224
+ /**
9225
+ * 本进程的**实例标识**(`daemonId`,config dir 内持久化):同机多实例注册时会复用同一行
9226
+ * `AgentRuntime`(同一 runtimeId),下行命令靠 `runtimeId` + `daemonId` 双重过滤才能定向到实例
9227
+ * (issues/duplicate-daemon-same-runtime-double-execution.md)。
9228
+ */
9229
+ daemonId = null;
7593
9230
  running = false;
7594
9231
  /** D1:心跳定时器(独立于 tick)与单飞标志——上一拍未返回则跳过本拍,避免慢请求堆积 */
7595
9232
  heartbeatTimer = null;
@@ -7622,6 +9259,12 @@ var DaemonRunner = class {
7622
9259
  isolatedDimEnv = {};
7623
9260
  /** ACP 连接池(ACP-only:daemon 启动即建,懒 spawn,首个任务才起 dim acp 进程) */
7624
9261
  acpPool = null;
9262
+ /**
9263
+ * 会话宿主(steward-core-acp-migration §4.4,P0 骨架):长驻会话(管家对话)的执行载体,
9264
+ * 每会话独立 dim acp 进程(不走连接池)。P0 只接骨架(WS 命令转发 + 机器协议上报),
9265
+ * 服务端发射端(agent-session:*)在 P1-5 接入。
9266
+ */
9267
+ sessionHost = null;
7625
9268
  /** LOCAL-AGENT-REPO-READ-001:本地 repo checkout 端点(agent 本轮内读仓库) */
7626
9269
  repoCheckout = null;
7627
9270
  async start() {
@@ -7657,6 +9300,15 @@ var DaemonRunner = class {
7657
9300
  env: this.isolatedDimEnv
7658
9301
  });
7659
9302
  this.log("[daemon] dim executor: acp-v1\uFF08ACP-only\uFF0C\u957F\u9A7B dim acp \u8FDB\u7A0B\uFF09");
9303
+ this.sessionHost = new SessionHost({
9304
+ api: this.api,
9305
+ workdir: cfg2?.workdir ?? process.cwd(),
9306
+ dimPath: this.dimPath,
9307
+ isolatedDimEnv: this.isolatedDimEnv,
9308
+ baseUrl: this.client.baseUrl,
9309
+ log: this.log
9310
+ });
9311
+ this.log(`[daemon] session host: ready\uFF08\u7A7A\u95F2\u56DE\u6536 ${Math.round(resolveSessionIdleMs() / 6e4)}min\uFF0CMORULA_SESSION_IDLE_MS \u8986\u76D6\uFF09`);
7660
9312
  this.repoCheckout = new LocalRepoCheckoutService({
7661
9313
  api: this.api,
7662
9314
  cfg: cfg2 ?? loadDaemonConfig(this.store),
@@ -7675,7 +9327,7 @@ var DaemonRunner = class {
7675
9327
  this.log(`[daemon] dim CLI: ${this.dimPath}`);
7676
9328
  let binaryMtime = "unknown";
7677
9329
  try {
7678
- binaryMtime = statSync3(process.execPath).mtime.toISOString();
9330
+ binaryMtime = statSync4(process.execPath).mtime.toISOString();
7679
9331
  } catch (_) {
7680
9332
  }
7681
9333
  this.log(`[daemon] binary=${process.execPath} mtime=${binaryMtime} v=${BUILD_VERSION2}`);
@@ -7712,6 +9364,7 @@ var DaemonRunner = class {
7712
9364
  providers: inventory
7713
9365
  });
7714
9366
  this.runtimeId = registered.id;
9367
+ this.daemonId = cfg.daemonId;
7715
9368
  if (cfg.runtimeId !== registered.id) {
7716
9369
  saveDaemonConfig(this.store, { runtimeId: registered.id });
7717
9370
  }
@@ -7741,6 +9394,11 @@ var DaemonRunner = class {
7741
9394
  });
7742
9395
  this.acpPool = null;
7743
9396
  }
9397
+ if (this.sessionHost) {
9398
+ void this.sessionHost.closeAll().catch(() => {
9399
+ });
9400
+ this.sessionHost = null;
9401
+ }
7744
9402
  this.repoCheckout?.close();
7745
9403
  this.repoCheckout = null;
7746
9404
  }
@@ -7857,6 +9515,41 @@ var DaemonRunner = class {
7857
9515
  this.log(`[daemon] cleanup poll \u5F02\u5E38: ${error instanceof Error ? error.message : String(error)}`);
7858
9516
  }
7859
9517
  }
9518
+ /**
9519
+ * 取消留痕(issues/daemon-stream-events-silently-stop-mid-run.md 未实施项 5;用户 2026-09-21 定案「取消也留痕」)。
9520
+ *
9521
+ * 取消路径原本直接 `return`:这一轮**已产出**的内容(dim 已写完的回答、中断前的部分回答)在平台侧
9522
+ * 零留存(唯一痕迹是 `agent_failed{status:cancelled}`)→「取消 = 这轮白跑且无法追溯」。
9523
+ *
9524
+ * 契约(定案):**不得**把任务状态改回 completed —— 服务端只把 `result.text` 合并进
9525
+ * `agent_tasks.result.cancelled.lastOutput`(既有列,与取消时的 who/when/step 同一条记录)。
9526
+ *
9527
+ * best-effort 边界:**不走 outbox、不重试、失败只 log** —— 留痕不得让取消流程变慢或失败;
9528
+ * 无已产出内容时不发请求(不制造空留痕噪声)。旧服务端不认识 `cancelled` 字段(whitelist 剥离)
9529
+ * → 行为退化为原样,不会报错。
9530
+ */
9531
+ async reportCancelTrace(taskId, text, reason) {
9532
+ const produced = (text ?? "").trim();
9533
+ if (!produced) {
9534
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u88AB\u53D6\u6D88\uFF0C\u672C\u8F6E\u65E0\u5DF2\u4EA7\u51FA\u5185\u5BB9\uFF0C\u4E0D\u5199\u53D6\u6D88\u7559\u75D5\uFF08${reason}\uFF09`);
9535
+ return;
9536
+ }
9537
+ try {
9538
+ const res = await this.api.submitResult(taskId, {
9539
+ runtimeId: this.runtimeId ?? "",
9540
+ ok: true,
9541
+ cancelled: true,
9542
+ // 与服务端 `result.text` 的 4000 上限同口径:够追溯「这轮产出了什么」
9543
+ result: { text: produced.slice(0, CANCEL_TRACE_TEXT_MAX) }
9544
+ });
9545
+ if (res?.ok)
9546
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u88AB\u53D6\u6D88\uFF1A\u5DF2\u8865\u8BB0\u672C\u8F6E\u4EA7\u51FA ${produced.length} \u5B57\uFF08\u670D\u52A1\u7AEF\u53EA\u7559\u75D5\uFF0C\u4E0D\u6539\u4EFB\u52A1\u72B6\u6001\uFF09`);
9547
+ else
9548
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u53D6\u6D88\u7559\u75D5\u672A\u843D\u5E93\uFF08${res?.code ?? "unknown"}\uFF09\u2014\u2014 \u4E0D\u5F71\u54CD\u53D6\u6D88\u672C\u8EAB`);
9549
+ } catch (error) {
9550
+ this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u53D6\u6D88\u7559\u75D5\u4E0A\u62A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
9551
+ }
9552
+ }
7860
9553
  /**
7861
9554
  * M1-A2:终态结果可靠回传——先写盘 → 单次尝试 → 成功删盘(顺序语义不变)。
7862
9555
  *
@@ -7895,7 +9588,7 @@ var DaemonRunner = class {
7895
9588
  let res = null;
7896
9589
  let transportError = null;
7897
9590
  try {
7898
- res = await this.api.submitResult(taskId, body);
9591
+ res = await this.api.submitResult(taskId, { ...body, runtimeId: this.runtimeId ?? "" });
7899
9592
  } catch (err) {
7900
9593
  transportError = err?.message ?? String(err);
7901
9594
  }
@@ -8046,8 +9739,8 @@ var DaemonRunner = class {
8046
9739
  */
8047
9740
  buildWsHandlers() {
8048
9741
  return {
8049
- onTaskDispatched: (taskId, runtimeId) => {
8050
- this.onTaskDispatched(taskId, runtimeId);
9742
+ onTaskDispatched: (taskId, runtimeId, daemonId) => {
9743
+ this.onTaskDispatched(taskId, runtimeId, daemonId);
8051
9744
  },
8052
9745
  // M1-A1:取消信号 → 中断执行中的 dim 进程
8053
9746
  onTaskCancelled: (taskId) => {
@@ -8077,9 +9770,74 @@ var DaemonRunner = class {
8077
9770
  // P1:重连后房间已重放 → 立即按 afterSeq 重新补齐(断连期间的帧不会补发,只能拉)
8078
9771
  onTaskResubscribed: (taskId) => {
8079
9772
  void this.catchUpTaskMessages(taskId);
9773
+ },
9774
+ // 会话宿主命令(steward-core-acp-migration §4.4,P1-5):转发给 SessionHost。
9775
+ // 服务端发到 **tenant 房间**(同租户每台 daemon 都收到,WS 无 per-runtime 房间),
9776
+ // 故与 `agent-task:dispatched` 同口径按 `runtimeId` + `daemonId` 自过滤。
9777
+ onSessionCommand: (kind, payload) => {
9778
+ void this.handleSessionCommand(kind, payload);
8080
9779
  }
8081
9780
  };
8082
9781
  }
9782
+ /**
9783
+ * 会话宿主命令分发。
9784
+ *
9785
+ * 两道闸:
9786
+ * ① **runtimeId + daemonId 自过滤**(tenant 广播 → 只有归属设备处理;字段缺失时不过滤,兼容旧服务端)。
9787
+ * daemonId 是**实例**维度(issues/duplicate-daemon-same-runtime-double-execution.md):同机多实例
9788
+ * 注册时会复用同一行 AgentRuntime(同一 runtimeId),缺了它两个实例会各自 `open` 同一 sessionId
9789
+ * 并各跑一轮 prompt(实测:同一轮两份正文事件、两个 daemon 都「会话已打开」);
9790
+ * ② **按会话串行**:服务端每轮 prompt 前会补发 `open` 做兜底,而 socket.io 只保证「送达有序」,
9791
+ * 不保证「异步处理有序」——`void handleSessionCommand()` 会让 open/prompt 交错执行,
9792
+ * prompt 可能先于 open 完成而拿到 SESSION_NOT_OPEN。故按 sessionId 串成一条 promise 链。
9793
+ */
9794
+ sessionCommandChains = /* @__PURE__ */ new Map();
9795
+ handleSessionCommand(kind, payload) {
9796
+ const runtimeId = typeof payload.runtimeId === "string" ? payload.runtimeId : void 0;
9797
+ if (runtimeId && this.runtimeId && runtimeId !== this.runtimeId)
9798
+ return Promise.resolve();
9799
+ const daemonId = typeof payload.daemonId === "string" ? payload.daemonId : void 0;
9800
+ if (daemonId && this.daemonId && daemonId !== this.daemonId)
9801
+ return Promise.resolve();
9802
+ const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : "";
9803
+ if (!sessionId) {
9804
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u7F3A\u5C11 sessionId\uFF08\u5FFD\u7565\uFF09`);
9805
+ return Promise.resolve();
9806
+ }
9807
+ const prev = this.sessionCommandChains.get(sessionId) ?? Promise.resolve();
9808
+ const next = prev.catch(() => {
9809
+ }).then(() => this.runSessionCommand(kind, payload)).catch((err) => {
9810
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
9811
+ });
9812
+ this.sessionCommandChains.set(sessionId, next);
9813
+ void next.finally(() => {
9814
+ if (this.sessionCommandChains.get(sessionId) === next)
9815
+ this.sessionCommandChains.delete(sessionId);
9816
+ });
9817
+ return next;
9818
+ }
9819
+ /** 单条会话命令执行(畸形命令记日志不抛——单条坏命令不该打断 daemon 主循环) */
9820
+ async runSessionCommand(kind, payload) {
9821
+ const host = this.sessionHost;
9822
+ if (!host) {
9823
+ this.log(`[daemon] \u6536\u5230\u4F1A\u8BDD\u547D\u4EE4 ${kind}\uFF0C\u4F46\u4F1A\u8BDD\u5BBF\u4E3B\u672A\u5C31\u7EEA\uFF08\u5FFD\u7565\uFF09`);
9824
+ return;
9825
+ }
9826
+ try {
9827
+ if (kind === "open")
9828
+ await host.open(payload);
9829
+ else if (kind === "prompt")
9830
+ await host.prompt(payload);
9831
+ else if (kind === "cancel")
9832
+ host.cancel(payload);
9833
+ else if (kind === "config")
9834
+ await host.config(payload);
9835
+ else if (kind === "close")
9836
+ await host.close(payload);
9837
+ } catch (err) {
9838
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
9839
+ }
9840
+ }
8083
9841
  /**
8084
9842
  * 派发事件入口(P0-b,issues/agent-task-false-failure-on-daemon-fetch-error.md):
8085
9843
  * 服务端把派发事件发到 **tenant 房间**(daemon-executor.service.ts `sendToTenant`,WS 无 per-runtime
@@ -8088,10 +9846,17 @@ var DaemonRunner = class {
8088
9846
  * 不校验任务归属,见同 issue 根因 ③ / ④)。
8089
9847
  *
8090
9848
  * `runtimeId` 缺失(旧服务端)→ 保持旧行为不过滤;poll 兜底本就按 runtimeId 查询,不受影响。
9849
+ *
9850
+ * `daemonId`(issues/duplicate-daemon-same-runtime-double-execution.md):同机多实例注册时会复用
9851
+ * 同一行 `AgentRuntime`(同一 runtimeId,服务端按设备收敛重复行)→ 单靠 runtimeId 两个实例都会处理。
9852
+ * 故服务端下行带**实例标识**,两个字段都匹配才处理;`daemonId` 缺失(旧服务端)→ 退回只按 runtimeId
9853
+ * 过滤,否则新 daemon 接旧服务端会全丢事件。
8091
9854
  */
8092
- onTaskDispatched(taskId, runtimeId) {
9855
+ onTaskDispatched(taskId, runtimeId, daemonId) {
8093
9856
  if (runtimeId && this.runtimeId && runtimeId !== this.runtimeId)
8094
9857
  return;
9858
+ if (daemonId && this.daemonId && daemonId !== this.daemonId)
9859
+ return;
8095
9860
  void this.handleTask(taskId);
8096
9861
  }
8097
9862
  /** 任务处理(幂等:已在处理中则跳过) */
@@ -8306,7 +10071,14 @@ var DaemonRunner = class {
8306
10071
  // 本任务的 AgentTask ID:`morula repo mr create`(服务端建 MR 通道)等机器协议据此定位任务;
8307
10072
  // agent 不需要也不应自己拼(issues/mr-created-outside-bound-project.md)
8308
10073
  ...taskId ? { MORULA_TASK_ID: taskId } : {},
8309
- ...this.client?.baseUrl ? { MORULA_API_URL: this.client.baseUrl } : {}
10074
+ ...this.client?.baseUrl ? { MORULA_API_URL: this.client.baseUrl } : {},
10075
+ // 插件 bin 前置 + 显式 CLI 路径(与会话链路同源):任务里的 agent 也必须用本包物化的 `morula`,
10076
+ // 而不是宿主/桌面应用自带的旧版(旧版不认任务/会话凭证,且缺部分命令旗标 → 契约指令直接失败)。
10077
+ // PATH 只是第一道保险(dim 会重写 agent shell 的 PATH),可靠的是 `MORULA_CLI` 绝对路径。
10078
+ // MORULA_CLI 优先本包自带入口(与当前 daemon 同源),产物缺失才回退插件启动器。
10079
+ // 见 issues/steward-cli-resolves-stale-bundled-morula.md、issues/agent-cli-stale-build-missing-steward-commands.md。
10080
+ PATH: prependPluginBinToPath(process.env.PATH),
10081
+ MORULA_CLI: resolveAgentCliPath()
8310
10082
  };
8311
10083
  let repoDir = cfg.workdir;
8312
10084
  let taskRoot = cfg.workdir;
@@ -8360,12 +10132,12 @@ var DaemonRunner = class {
8360
10132
  });
8361
10133
  if (spaceToken?.token)
8362
10134
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
8363
- repoDir = join18(cfg.workdir, "scratch", taskId);
10135
+ repoDir = join20(cfg.workdir, "scratch", taskId);
8364
10136
  taskRoot = repoDir;
8365
- mkdirSync11(repoDir, { recursive: true });
8366
- const readme = join18(repoDir, "README.md");
10137
+ mkdirSync13(repoDir, { recursive: true });
10138
+ const readme = join20(repoDir, "README.md");
8367
10139
  if (!existsSync15(readme)) {
8368
- writeFileSync9(
10140
+ writeFileSync11(
8369
10141
  readme,
8370
10142
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
8371
10143
 
@@ -8373,7 +10145,8 @@ var DaemonRunner = class {
8373
10145
  \u7981\u6B62\u63A2\u7D22/\u4FEE\u6539\u672C\u76EE\u5F55\u4E4B\u5916\u7684\u4EFB\u4F55\u8DEF\u5F84\u2014\u2014\u5C24\u5176\u662F daemon \u6839\u76EE\u5F55\u4E0B\u7684 repos/\u3001worktrees/\u3001scratch/ \u7B49\u3002
8374
10146
 
8375
10147
  ## \u8BFB\u53D6\u4ED3\u5E93\u5185\u5BB9\uFF08\u91CD\u8981\uFF09
8376
- \u82E5\u7CFB\u7EDF\u63D0\u793A/\u4EFB\u52A1\u4E0A\u4E0B\u6587\u51FA\u73B0\u300C\u53EF\u7528\u4ED3\u5E93\u300D\u6216\u4F60\u88AB @ \u4E86\u67D0\u4E2A\u4ED3\u5E93\uFF08\u5982 \`human-resources/xxx\`\uFF09\uFF0C\u672C\u76EE\u5F55\u91CC\u6CA1\u6709\u8BE5\u4ED3\u5E93\u662F\u6B63\u5E38\u7684\u2014\u2014\u4ED3\u5E93\u7531 \`morula repo checkout <\u672C\u4EFB\u52A1ID> <repoFullName>\` \u7269\u5316\uFF08\u672C\u5730 daemon \u7AEF\u70B9\uFF0C\u672C\u8F6E\u7ACB\u5373\u7269\u5316\u5E76\u8FD4\u56DE\u8DEF\u5F84\uFF09\uFF0C\u7269\u5316\u540E\u53EA\u8BFB\u67E5\u8BC1\uFF08git log / \u8BFB\u6587\u4EF6\uFF09\uFF0C\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u6539\u4EE3\u7801\u3002
10148
+ \u82E5\u4EFB\u52A1\u9700\u8981\u8BFB/\u6539\u67D0\u4E2A\u4ED3\u5E93\uFF08\u4EFB\u52A1\u63CF\u8FF0\u91CC\u63D0\u5230\u3001\u6216\u4F60\u88AB @ \u4E86\u67D0\u4E2A\u4ED3\u5E93\u5982 \`human-resources/xxx\`\uFF09\uFF0C\u672C\u76EE\u5F55\u91CC\u6CA1\u6709\u8BE5\u4ED3\u5E93\u662F\u6B63\u5E38\u7684\u2014\u2014\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u54EA\u4E9B\u4ED3\u5E93\u7528 \`morula repo list --space <spaceId>\` \u67E5\uFF08spaceId \u89C1\u7CFB\u7EDF\u63D0\u793A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF09\u3002
10149
+ \u4ED3\u5E93\u7531 \`morula repo checkout <\u672C\u4EFB\u52A1ID> <repoFullName>\` \u7269\u5316\uFF08\u672C\u5730 daemon \u7AEF\u70B9\uFF0C\u672C\u8F6E\u7ACB\u5373\u7269\u5316\u5E76\u8FD4\u56DE\u8DEF\u5F84\uFF09\uFF0C\u7269\u5316\u540E\u53EA\u8BFB\u67E5\u8BC1\uFF08git log / \u8BFB\u6587\u4EF6\uFF09\uFF0C\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u6539\u4EE3\u7801\u3002
8377
10150
  \u82E5\u4EFB\u52A1\u9700\u8981**\u6539\u52A8**\u4ED3\u5E93\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\u7531\u4F60\u5224\u65AD\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\uFF09\uFF0C\u7528 \`morula repo prepare <\u672C\u4EFB\u52A1ID> <repoFullName>\` \u7269\u5316\u5230\u672C\u76EE\u5F55\uFF08\u8FD4\u56DE\u8DEF\u5F84\uFF09\uFF0C\u4ED3\u5E93\u4F1A\u4F5C\u4E3A\u672C\u76EE\u5F55\u4E0B\u7684\u5B50\u6587\u4EF6\u5939\u51FA\u73B0\uFF08\u5982 \`<owner>-<repo>/\`\uFF09\uFF0Cgit \u64CD\u4F5C\u5728\u8BE5\u5B50\u6587\u4EF6\u5939\u5185\u8FDB\u884C\u3002
8378
10151
  \u4E0D\u8981\u5728\u672C\u5730\u641C\u7D22\u4ED3\u5E93\u662F\u5426\u5B58\u5728\u2014\u2014\u672C\u5730\u6CA1\u6709 \u2260 \u4ED3\u5E93\u4E0D\u5B58\u5728\uFF0C\u5148\u6267\u884C checkout/prepare\u3002\u8BE6\u89C1\u534F\u4F5C\u5951\u7EA6 0.5 \u8282\u3002
8379
10152
  `,
@@ -8466,6 +10239,8 @@ var DaemonRunner = class {
8466
10239
  dimWorkDir: taskRoot
8467
10240
  });
8468
10241
  this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u5B8C\u6210`);
10242
+ if (signal?.aborted)
10243
+ await this.reportCancelTrace(taskId, result.finalText, "\u6536\u5C3E\u9636\u6BB5\u88AB\u53D6\u6D88");
8469
10244
  const isReviewTask = detail.kind === "review";
8470
10245
  const isAnalysisTask = detail.stageCode === "todo";
8471
10246
  if (!isReviewTask && !isAnalysisTask && isFlagEnabled("MORULA_LOCAL_AGENT_DELIVERY_V1", true))
@@ -8474,6 +10249,7 @@ var DaemonRunner = class {
8474
10249
  const error = result.error?.message ?? "dim \u6267\u884C\u5931\u8D25";
8475
10250
  if (signal?.aborted) {
8476
10251
  this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88\uFF0C\u8DF3\u8FC7\u7ED3\u679C\u56DE\u4F20: ${error}`);
10252
+ await this.reportCancelTrace(taskId, result.finalText, error);
8477
10253
  return;
8478
10254
  }
8479
10255
  await this.submitResultReliable(taskId, { ok: false, error });
@@ -8549,8 +10325,6 @@ var DaemonRunner = class {
8549
10325
  */
8550
10326
  async runOnce(taskId, detail, taskRoot, provider, providerKey, requestedModelId, env, repoBaseUrl, signal) {
8551
10327
  const streamChunks = [];
8552
- let textBuf = "";
8553
- let textTimer = null;
8554
10328
  let thoughtBuf = "";
8555
10329
  let thoughtTimer = null;
8556
10330
  let streamFlushing = false;
@@ -8649,6 +10423,16 @@ var DaemonRunner = class {
8649
10423
  void flushStreamChunks(false);
8650
10424
  }
8651
10425
  };
10426
+ const textStream = new AssistantTextStream({
10427
+ onDelta: (seg, text) => {
10428
+ streamChunks.push({ chunkType: "delta", data: { kind: "text", seg, text } });
10429
+ void flushStreamChunks(true);
10430
+ },
10431
+ onParagraph: (seg, text) => {
10432
+ streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text, seg } });
10433
+ void flushStreamChunks(true);
10434
+ }
10435
+ }, { log: (line) => this.log(`[daemon] ${line}`) });
8652
10436
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
8653
10437
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
8654
10438
  const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync15(resumeWorkDir);
@@ -8695,16 +10479,19 @@ var DaemonRunner = class {
8695
10479
  }
8696
10480
  }
8697
10481
  if (event.type === "tool_call_started") {
8698
- streamChunks.push({
8699
- chunkType: "tool",
8700
- seqHint: nextChunkSeq(),
8701
- data: {
8702
- toolName: event.toolName,
8703
- callId: event.callId,
8704
- input: event.input !== void 0 ? redactInputSummary(event.input) : void 0
8705
- }
10482
+ void textStream.flushParagraph().then(() => {
10483
+ streamChunks.push({
10484
+ chunkType: "tool",
10485
+ seqHint: nextChunkSeq(),
10486
+ data: {
10487
+ toolName: event.toolName,
10488
+ callId: event.callId,
10489
+ input: event.input !== void 0 ? redactInputSummary(event.input) : void 0
10490
+ }
10491
+ });
10492
+ void flushStreamChunks(true);
10493
+ }).catch(() => {
8706
10494
  });
8707
- void flushStreamChunks(true);
8708
10495
  }
8709
10496
  if (event.type === "tool_call_completed") {
8710
10497
  streamChunks.push({
@@ -8720,17 +10507,7 @@ var DaemonRunner = class {
8720
10507
  void flushStreamChunks(true);
8721
10508
  }
8722
10509
  if (event.type === "assistant_text_delta") {
8723
- textBuf += event.text;
8724
- if (!textTimer) {
8725
- textTimer = setTimeout(() => {
8726
- textTimer = null;
8727
- if (textBuf) {
8728
- streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text: textBuf } });
8729
- textBuf = "";
8730
- void flushStreamChunks(false);
8731
- }
8732
- }, 200);
8733
- }
10510
+ textStream.push(event.text);
8734
10511
  }
8735
10512
  if (event.type === "usage") {
8736
10513
  streamChunks.push({ chunkType: "usage", seqHint: nextChunkSeq(), data: event.usage });
@@ -8748,14 +10525,8 @@ var DaemonRunner = class {
8748
10525
  }
8749
10526
  };
8750
10527
  const result = await provider.run(input);
8751
- if (textTimer) {
8752
- clearTimeout(textTimer);
8753
- textTimer = null;
8754
- }
8755
- if (textBuf) {
8756
- streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text: textBuf } });
8757
- textBuf = "";
8758
- }
10528
+ await textStream.flushParagraph();
10529
+ textStream.dispose();
8759
10530
  if (thoughtTimer) {
8760
10531
  clearTimeout(thoughtTimer);
8761
10532
  thoughtTimer = null;
@@ -8925,7 +10696,7 @@ var DaemonRunner = class {
8925
10696
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
8926
10697
  return;
8927
10698
  }
8928
- const managedRoot = join18(cfg.workdir, "worktrees", this.tenantId);
10699
+ const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
8929
10700
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
8930
10701
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
8931
10702
  ok: false,
@@ -8944,7 +10715,7 @@ var DaemonRunner = class {
8944
10715
  repoFullName: wc.repoFullName,
8945
10716
  branchName: wc.branchName
8946
10717
  };
8947
- const repoCachePath = buildRepoCachePath(cfg.workdir, this.tenantId, wc.repoFullName);
10718
+ const repoCachePath = buildRepoCachePath(cfg.workdir, this.tenantId, this.runtimeId, wc.repoFullName);
8948
10719
  const errorCode = validateRemovable(wc.worktreePath, expected, repoCachePath);
8949
10720
  if (errorCode) {
8950
10721
  this.log(`[daemon] cleanup ${item.id} \u62D2\u7EDD\u5220\u9664: ${errorCode}`);
@@ -8973,38 +10744,38 @@ function isDebugLogEnabled() {
8973
10744
  var spaceTokenFailureLogged = false;
8974
10745
  function writeCustomSkills(targetDir, skills, log) {
8975
10746
  const NAME_RE = /^[\w-]{1,64}$/;
8976
- const base = join18(targetDir, ".agents", "skills");
10747
+ const base = join20(targetDir, ".agents", "skills");
8977
10748
  for (const skill of skills) {
8978
10749
  const name = skill.name.trim();
8979
10750
  if (!NAME_RE.test(name)) {
8980
10751
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
8981
10752
  continue;
8982
10753
  }
8983
- const dir = join18(base, name);
8984
- if (dirname7(dir) !== base) {
10754
+ const dir = join20(base, name);
10755
+ if (dirname8(dir) !== base) {
8985
10756
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
8986
10757
  continue;
8987
10758
  }
8988
- mkdirSync11(dir, { recursive: true });
10759
+ mkdirSync13(dir, { recursive: true });
8989
10760
  const meta = ["---", `name: ${name}`];
8990
10761
  if (skill.description?.trim()) {
8991
10762
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
8992
10763
  }
8993
10764
  meta.push("---", "");
8994
- writeFileSync9(join18(dir, "SKILL.md"), `${meta.join("\n")}
10765
+ writeFileSync11(join20(dir, "SKILL.md"), `${meta.join("\n")}
8995
10766
  ${skill.content ?? ""}`, "utf8");
8996
10767
  }
8997
10768
  }
8998
10769
  function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
8999
- const dir = join18(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
9000
- const refsDir = join18(dir, "references");
9001
- mkdirSync11(refsDir, { recursive: true });
10770
+ const dir = join20(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
10771
+ const refsDir = join20(dir, "references");
10772
+ mkdirSync13(refsDir, { recursive: true });
9002
10773
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
9003
10774
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
9004
10775
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
9005
- writeFileSync9(join18(dir, "SKILL.md"), `${front}
10776
+ writeFileSync11(join20(dir, "SKILL.md"), `${front}
9006
10777
  ${body}`, "utf8");
9007
- writeFileSync9(join18(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
10778
+ writeFileSync11(join20(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
9008
10779
  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/`);
9009
10780
  }
9010
10781
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -9035,17 +10806,18 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
9035
10806
  };
9036
10807
  }
9037
10808
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
9038
- const briefDir = join18(targetDir, ".agent_context");
9039
- mkdirSync11(briefDir, { recursive: true });
10809
+ const briefDir = join20(targetDir, ".agent_context");
10810
+ mkdirSync13(briefDir, { recursive: true });
9040
10811
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
9041
10812
 
9042
10813
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
9043
10814
  - \u5DE5\u4F5C\u76EE\u5F55\uFF1A\u672C\u6587\u4EF6\u6240\u5728\u76EE\u5F55\uFF08\u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\uFF1B\u4ED3\u5E93\u662F\u5B83\u4E0B\u9762\u7684\u5B50\u76EE\u5F55\uFF0Cgit \u64CD\u4F5C\u5728\u4ED3\u5E93\u5B50\u76EE\u5F55\u5185\uFF09
10815
+ - \u534F\u4F5C\u5951\u7EA6\uFF08\u5FC5\u8BFB\uFF09\uFF1A\`.agents/skills/morula-working-on-issues/SKILL.md\`\uFF08\u76F8\u5BF9\u672C\u76EE\u5F55\uFF09\u3002\u82E5 \`skill\` \u5DE5\u5177\u62A5 \`Unknown skill\`\uFF0C**\u76F4\u63A5\u8BFB\u8BE5\u6587\u4EF6**\uFF08read/cat\uFF09\uFF0C\u4E0D\u8981\u56E0\u4E3A\u52A0\u8F7D\u5931\u8D25\u5C31\u8DF3\u8FC7\u5951\u7EA6\u3002
9044
10816
 
9045
10817
  ---
9046
10818
 
9047
10819
  ${buildTaskPromptWithContract(detail, void 0)}`;
9048
- writeFileSync9(join18(briefDir, "task-context.md"), brief, "utf8");
10820
+ writeFileSync11(join20(briefDir, "task-context.md"), brief, "utf8");
9049
10821
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
9050
10822
  }
9051
10823
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -9059,8 +10831,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
9059
10831
  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`);
9060
10832
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
9061
10833
  }
9062
- mkdirSync11(targetDir, { recursive: true });
9063
- writeFileSync9(join18(targetDir, "AGENTS.md"), `${content}
10834
+ mkdirSync13(targetDir, { recursive: true });
10835
+ writeFileSync11(join20(targetDir, "AGENTS.md"), `${content}
9064
10836
  `, "utf8");
9065
10837
  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`);
9066
10838
  return true;
@@ -9080,52 +10852,20 @@ function resolveRepo(detail) {
9080
10852
  }
9081
10853
  function buildTaskPromptWithContract(detail, repoBaseUrl) {
9082
10854
  const prompt = detail.taskPrompt ?? "";
9083
- const repo = resolveRepo(detail);
9084
- const snapRepo = detail.contextSnapshot;
9085
- const repoResolution = typeof snapRepo?.repoResolution === "string" ? snapRepo.repoResolution : void 0;
9086
- const availableRepos = Array.isArray(detail.availableRepos) ? detail.availableRepos.filter((r) => r.repoFullName) : [];
9087
- const multiRepoList = availableRepos.length > 1 ? availableRepos.map((r) => `- ${r.repoFullName}\uFF08\u9ED8\u8BA4\u5206\u652F\uFF1A${r.defaultBranch || "main"}\uFF09`).join("\n") : "";
9088
- const multiRepoGuide = availableRepos.length > 1 ? `\u3010\u591A\u4ED3\u5E93\u4EFB\u52A1\u3011\u672C\u4EFB\u52A1\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u591A\u4E2A\u4ED3\u5E93\uFF08\u6E05\u5355\u89C1\u4E0B\uFF09\uFF0C\u6D89\u53CA\u54EA\u4E9B\u4ED3\u5E93\u7531\u4F60\u7ED3\u5408\u4EFB\u52A1\u63CF\u8FF0\u5224\u65AD\u6267\u884C\uFF08\u4EFB\u52A1\u63CF\u8FF0\u300C\u6D89\u53CA\u4ED3\u5E93\u300D\u6BB5\u4E3A\u521D\u59CB\u5224\u65AD\uFF09\uFF1B\u6539\u52A8\u53D1\u751F\u5728\u54EA\u4E2A\u4ED3\u5E93\uFF0C\u5C31\u5728\u54EA\u4E2A\u4ED3\u5E93\u5185\u5EFA\u5206\u652F\u5E76\u63D0 PR/MR\uFF0C\u6BCF\u4E2A\u4ED3\u5E93\u7684 PR \u90FD\u4F1A\u88AB\u670D\u52A1\u7AEF\u81EA\u52A8\u5173\u8054\u5230\u672C\u4EFB\u52A1\u3002
9089
- \u53EF\u7528\u4ED3\u5E93\u6E05\u5355\uFF1A
9090
- ${multiRepoList}
9091
-
9092
- ` : "";
9093
- let repoHint = "";
9094
- const repoRefs = Array.isArray(detail.contextSnapshot?.repoRefs) ? detail.contextSnapshot.repoRefs : [];
9095
- if (repoRefs.length > 0) {
9096
- const list = repoRefs.map((r) => r.repoFullName).filter(Boolean).join("\u3001");
9097
- repoHint = `\u7528\u6237 @ \u6307\u5B9A\u8BFB\u53D6\u4ED3\u5E93\uFF1A${list}\uFF08\u53EA\u8BFB\u67E5\u8BC1\uFF0C\u4E0D\u5EFA\u5206\u652F\u3001\u4E0D\u6539\u4EE3\u7801\uFF1B\u7269\u5316\u65B9\u5F0F\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\u3002
9098
-
9099
- `;
9100
- } else if (repo) {
9101
- repoHint = `${multiRepoGuide}\u53EF\u7528\u4ED3\u5E93\uFF1A${repo}\uFF08\u7A7A\u95F4\u7ED1\u5B9A\u4ED3\u5E93\uFF1B\u7269\u5316/\u5206\u652F/MR \u64CD\u4F5C\u65B9\u5F0F\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\u3002
9102
-
9103
- `;
9104
- } else if (repoResolution === "MULTI_REPO_AGENT_DECIDES") {
9105
- repoHint = `${multiRepoGuide}\u53EF\u7528\u4ED3\u5E93\uFF1A${availableRepos.map((r) => r.repoFullName).join("\u3001")}\uFF08\u4EFB\u52A1\u7A7A\u95F4\u7ED1\u5B9A\uFF1B\u7269\u5316/\u5206\u652F/MR \u64CD\u4F5C\u65B9\u5F0F\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\u3002
9106
-
9107
- `;
9108
- } else if (repoResolution === "REPO_SELECTION_REQUIRED") {
9109
- const snapCandidates = Array.isArray(detail.contextSnapshot?.repoCandidates) ? detail.contextSnapshot.repoCandidates : [];
9110
- const candidates = snapCandidates.length > 0 ? snapCandidates : availableRepos.map((r) => r.repoFullName).filter((n) => Boolean(n));
9111
- repoHint = candidates.length > 0 ? `\u7A7A\u95F4\u5173\u8054\u591A\u4E2A\u4ED3\u5E93\uFF0C\u9700\u8981\u5411\u7528\u6237\u786E\u8BA4\u8981\u64CD\u4F5C\u7684\u4ED3\u5E93\uFF08\u5019\u9009\uFF1A${candidates.join("\u3001")}\uFF09\u3002
9112
-
9113
- ` : `\u7A7A\u95F4\u5173\u8054\u591A\u4E2A\u4ED3\u5E93\uFF0C\u9700\u8981\u5411\u7528\u6237\u786E\u8BA4\u8981\u64CD\u4F5C\u7684\u4ED3\u5E93\u3002
9114
-
9115
- `;
9116
- } else if (!repo && (repoResolution === "NO_CONNECTION" || !repoResolution)) {
9117
- if (availableRepos.length > 0) {
9118
- const list = availableRepos.map((r) => r.repoFullName).filter(Boolean).join("\u3001");
9119
- repoHint = `\u53EF\u7528\u4ED3\u5E93\uFF1A${list}\uFF08\u4EFB\u52A1\u7A7A\u95F4\u7ED1\u5B9A\uFF1B\u53EA\u8BFB\u67E5\u8BC1\u65B9\u5F0F\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\u3002
10855
+ void repoBaseUrl;
10856
+ const snap = detail.contextSnapshot;
10857
+ const spaceId = typeof snap?.spaceId === "string" && snap.spaceId.trim() ? snap.spaceId.trim() : "";
10858
+ const keysHint = `## \u57FA\u672C\u4FE1\u606F
9120
10859
 
9121
- `;
9122
- }
9123
- }
10860
+ - \u4EFB\u52A1 ID\uFF08AgentTask\uFF0C\`morula task branch\` \u7B49\u547D\u4EE4\u7684 taskId \u7528\u5B83\uFF09\uFF1A\`${detail.id}\`
10861
+ ${spaceId ? `- \u7A7A\u95F4 ID\uFF08\u6240\u6709 \`--space\` \u7528\u5B83\uFF09\uFF1A\`${spaceId}\`
10862
+ ` : ""}`;
9124
10863
  const toolsHint = `## \u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177
9125
10864
 
9126
- - \u5DE5\u4F5C\u76EE\u5F55 = \u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\uFF0C\u4ED3\u5E93\u662F\u5176\u4E0B\u7684\u5B50\u76EE\u5F55\uFF08\u5E03\u5C40\u4E0E\u7269\u5316/\u5206\u652F/MR \u64CD\u4F5C\u65B9\u5F0F\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\uFF1B
10865
+ - \u5DE5\u4F5C\u76EE\u5F55 = \u4EFB\u52A1\u5DE5\u4F5C\u533A\u6839\uFF08cwd\uFF09\uFF1B\u4ED3\u5E93\u662F\u5B83\u4E0B\u9762\u7684\u5B50\u76EE\u5F55\uFF08\u5E03\u5C40 / \u7269\u5316 / \u5206\u652F / MR \u7528\u6CD5\u89C1 \`morula-working-on-issues\` skill \xA70.5\uFF09\uFF1B
9127
10866
  - \u5E73\u53F0\u4EA4\u4E92\u5951\u7EA6\u4E0E\u547D\u4EE4\u7528\u6CD5\uFF1A\`morula-working-on-issues\` skill\uFF08\u5DF2\u6CE8\u5165 \`.agents/skills/\`\uFF09\uFF0C\u6309\u9700\u8BFB\u53D6\uFF1B
9128
- - \u4EFB\u52A1\u8BE6\u60C5\uFF08\u4EFB\u52A1\u7F16\u53F7 / \u5173\u8054\u5BF9\u8C61 / \u4E1A\u52A1\u76EE\u6807 / \u8BC4\u8BBA / PR\uFF09\uFF1A\`morula task get\`\uFF08\u65E0\u53C2\uFF0CtaskId \u7531\u6267\u884C\u73AF\u5883\u63D0\u4F9B\uFF09\u3002
10867
+ - \u4EFB\u52A1\u8BE6\u60C5\uFF08\u4EFB\u52A1\u7F16\u53F7 / \u5173\u8054\u5BF9\u8C61 / \u4E1A\u52A1\u76EE\u6807 / \u8BC4\u8BBA / PR\uFF09\uFF1A\`morula task get\`\uFF08\u65E0\u53C2\uFF0CtaskId \u7531\u6267\u884C\u73AF\u5883\u63D0\u4F9B\uFF09\uFF1B
10868
+ - \u7A7A\u95F4\u4ED3\u5E93\u6E05\u5355\uFF08\u591A\u4ED3\u5E93\u4EFB\u52A1\uFF09\uFF1A\`morula repo list --space <spaceId>\`\uFF08spaceId \u89C1\u4E0A\u300C\u57FA\u672C\u4FE1\u606F\u300D\u6BB5\uFF09\u3002
9129
10869
 
9130
10870
  `;
9131
10871
  const taskSource = detail.contextSnapshot?.source;
@@ -9133,19 +10873,12 @@ ${multiRepoList}
9133
10873
  const stageContractBlock = stageContract ? `${stageContract}
9134
10874
 
9135
10875
  ` : "";
9136
- const snapCtx = detail.contextSnapshot;
9137
- let extraHint = "";
9138
- if (snapCtx?.triggerSource === "comment_mention" && snapCtx.commentId) {
9139
- extraHint += `\u672C\u4EFB\u52A1\u7531\u8BC4\u8BBA\u89E6\u53D1\uFF08\u6700\u7EC8\u603B\u7ED3\u7531\u670D\u52A1\u7AEF\u81EA\u52A8\u4F5C\u4E3A\u8BE5\u89E6\u53D1\u8BC4\u8BBA\u7684\u56DE\u590D\u53D1\u51FA\uFF0C\u65E0\u9700\u81EA\u884C\u8BC4\u8BBA\uFF09\u3002
9140
-
9141
- `;
9142
- }
9143
- if (snapCtx?.handoffNote && typeof snapCtx.handoffNote === "string" && snapCtx.handoffNote.trim()) {
9144
- extraHint += `\u6307\u6D3E\u4EA4\u4ED8\u8981\u6C42\uFF08\u7528\u6237\u663E\u5F0F\u6307\u5B9A\uFF0C\u5FC5\u987B\u6EE1\u8DB3\uFF09\uFF1A**${snapCtx.handoffNote.trim()}**
10876
+ const handoff = typeof snap?.handoffNote === "string" ? snap.handoffNote.trim() : "";
10877
+ const extraHint = handoff ? `\u6307\u6D3E\u4EA4\u4ED8\u8981\u6C42\uFF08\u7528\u6237\u663E\u5F0F\u6307\u5B9A\uFF0C\u5FC5\u987B\u6EE1\u8DB3\uFF09\uFF1A**${handoff}**
9145
10878
 
9146
- `;
9147
- }
9148
- const head = `${stageContractBlock}${toolsHint}${repoHint}${extraHint}`;
10879
+ ` : "";
10880
+ const head = `${stageContractBlock}${keysHint}
10881
+ ${toolsHint}${extraHint}`;
9149
10882
  return head ? `${head}========== \u4F60\u7684\u76EE\u6807 ==========
9150
10883
  ${prompt}` : `${prompt}`;
9151
10884
  }
@@ -9158,7 +10891,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
9158
10891
  }
9159
10892
  async function reportOneRepoTarget(taskId, target, env, ops) {
9160
10893
  const execEnv = mergeExecEnv(env);
9161
- if (!existsSync15(target.dir) || !existsSync15(join18(target.dir, ".git"))) {
10894
+ if (!existsSync15(target.dir) || !existsSync15(join20(target.dir, ".git"))) {
9162
10895
  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`);
9163
10896
  return;
9164
10897
  }
@@ -9327,8 +11060,8 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
9327
11060
  // src/commands/doctor.ts
9328
11061
  init_dist2();
9329
11062
  import { existsSync as existsSync16, readFileSync as readFileSync10, realpathSync } from "node:fs";
9330
- import { homedir as homedir11 } from "node:os";
9331
- import { dirname as dirname8, join as join19 } from "node:path";
11063
+ import { homedir as homedir12 } from "node:os";
11064
+ import { dirname as dirname9, join as join21 } from "node:path";
9332
11065
  init_daemon_config();
9333
11066
  init_daemon_lock();
9334
11067
  init_dim_locate();
@@ -9348,10 +11081,10 @@ async function doctorCommand(context) {
9348
11081
  async function collectDoctor(context, probe) {
9349
11082
  const checks = [];
9350
11083
  const env = context.env;
9351
- const configDir = env.MORULA_CONFIG_DIR ?? join19(homedir11(), ".morula-cli");
9352
- const configPath = env.MORULA_CONFIG ?? join19(configDir, "config.yaml");
9353
- const daemonPath = join19(dirname8(configPath), "daemon");
9354
- const logFile = join19(daemonPath, "daemon.log");
11084
+ const configDir = env.MORULA_CONFIG_DIR ?? join21(homedir12(), ".morula-cli");
11085
+ const configPath = env.MORULA_CONFIG ?? join21(configDir, "config.yaml");
11086
+ const daemonPath = join21(dirname9(configPath), "daemon");
11087
+ const logFile = join21(daemonPath, "daemon.log");
9355
11088
  const contextName = context.runtime.context ?? null;
9356
11089
  const baseUrl = context.runtime.baseUrl ?? null;
9357
11090
  const tokenPresent = Boolean(context.runtime.token);
@@ -9365,12 +11098,11 @@ async function collectDoctor(context, probe) {
9365
11098
  configReadable = false;
9366
11099
  }
9367
11100
  }
9368
- const lockPath = join19(daemonPath, DAEMON_LOCK_FILE);
11101
+ const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
9369
11102
  const lock = probeDaemonLock(lockPath);
9370
- const pidFile = join19(daemonPath, "daemon.pid");
9371
- const filePid = existsSync16(pidFile) ? Number(readFileSync10(pidFile, "utf8").trim() || 0) : 0;
9372
- const lockPid = lock.state === "active" ? lock.record.pid : 0;
9373
- const daemonPid = lockPid || (isProcessAlive(filePid) ? filePid : 0) || null;
11103
+ const pidFile = join21(daemonPath, DAEMON_PID_FILE);
11104
+ const filePid = readPidFile(pidFile);
11105
+ const daemonPid = probeDaemonInstance(configDir).pid;
9374
11106
  const cfg = loadDaemonConfig(context.store);
9375
11107
  const dim = safeResolveDim();
9376
11108
  const dimPath = dim.path;
@@ -9538,13 +11270,14 @@ function detectInstallMode() {
9538
11270
 
9539
11271
  // src/commands/daemon.ts
9540
11272
  function configFilePath(env) {
9541
- return env.MORULA_CONFIG ?? join20(env.MORULA_CONFIG_DIR ?? join20(homedir12(), ".morula-cli"), "config.yaml");
11273
+ return env.MORULA_CONFIG ?? join22(env.MORULA_CONFIG_DIR ?? join22(homedir13(), ".morula-cli"), "config.yaml");
9542
11274
  }
9543
11275
  function daemonDir(configPath) {
9544
- return join20(dirname9(configPath), "daemon");
11276
+ return join22(dirname10(configPath), "daemon");
9545
11277
  }
9546
- var PID_FILE = "daemon.pid";
11278
+ var PID_FILE = DAEMON_PID_FILE;
9547
11279
  var LOG_FILE = "daemon.log";
11280
+ var DAEMON_EXIT_FALLBACK_MS = 3e3;
9548
11281
  async function daemonCommand(context) {
9549
11282
  const { action } = context.parsed;
9550
11283
  if (action === "run")
@@ -9727,6 +11460,9 @@ async function models(context) {
9727
11460
  }
9728
11461
  async function run2(context) {
9729
11462
  rejectUnknownFlags(context.parsed.flags, ["name", "workdir", "max-concurrent"]);
11463
+ const armBoundedExit = () => {
11464
+ setTimeout(() => process.exit(0), DAEMON_EXIT_FALLBACK_MS).unref();
11465
+ };
9730
11466
  const name = flagString(context.parsed.flags, "name") ?? void 0;
9731
11467
  const workdir = flagString(context.parsed.flags, "workdir") ?? void 0;
9732
11468
  const maxConcurrent = parseMaxConcurrent(context);
@@ -9735,13 +11471,14 @@ async function run2(context) {
9735
11471
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
9736
11472
  }
9737
11473
  const resolvedWorkdir = workdir ?? loadDaemonConfig(context.store)?.workdir ?? DEFAULT_WORKDIR;
9738
- mkdirSync12(resolvedWorkdir, { recursive: true });
9739
- const lockPath = join20(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
11474
+ mkdirSync14(resolvedWorkdir, { recursive: true });
11475
+ const lockPath = join22(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
9740
11476
  const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
9741
11477
  if (!acquired.ok) {
9742
11478
  const pid = acquired.existing?.pid;
9743
11479
  if (context.env.MORULA_DAEMON_SUPERVISED === "1") {
9744
11480
  context.err(`[daemon] \u5DF2\u6709\u5B9E\u4F8B\u5728\u8FD0\u884C\uFF08pid=${pid ?? "\u672A\u77E5"}\uFF09\uFF0C\u81EA\u542F\u5B9E\u4F8B\u793C\u8BA9\u9000\u51FA`);
11481
+ armBoundedExit();
9745
11482
  return { running: false, yieldedTo: pid ?? null };
9746
11483
  }
9747
11484
  throw new CliError(
@@ -9782,12 +11519,14 @@ async function run2(context) {
9782
11519
  if (isAuthRejected(error)) {
9783
11520
  context.err(`[daemon] \u6CE8\u518C\u88AB\u62D2\uFF08\u767B\u5F55\u6001\u5931\u6548\uFF09\uFF1A${error instanceof Error ? error.message : String(error)}`);
9784
11521
  context.err("[daemon] \u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u6B21\u542F\u52A8\uFF1Amorula-runtime auth login && morula-runtime daemon start");
11522
+ armBoundedExit();
9785
11523
  return { running: false, stoppedReason: "auth" };
9786
11524
  }
9787
11525
  throw error;
9788
11526
  } finally {
9789
11527
  releaseLock();
9790
11528
  }
11529
+ armBoundedExit();
9791
11530
  return { running: false };
9792
11531
  }
9793
11532
  function isAuthRejected(error) {
@@ -9810,10 +11549,10 @@ async function start(context) {
9810
11549
  }
9811
11550
  const dir = daemonDir(configFilePath(context.env));
9812
11551
  if (!dryRun)
9813
- mkdirSync12(dir, { recursive: true, mode: 448 });
9814
- const pidFile = join20(dir, PID_FILE);
9815
- const logFile = join20(dir, LOG_FILE);
9816
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
11552
+ mkdirSync14(dir, { recursive: true, mode: 448 });
11553
+ const pidFile = join22(dir, PID_FILE);
11554
+ const logFile = join22(dir, LOG_FILE);
11555
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
9817
11556
  const lock = probeDaemonLock(lockPath);
9818
11557
  if (lock.state === "active") {
9819
11558
  throw new CliError(
@@ -9832,7 +11571,7 @@ async function start(context) {
9832
11571
  });
9833
11572
  }
9834
11573
  if (!dryRun)
9835
- rmSync8(pidFile, { force: true });
11574
+ rmSync10(pidFile, { force: true });
9836
11575
  }
9837
11576
  const env = {
9838
11577
  ...process.env,
@@ -9870,12 +11609,12 @@ async function start(context) {
9870
11609
  const { spawnDaemonProcess: spawnDaemonProcess2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
9871
11610
  const result = spawnDaemonProcess2({ logFile, env });
9872
11611
  if (result.error || result.pid <= 0) {
9873
- rmSync8(pidFile, { force: true });
11612
+ rmSync10(pidFile, { force: true });
9874
11613
  throw new CliError("DAEMON_START_FAILED", EXIT.OTHER, `[daemon] \u542F\u52A8\u5931\u8D25: ${result.error ?? `pid=${result.pid}`}`, false, {
9875
11614
  hint: `tail -n 50 ${logFile}`
9876
11615
  });
9877
11616
  }
9878
- writeFileSync10(pidFile, String(result.pid), { mode: 384 });
11617
+ writeFileSync12(pidFile, String(result.pid), { mode: 384 });
9879
11618
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
9880
11619
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
9881
11620
  }
@@ -9883,8 +11622,8 @@ async function stop(context) {
9883
11622
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
9884
11623
  const dryRun = isDryRun(context);
9885
11624
  const dir = daemonDir(configFilePath(context.env));
9886
- const pidFile = join20(dir, PID_FILE);
9887
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
11625
+ const pidFile = join22(dir, PID_FILE);
11626
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
9888
11627
  const lock = probeDaemonLock(lockPath);
9889
11628
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
9890
11629
  const filePid = existsSync17(pidFile) ? Number(readFileSync11(pidFile, "utf8").trim()) : null;
@@ -9897,7 +11636,7 @@ async function stop(context) {
9897
11636
  };
9898
11637
  }
9899
11638
  if (!pid) {
9900
- rmSync8(pidFile, { force: true });
11639
+ rmSync10(pidFile, { force: true });
9901
11640
  releaseDaemonLock(lockPath);
9902
11641
  return { running: false, message: "daemon \u672A\u5728\u8FD0\u884C\uFF08\u65E0\u5B58\u6D3B pid\uFF09" };
9903
11642
  }
@@ -9906,7 +11645,7 @@ async function stop(context) {
9906
11645
  releaseDaemonLock(lockPath);
9907
11646
  else
9908
11647
  releaseDaemonLock(lockPath, pid);
9909
- rmSync8(pidFile, { force: true });
11648
+ rmSync10(pidFile, { force: true });
9910
11649
  if (result.outcome === "killed")
9911
11650
  context.err(`[daemon] pid=${pid} \u672A\u54CD\u5E94 SIGTERM\uFF0C\u5DF2 SIGKILL\uFF08\u9501\u5DF2\u91CA\u653E\uFF09`);
9912
11651
  return { running: false, stoppedPid: pid, outcome: result.outcome };
@@ -9915,21 +11654,17 @@ async function status2(context) {
9915
11654
  rejectUnknownFlags(context.parsed.flags, []);
9916
11655
  const cfg = loadDaemonConfig(context.store);
9917
11656
  const dir = daemonDir(configFilePath(context.env));
9918
- const pidFile = join20(dir, PID_FILE);
9919
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
9920
- const lock = probeDaemonLock(lockPath);
9921
- const lockPid = lock.state === "active" ? lock.record.pid : null;
9922
- const filePid = existsSync17(pidFile) ? Number(readFileSync11(pidFile, "utf8").trim() || 0) : 0;
9923
- const pid = lockPid ?? (isProcessAlive(filePid) ? filePid : null);
11657
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
11658
+ const probe = probeDaemonInstance(dirname10(configFilePath(context.env)));
9924
11659
  return {
9925
- running: pid !== null,
9926
- pid,
11660
+ running: probe.running,
11661
+ pid: probe.pid,
9927
11662
  daemonId: cfg?.daemonId ?? null,
9928
11663
  runtimeId: cfg?.runtimeId ?? null,
9929
11664
  name: cfg?.name ?? null,
9930
11665
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
9931
11666
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
9932
- logFile: join20(dir, LOG_FILE),
11667
+ logFile: join22(dir, LOG_FILE),
9933
11668
  lockFile: lockPath
9934
11669
  };
9935
11670
  }
@@ -9957,95 +11692,6 @@ function parseMaxConcurrent(context) {
9957
11692
 
9958
11693
  // src/commands/setup.ts
9959
11694
  init_dist2();
9960
-
9961
- // src/daemon/plugin-register.ts
9962
- import { chmodSync as chmodSync3, cpSync, existsSync as existsSync18, mkdirSync as mkdirSync13, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync9, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
9963
- import { homedir as homedir13 } from "node:os";
9964
- import { dirname as dirname10, join as join21 } from "node:path";
9965
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9966
- var PLUGIN_NAME = "morula-cli";
9967
- var MANIFEST_DIR_NAME = ".codex-plugin";
9968
- function legacyAgentsPluginDir(home = homedir13()) {
9969
- return join21(home, ".agents", "plugins", PLUGIN_NAME);
9970
- }
9971
- function resolveDimcodeHome(env = process.env, home = homedir13()) {
9972
- const fromEnv = env.DIMCODE_HOME?.trim();
9973
- return fromEnv || join21(home, ".dimcode", "v2");
9974
- }
9975
- function defaultPluginsRoot(env = process.env, home = homedir13()) {
9976
- return join21(resolveDimcodeHome(env, home), "plugins");
9977
- }
9978
- function resolvePackagePaths(entryUrl) {
9979
- const distDir = dirname10(fileURLToPath2(entryUrl));
9980
- return {
9981
- packageRoot: dirname10(distDir),
9982
- distDir,
9983
- assetsDir: join21(distDir, "plugin")
9984
- };
9985
- }
9986
- function buildLauncherScript(opts) {
9987
- const entry = join21(opts.packageRoot, "dist", "morula.js");
9988
- return `#!/bin/sh
9989
- # Morula \u4E1A\u52A1 CLI \u542F\u52A8\u5668\uFF08\u7531 \`morula-runtime setup\` \u751F\u6210\uFF1BNode \u5347\u7EA7\u540E\u91CD\u8DD1 setup \u5237\u65B0\uFF09\u3002
9990
- # \u4E0D\u4F9D\u8D56\u8C03\u7528\u65B9 PATH \u91CC\u7684 node\uFF08agent \u6267\u884C\u73AF\u5883 PATH \u5E38\u88AB\u88C1\u526A\uFF09\uFF0C\u4EC5\u4F5C\u515C\u5E95\u3002
9991
- NODE=${JSON.stringify(opts.nodePath)}
9992
- [ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
9993
- if [ -z "$NODE" ]; then
9994
- echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
9995
- exit 127
9996
- fi
9997
- exec "$NODE" ${JSON.stringify(entry)} "$@"
9998
- `;
9999
- }
10000
- function assetsReady(paths) {
10001
- return existsSync18(join21(paths.assetsDir, "plugin.json")) && existsSync18(join21(paths.assetsDir, "skills"));
10002
- }
10003
- function listSkillNames(assetsDir) {
10004
- const dir = join21(assetsDir, "skills");
10005
- if (!existsSync18(dir))
10006
- return [];
10007
- return readdirSync5(dir).filter((name) => {
10008
- try {
10009
- return statSync4(join21(dir, name)).isDirectory();
10010
- } catch {
10011
- return false;
10012
- }
10013
- }).sort();
10014
- }
10015
- function registerPlugin(opts) {
10016
- const { pluginsRoot, paths, version } = opts;
10017
- const nodePath = opts.nodePath ?? process.execPath;
10018
- const pluginDir = join21(pluginsRoot, PLUGIN_NAME);
10019
- if (!assetsReady(paths))
10020
- throw new Error(`plugin assets not found at ${paths.assetsDir}; run \`pnpm run build:dist\` (dev) or reinstall the package`);
10021
- const skillsSrc = join21(paths.assetsDir, "skills");
10022
- const skills = listSkillNames(paths.assetsDir);
10023
- const manifestDir = join21(pluginDir, MANIFEST_DIR_NAME);
10024
- mkdirSync13(manifestDir, { recursive: true });
10025
- const manifest = JSON.parse(readFileSync12(join21(paths.assetsDir, "plugin.json"), "utf8"));
10026
- writeFileSync11(join21(manifestDir, "plugin.json"), `${JSON.stringify({ ...manifest, version }, null, 2)}
10027
- `);
10028
- const skillsDest = join21(pluginDir, "skills");
10029
- rmSync9(skillsDest, { recursive: true, force: true });
10030
- mkdirSync13(skillsDest, { recursive: true });
10031
- cpSync(skillsSrc, skillsDest, { recursive: true });
10032
- const binDir = join21(pluginDir, "bin");
10033
- mkdirSync13(binDir, { recursive: true });
10034
- const binPath = join21(binDir, "morula");
10035
- writeFileSync11(binPath, buildLauncherScript({ nodePath, packageRoot: paths.packageRoot }));
10036
- chmodSync3(binPath, 493);
10037
- const legacy = legacyAgentsPluginDir(opts.home);
10038
- return {
10039
- pluginDir,
10040
- binPath,
10041
- skills,
10042
- version,
10043
- node: nodePath,
10044
- legacyDir: existsSync18(join21(legacy, MANIFEST_DIR_NAME)) ? legacy : null
10045
- };
10046
- }
10047
-
10048
- // src/commands/setup.ts
10049
11695
  var KNOWN_FLAGS = ["root", "help"];
10050
11696
  async function setupCommand(context) {
10051
11697
  rejectUnknownFlags(context.parsed.flags, KNOWN_FLAGS);