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

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 = "20260921065825.nogit";
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,6 +886,93 @@ 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
830
977
  function sanitizeHeaderValue(raw, maxLength) {
831
978
  if (!raw)
@@ -870,28 +1017,35 @@ function isEnvelope(value) {
870
1017
  function errorFromResponse(status3, parsed, raw) {
871
1018
  const message = extractMessage(parsed, raw, status3);
872
1019
  const details = isEnvelope(parsed) ? parsed.data : parsed;
1020
+ const errorCode = extractErrorCode(parsed);
873
1021
  if (status3 === 400 || status3 === 422) {
874
- return new MorulaApiError("BAD_REQUEST", message, EXIT_CODE.USAGE, status3, false, details);
1022
+ return new MorulaApiError("BAD_REQUEST", message, EXIT_CODE.USAGE, status3, false, details, errorCode);
875
1023
  }
876
1024
  if (status3 === 401) {
877
- return new MorulaApiError("UNAUTHENTICATED", message, EXIT_CODE.UNAUTHENTICATED, status3, false, details);
1025
+ return new MorulaApiError("UNAUTHENTICATED", message, EXIT_CODE.UNAUTHENTICATED, status3, false, details, errorCode);
878
1026
  }
879
1027
  if (status3 === 403) {
880
- return new MorulaApiError("FORBIDDEN", message, EXIT_CODE.FORBIDDEN, status3, false, details);
1028
+ return new MorulaApiError("FORBIDDEN", message, EXIT_CODE.FORBIDDEN, status3, false, details, errorCode);
881
1029
  }
882
1030
  if (status3 === 404) {
883
- return new MorulaApiError("NOT_FOUND", message, EXIT_CODE.NOT_FOUND, status3, false, details);
1031
+ return new MorulaApiError("NOT_FOUND", message, EXIT_CODE.NOT_FOUND, status3, false, details, errorCode);
884
1032
  }
885
1033
  if (status3 === 409) {
886
- return new MorulaApiError("CONFLICT", message, EXIT_CODE.CONFLICT, status3, false, details);
1034
+ return new MorulaApiError("CONFLICT", message, EXIT_CODE.CONFLICT, status3, false, details, errorCode);
887
1035
  }
888
1036
  if (status3 === 429) {
889
- return new MorulaApiError("RATE_LIMITED", message, EXIT_CODE.UPSTREAM, status3, true, details);
1037
+ return new MorulaApiError("RATE_LIMITED", message, EXIT_CODE.UPSTREAM, status3, true, details, errorCode);
890
1038
  }
891
1039
  if (status3 >= 500) {
892
- return new MorulaApiError("UPSTREAM", message, EXIT_CODE.UPSTREAM, status3, true, details);
1040
+ return new MorulaApiError("UPSTREAM", message, EXIT_CODE.UPSTREAM, status3, true, details, errorCode);
893
1041
  }
894
- return new MorulaApiError("HTTP_ERROR", message, EXIT_CODE.OTHER, status3, false, details);
1042
+ return new MorulaApiError("HTTP_ERROR", message, EXIT_CODE.OTHER, status3, false, details, errorCode);
1043
+ }
1044
+ function extractErrorCode(parsed) {
1045
+ if (!parsed || typeof parsed !== "object")
1046
+ return void 0;
1047
+ const value = parsed.errorCode;
1048
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
895
1049
  }
896
1050
  function extractMessage(parsed, raw, status3) {
897
1051
  if (parsed && typeof parsed === "object" && "message" in parsed) {
@@ -927,14 +1081,16 @@ var init_client = __esm({
927
1081
  status;
928
1082
  retryable;
929
1083
  details;
1084
+ errorCode;
930
1085
  name = "MorulaApiError";
931
- constructor(code, message, exitCode, status3, retryable = false, details) {
1086
+ constructor(code, message, exitCode, status3, retryable = false, details, errorCode) {
932
1087
  super(message);
933
1088
  this.code = code;
934
1089
  this.exitCode = exitCode;
935
1090
  this.status = status3;
936
1091
  this.retryable = retryable;
937
1092
  this.details = details;
1093
+ this.errorCode = errorCode;
938
1094
  }
939
1095
  };
940
1096
  CLI_BUILD_HEADER = "X-Morula-Cli-Build";
@@ -1061,7 +1217,7 @@ var init_client = __esm({
1061
1217
  });
1062
1218
 
1063
1219
  // ../morula-api-client/dist/comment.js
1064
- function requiredId(id) {
1220
+ function requiredId3(id) {
1065
1221
  const normalized = id.trim();
1066
1222
  if (!normalized)
1067
1223
  throw new TypeError("comment id is required");
@@ -1083,23 +1239,142 @@ var init_comment = __esm({
1083
1239
  return this.client.get("/comments", params);
1084
1240
  }
1085
1241
  get(id) {
1086
- return this.client.get(`/comments/${encodeURIComponent(requiredId(id))}`);
1242
+ return this.client.get(`/comments/${encodeURIComponent(requiredId3(id))}`);
1087
1243
  }
1088
1244
  create(input) {
1089
1245
  return this.client.post("/comments", input);
1090
1246
  }
1091
1247
  update(id, input) {
1092
- return this.client.patch(`/comments/${encodeURIComponent(requiredId(id))}`, input);
1248
+ return this.client.patch(`/comments/${encodeURIComponent(requiredId3(id))}`, input);
1093
1249
  }
1094
1250
  delete(id) {
1095
- return this.client.delete(`/comments/${encodeURIComponent(requiredId(id))}`);
1251
+ return this.client.delete(`/comments/${encodeURIComponent(requiredId3(id))}`);
1252
+ }
1253
+ };
1254
+ }
1255
+ });
1256
+
1257
+ // ../morula-api-client/dist/dashboard.js
1258
+ function requiredId4(id) {
1259
+ const normalized = id.trim();
1260
+ if (!normalized)
1261
+ throw new TypeError("space id is required");
1262
+ return normalized;
1263
+ }
1264
+ var DashboardApi;
1265
+ var init_dashboard = __esm({
1266
+ "../morula-api-client/dist/dashboard.js"() {
1267
+ "use strict";
1268
+ DashboardApi = class {
1269
+ client;
1270
+ constructor(client) {
1271
+ this.client = client;
1272
+ }
1273
+ get(spaceId) {
1274
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/dashboard`);
1275
+ }
1276
+ statistics(spaceId) {
1277
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/statistics`);
1278
+ }
1279
+ workload(spaceId) {
1280
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId4(spaceId))}/workload`);
1281
+ }
1282
+ };
1283
+ }
1284
+ });
1285
+
1286
+ // ../morula-api-client/dist/document.js
1287
+ function requiredId5(id) {
1288
+ const normalized = String(id).trim();
1289
+ if (!normalized)
1290
+ throw new TypeError("document id is required");
1291
+ return normalized;
1292
+ }
1293
+ var DocumentApi;
1294
+ var init_document = __esm({
1295
+ "../morula-api-client/dist/document.js"() {
1296
+ "use strict";
1297
+ DocumentApi = class {
1298
+ client;
1299
+ constructor(client) {
1300
+ this.client = client;
1301
+ }
1302
+ list(query = {}) {
1303
+ return this.client.get("/documents", query);
1304
+ }
1305
+ get(id) {
1306
+ return this.client.get(`/documents/${encodeURIComponent(requiredId5(id))}`);
1307
+ }
1308
+ /** 已解析归档的文档内容(Markdown 正文等) */
1309
+ artifacts(id) {
1310
+ return this.client.get(`/documents/${encodeURIComponent(requiredId5(id))}/artifacts`);
1311
+ }
1312
+ };
1313
+ }
1314
+ });
1315
+
1316
+ // ../morula-api-client/dist/object-meta.js
1317
+ function requiredId6(id) {
1318
+ const normalized = id.trim();
1319
+ if (!normalized)
1320
+ throw new TypeError("object id is required");
1321
+ return normalized;
1322
+ }
1323
+ function requiredKey(key) {
1324
+ const normalized = key.trim();
1325
+ if (!normalized)
1326
+ throw new TypeError("meta key is required");
1327
+ return normalized;
1328
+ }
1329
+ var ObjectMetaApi;
1330
+ var init_object_meta = __esm({
1331
+ "../morula-api-client/dist/object-meta.js"() {
1332
+ "use strict";
1333
+ ObjectMetaApi = class {
1334
+ client;
1335
+ constructor(client) {
1336
+ this.client = client;
1337
+ }
1338
+ list(objectId) {
1339
+ return this.client.get(`/objects/${encodeURIComponent(requiredId6(objectId))}/agent-meta`);
1340
+ }
1341
+ set(objectId, key, value) {
1342
+ return this.client.request(`/objects/${encodeURIComponent(requiredId6(objectId))}/agent-meta/${encodeURIComponent(requiredKey(key))}`, { method: "PUT", body: { value } });
1343
+ }
1344
+ };
1345
+ }
1346
+ });
1347
+
1348
+ // ../morula-api-client/dist/repo.js
1349
+ function requiredId7(id) {
1350
+ const normalized = id.trim();
1351
+ if (!normalized)
1352
+ throw new TypeError("space id is required");
1353
+ return normalized;
1354
+ }
1355
+ var RepoApi;
1356
+ var init_repo = __esm({
1357
+ "../morula-api-client/dist/repo.js"() {
1358
+ "use strict";
1359
+ RepoApi = class {
1360
+ client;
1361
+ constructor(client) {
1362
+ this.client = client;
1363
+ }
1364
+ /** 目录清单(path 缺省 = 仓库根) */
1365
+ files(spaceId, query = {}) {
1366
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId7(spaceId))}/repo/files`, query);
1367
+ }
1368
+ /** 单文件内容(path 必填) */
1369
+ read(spaceId, query) {
1370
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId7(spaceId))}/repo/file`, query);
1096
1371
  }
1097
1372
  };
1098
1373
  }
1099
1374
  });
1100
1375
 
1101
1376
  // ../morula-api-client/dist/report.js
1102
- function requiredId2(id, label = "report") {
1377
+ function requiredId8(id, label = "report") {
1103
1378
  const normalized = id.trim();
1104
1379
  if (!normalized)
1105
1380
  throw new TypeError(`${label} id is required`);
@@ -1118,17 +1393,17 @@ var init_report = __esm({
1118
1393
  return this.client.post("/agent-reports", input);
1119
1394
  }
1120
1395
  get(id) {
1121
- return this.client.get(`/agent-reports/${encodeURIComponent(requiredId2(id))}`);
1396
+ return this.client.get(`/agent-reports/${encodeURIComponent(requiredId8(id))}`);
1122
1397
  }
1123
1398
  listByAutopilot(autopilotId, query = {}) {
1124
- return this.client.get(`/agent-autopilots/${encodeURIComponent(requiredId2(autopilotId, "autopilot"))}/reports`, { ...query });
1399
+ return this.client.get(`/agent-autopilots/${encodeURIComponent(requiredId8(autopilotId, "autopilot"))}/reports`, { ...query });
1125
1400
  }
1126
1401
  };
1127
1402
  }
1128
1403
  });
1129
1404
 
1130
1405
  // ../morula-api-client/dist/space-member.js
1131
- function requiredId3(id) {
1406
+ function requiredId9(id) {
1132
1407
  const normalized = id.trim();
1133
1408
  if (!normalized)
1134
1409
  throw new TypeError("space member id is required");
@@ -1147,23 +1422,23 @@ var init_space_member = __esm({
1147
1422
  return this.client.get("/space-members", { spaceId, ...query });
1148
1423
  }
1149
1424
  get(id) {
1150
- return this.client.get(`/space-members/${encodeURIComponent(requiredId3(id))}`);
1425
+ return this.client.get(`/space-members/${encodeURIComponent(requiredId9(id))}`);
1151
1426
  }
1152
1427
  create(input) {
1153
1428
  return this.client.post("/space-members", input);
1154
1429
  }
1155
1430
  update(id, input) {
1156
- return this.client.patch(`/space-members/${encodeURIComponent(requiredId3(id))}`, input);
1431
+ return this.client.patch(`/space-members/${encodeURIComponent(requiredId9(id))}`, input);
1157
1432
  }
1158
1433
  delete(id) {
1159
- return this.client.delete(`/space-members/${encodeURIComponent(requiredId3(id))}`);
1434
+ return this.client.delete(`/space-members/${encodeURIComponent(requiredId9(id))}`);
1160
1435
  }
1161
1436
  };
1162
1437
  }
1163
1438
  });
1164
1439
 
1165
1440
  // ../morula-api-client/dist/space.js
1166
- function requiredId4(id) {
1441
+ function requiredId10(id) {
1167
1442
  const normalized = id.trim();
1168
1443
  if (!normalized)
1169
1444
  throw new TypeError("space id is required");
@@ -1182,23 +1457,51 @@ var init_space = __esm({
1182
1457
  return this.client.get("/spaces", query);
1183
1458
  }
1184
1459
  get(id) {
1185
- return this.client.get(`/spaces/${encodeURIComponent(requiredId4(id))}`);
1460
+ return this.client.get(`/spaces/${encodeURIComponent(requiredId10(id))}`);
1186
1461
  }
1187
1462
  create(input) {
1188
1463
  return this.client.post("/spaces", input);
1189
1464
  }
1190
1465
  update(id, input) {
1191
- return this.client.patch(`/spaces/${encodeURIComponent(requiredId4(id))}`, input);
1466
+ return this.client.patch(`/spaces/${encodeURIComponent(requiredId10(id))}`, input);
1192
1467
  }
1193
1468
  delete(id) {
1194
- return this.client.delete(`/spaces/${encodeURIComponent(requiredId4(id))}`);
1469
+ return this.client.delete(`/spaces/${encodeURIComponent(requiredId10(id))}`);
1470
+ }
1471
+ };
1472
+ }
1473
+ });
1474
+
1475
+ // ../morula-api-client/dist/steward.js
1476
+ function requiredId11(id) {
1477
+ const normalized = id.trim();
1478
+ if (!normalized)
1479
+ throw new TypeError("project id is required");
1480
+ return normalized;
1481
+ }
1482
+ var StewardApi;
1483
+ var init_steward = __esm({
1484
+ "../morula-api-client/dist/steward.js"() {
1485
+ "use strict";
1486
+ StewardApi = class {
1487
+ client;
1488
+ constructor(client) {
1489
+ this.client = client;
1490
+ }
1491
+ /** 管家信息(未接管的空间返回 null) */
1492
+ get(projectId) {
1493
+ return this.client.get(`/stewards/projects/${encodeURIComponent(requiredId11(projectId))}`);
1494
+ }
1495
+ /** 派单:建任务并派发(目标状态由 state 决定,backlog 不派发执行) */
1496
+ dispatch(projectId, input) {
1497
+ return this.client.post(`/stewards/projects/${encodeURIComponent(requiredId11(projectId))}/dispatch`, input);
1195
1498
  }
1196
1499
  };
1197
1500
  }
1198
1501
  });
1199
1502
 
1200
1503
  // ../morula-api-client/dist/task.js
1201
- function requiredId5(id) {
1504
+ function requiredId12(id) {
1202
1505
  const normalized = id.trim();
1203
1506
  if (!normalized)
1204
1507
  throw new TypeError("task id is required");
@@ -1217,16 +1520,16 @@ var init_task = __esm({
1217
1520
  return this.client.get("/objects", query);
1218
1521
  }
1219
1522
  get(id) {
1220
- return this.client.get(`/objects/${encodeURIComponent(requiredId5(id))}`);
1523
+ return this.client.get(`/objects/${encodeURIComponent(requiredId12(id))}`);
1221
1524
  }
1222
1525
  create(input) {
1223
1526
  return this.client.post("/objects", input);
1224
1527
  }
1225
1528
  update(id, input) {
1226
- return this.client.patch(`/objects/${encodeURIComponent(requiredId5(id))}`, input);
1529
+ return this.client.patch(`/objects/${encodeURIComponent(requiredId12(id))}`, input);
1227
1530
  }
1228
1531
  delete(id) {
1229
- return this.client.delete(`/objects/${encodeURIComponent(requiredId5(id))}`);
1532
+ return this.client.delete(`/objects/${encodeURIComponent(requiredId12(id))}`);
1230
1533
  }
1231
1534
  /** 查询空间字段模板(字段 key → id 映射、options) */
1232
1535
  getSchemaFields(spaceId) {
@@ -1293,44 +1596,76 @@ var MorulaClient;
1293
1596
  var init_dist = __esm({
1294
1597
  "../morula-api-client/dist/index.js"() {
1295
1598
  "use strict";
1599
+ init_agent_task();
1296
1600
  init_attachment();
1297
1601
  init_auth();
1602
+ init_autopilot();
1603
+ init_ci();
1298
1604
  init_client();
1299
1605
  init_comment();
1606
+ init_dashboard();
1607
+ init_document();
1608
+ init_object_meta();
1609
+ init_repo();
1300
1610
  init_report();
1301
1611
  init_space_member();
1302
1612
  init_space();
1613
+ init_steward();
1303
1614
  init_task();
1304
1615
  init_webhook();
1616
+ init_agent_task();
1305
1617
  init_attachment();
1306
1618
  init_auth();
1619
+ init_autopilot();
1620
+ init_ci();
1307
1621
  init_client();
1308
1622
  init_comment();
1623
+ init_dashboard();
1624
+ init_document();
1625
+ init_object_meta();
1626
+ init_repo();
1309
1627
  init_report();
1310
1628
  init_space_member();
1311
1629
  init_space();
1630
+ init_steward();
1312
1631
  init_task();
1313
1632
  init_types();
1314
1633
  init_webhook();
1315
1634
  MorulaClient = class {
1316
1635
  http;
1636
+ agentTasks;
1317
1637
  attachments;
1318
1638
  auth;
1639
+ autopilots;
1640
+ ci;
1319
1641
  comments;
1642
+ dashboard;
1643
+ documents;
1644
+ objectMeta;
1320
1645
  reports;
1646
+ repos;
1321
1647
  tasks;
1322
1648
  spaces;
1323
1649
  spaceMembers;
1650
+ stewards;
1324
1651
  webhooks;
1325
1652
  constructor(options) {
1326
1653
  this.http = new MorulaApiClient(options);
1654
+ this.agentTasks = new AgentTaskApi(this.http);
1327
1655
  this.attachments = new AttachmentApi(this.http);
1328
1656
  this.auth = new AuthApi(this.http);
1657
+ this.autopilots = new AutopilotApi(this.http);
1658
+ this.ci = new CiApi(this.http);
1329
1659
  this.comments = new CommentApi(this.http);
1660
+ this.dashboard = new DashboardApi(this.http);
1661
+ this.documents = new DocumentApi(this.http);
1662
+ this.objectMeta = new ObjectMetaApi(this.http);
1330
1663
  this.reports = new ReportApi(this.http);
1664
+ this.repos = new RepoApi(this.http);
1331
1665
  this.tasks = new TaskApi(this.http);
1332
1666
  this.spaces = new SpaceApi(this.http);
1333
1667
  this.spaceMembers = new SpaceMemberApi(this.http);
1668
+ this.stewards = new StewardApi(this.http);
1334
1669
  this.webhooks = new WebhookApi(this.http);
1335
1670
  }
1336
1671
  };
@@ -1384,7 +1719,8 @@ function renderActionHelp(base, resource, action) {
1384
1719
  ...f.valueHint ? { valueHint: f.valueHint } : {},
1385
1720
  required: f.required ?? false,
1386
1721
  ...f.enum ? { enum: f.enum } : {},
1387
- ...f.default !== void 0 ? { default: f.default } : {}
1722
+ ...f.default !== void 0 ? { default: f.default } : {},
1723
+ ...f.repeatable ? { repeatable: true } : {}
1388
1724
  })),
1389
1725
  examples: action.examples ?? [],
1390
1726
  writes: action.writes ?? "none",
@@ -1522,6 +1858,16 @@ function validateArgs(resource, action, parsed, options = {}) {
1522
1858
  hint: `${usageHint} (e.g. --${flag} ${valuePart})`
1523
1859
  });
1524
1860
  }
1861
+ for (const [name, values] of Object.entries(parsed.repeatedFlags)) {
1862
+ if (values.length < 2)
1863
+ continue;
1864
+ const spec = declaredFlags.find((f) => f.name === name);
1865
+ if (spec?.repeatable)
1866
+ continue;
1867
+ throw usage(`duplicate flag --${name} (given ${values.length} times; only the last value would be used)`, {
1868
+ hint: usageHint
1869
+ });
1870
+ }
1525
1871
  for (const spec of declaredFlags) {
1526
1872
  if (!spec.required)
1527
1873
  continue;
@@ -1631,6 +1977,8 @@ function resolveToken(flags, env, context, baseUrl, saved, options) {
1631
1977
  return { token: explicitToken, tokenSource: "flag" };
1632
1978
  if (!options.excludeTaskToken && env.MORULA_TASK_TOKEN)
1633
1979
  return { token: env.MORULA_TASK_TOKEN, tokenSource: "env:MORULA_TASK_TOKEN" };
1980
+ if (!options.excludeTaskToken && env.MORULA_SESSION_TOKEN)
1981
+ return { token: env.MORULA_SESSION_TOKEN, tokenSource: "env:MORULA_SESSION_TOKEN" };
1634
1982
  if (env.MORULA_TOKEN)
1635
1983
  return { token: env.MORULA_TOKEN, tokenSource: "env:MORULA_TOKEN" };
1636
1984
  const savedToken = saved?.url && normalizeBaseUrl(saved.url) === baseUrl ? saved.token : void 0;
@@ -1775,6 +2123,10 @@ async function run(argv, deps = {}) {
1775
2123
  code: error.code,
1776
2124
  message: error.message,
1777
2125
  ...error.retryable ? { retryable: true } : {},
2126
+ // 服务端业务错误码(如 SESSION_SCOPE_DENIED):`code` 是按 HTTP 状态派生的粗档位,
2127
+ // 行为契约要判断的是业务码——不打印出来,契约就永远不可执行
2128
+ // (issues/api-client-drops-business-error-code.md)。退出码仍由 `error.exitCode` 决定。
2129
+ ...error instanceof MorulaApiError && error.errorCode ? { errorCode: error.errorCode } : {},
1778
2130
  // 新增字段(可选):可执行的下一步 + typo 建议
1779
2131
  ...error instanceof CliError && error.hint ? { hint: error.hint } : {},
1780
2132
  ...error instanceof CliError && error.didYouMean ? { didYouMean: error.didYouMean } : {}
@@ -2293,8 +2645,27 @@ var init_daemon_config = __esm({
2293
2645
 
2294
2646
  // src/daemon/daemon-lock.ts
2295
2647
  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";
2648
+ 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
2649
  import { dirname as dirname4, join as join6 } from "node:path";
2650
+ function daemonPidPath(configDir) {
2651
+ return join6(configDir, "daemon", DAEMON_PID_FILE);
2652
+ }
2653
+ function readPidFile(file) {
2654
+ if (!existsSync5(file))
2655
+ return 0;
2656
+ try {
2657
+ return Number(readFileSync2(file, "utf8").trim() || 0);
2658
+ } catch {
2659
+ return 0;
2660
+ }
2661
+ }
2662
+ function probeDaemonInstance(configDir) {
2663
+ const lock = probeDaemonLock(join6(configDir, "daemon", DAEMON_LOCK_FILE));
2664
+ const lockPid = lock.state === "active" ? lock.record.pid : 0;
2665
+ const filePid = readPidFile(daemonPidPath(configDir));
2666
+ const pid = lockPid || (isProcessAlive(filePid) ? filePid : 0);
2667
+ return { running: pid > 0, pid: pid > 0 ? pid : null, lockState: lock.state };
2668
+ }
2298
2669
  function isProcessAlive(pid) {
2299
2670
  if (!pid || pid <= 0)
2300
2671
  return false;
@@ -2444,11 +2815,12 @@ async function stopDaemonByPid(pid, options = {}) {
2444
2815
  await waitForExit(pid, 2e3, pollMs);
2445
2816
  return { pid, outcome: "killed" };
2446
2817
  }
2447
- var DAEMON_LOCK_FILE;
2818
+ var DAEMON_LOCK_FILE, DAEMON_PID_FILE;
2448
2819
  var init_daemon_lock = __esm({
2449
2820
  "src/daemon/daemon-lock.ts"() {
2450
2821
  "use strict";
2451
2822
  DAEMON_LOCK_FILE = "daemon.lock";
2823
+ DAEMON_PID_FILE = "daemon.pid";
2452
2824
  }
2453
2825
  });
2454
2826
 
@@ -2458,10 +2830,9 @@ __export(portal_instances_exports, {
2458
2830
  candidateConfigFiles: () => candidateConfigFiles,
2459
2831
  createPortalEnvironmentResolver: () => createPortalEnvironmentResolver,
2460
2832
  normalizeBaseUrl: () => normalizeBaseUrl2,
2461
- parseBaseUrl: () => parseBaseUrl,
2462
- readPidFile: () => readPidFile
2833
+ parseBaseUrl: () => parseBaseUrl
2463
2834
  });
2464
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
2835
+ import { readdirSync, statSync } from "node:fs";
2465
2836
  import { homedir as homedir5 } from "node:os";
2466
2837
  import { dirname as dirname5, join as join7 } from "node:path";
2467
2838
  function parseBaseUrl(raw) {
@@ -2527,16 +2898,6 @@ function readContexts(file) {
2527
2898
  return null;
2528
2899
  }
2529
2900
  }
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
2901
  function createPortalEnvironmentResolver(options = {}) {
2541
2902
  const env = options.env ?? process.env;
2542
2903
  const log = options.log ?? (() => {
@@ -2565,7 +2926,7 @@ function createPortalEnvironmentResolver(options = {}) {
2565
2926
  baseUrl: entry.url,
2566
2927
  normalized,
2567
2928
  token: entry.token,
2568
- hasLiveDaemon: isProcessAlive(readPidFile(configDir)),
2929
+ hasLiveDaemon: probeDaemonInstance(configDir).running,
2569
2930
  isDefaultDir: configDir === defaultConfigDir,
2570
2931
  configMtimeMs: configMtimeMs(file)
2571
2932
  });
@@ -2611,6 +2972,25 @@ function createPortalEnvironmentResolver(options = {}) {
2611
2972
  return void 0;
2612
2973
  }
2613
2974
  },
2975
+ daemonId: () => {
2976
+ try {
2977
+ return loadDaemonConfig(new FileConfigStore(join7(instance.configDir, "config.yaml")))?.daemonId;
2978
+ } catch {
2979
+ return void 0;
2980
+ }
2981
+ },
2982
+ isDeviceOnline: async (daemonId) => {
2983
+ if (!instance.token)
2984
+ return null;
2985
+ try {
2986
+ const client = createMorulaClient({ baseUrl: instance.baseUrl, token: instance.token, fetch: options.fetch });
2987
+ const rows = await client.http.get("/agent-runtimes");
2988
+ return (Array.isArray(rows) ? rows : []).some((row) => row.daemonId === daemonId && row.status === "online");
2989
+ } catch (error) {
2990
+ log(`[portal] \u67E5\u8BE2 ${instance.context} \u7684\u8BBE\u5907\u5728\u7EBF\u72B6\u6001\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
2991
+ return null;
2992
+ }
2993
+ },
2614
2994
  validateToken: async (token) => {
2615
2995
  if (!instance.token)
2616
2996
  return false;
@@ -2669,9 +3049,13 @@ var init_portal_instances = __esm({
2669
3049
  var portal_server_exports = {};
2670
3050
  __export(portal_server_exports, {
2671
3051
  DEFAULT_PORTAL_PORT: () => DEFAULT_PORTAL_PORT,
2672
- PortalServer: () => PortalServer
3052
+ PortalServer: () => PortalServer,
3053
+ REGISTRATION_CONFIRM_TIMEOUT_MS: () => REGISTRATION_CONFIRM_TIMEOUT_MS,
3054
+ START_CONFIRM_TIMEOUT_MS: () => START_CONFIRM_TIMEOUT_MS,
3055
+ waitForDaemonRegistration: () => waitForDaemonRegistration,
3056
+ waitForDaemonStart: () => waitForDaemonStart
2673
3057
  });
2674
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
3058
+ import { readFileSync as readFileSync3, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "node:fs";
2675
3059
  import { createServer as createServer2 } from "node:http";
2676
3060
  import { homedir as homedir6 } from "node:os";
2677
3061
  import { join as join8 } from "node:path";
@@ -2679,35 +3063,61 @@ function stateDir() {
2679
3063
  return process.env.MORULA_CONFIG_DIR ?? join8(homedir6(), ".morula-cli");
2680
3064
  }
2681
3065
  function pidFileIn(configDir) {
2682
- return join8(configDir, "daemon", "daemon.pid");
3066
+ return join8(configDir, "daemon", DAEMON_PID_FILE);
2683
3067
  }
2684
3068
  function daemonLogIn(configDir) {
2685
3069
  return join8(configDir, "daemon", "daemon.log");
2686
3070
  }
2687
- function isPidAlive(pid) {
2688
- if (!pid || pid <= 0)
2689
- return false;
3071
+ async function waitForDaemonStart(configDir, childPid, timeoutMs = START_CONFIRM_TIMEOUT_MS) {
3072
+ const deadline = Date.now() + timeoutMs;
3073
+ for (; ; ) {
3074
+ const probe = probeDaemonInstance(configDir);
3075
+ if (probe.running)
3076
+ return { running: true, pid: probe.pid };
3077
+ if (!isProcessAlive(childPid))
3078
+ return { running: false, reason: "exited" };
3079
+ if (Date.now() >= deadline)
3080
+ return { running: false, reason: "timeout" };
3081
+ await new Promise((resolve2) => setTimeout(resolve2, START_CONFIRM_POLL_MS));
3082
+ }
3083
+ }
3084
+ async function waitForDaemonRegistration(isDeviceOnline, daemonId, childPid, timeoutMs = REGISTRATION_CONFIRM_TIMEOUT_MS) {
3085
+ const deadline = Date.now() + timeoutMs;
3086
+ let queryFailed = false;
3087
+ for (; ; ) {
3088
+ const online = await isDeviceOnline(daemonId);
3089
+ if (online === true)
3090
+ return "registered";
3091
+ if (online === null)
3092
+ queryFailed = true;
3093
+ if (!isProcessAlive(childPid))
3094
+ return "exited";
3095
+ if (Date.now() >= deadline)
3096
+ return queryFailed ? "unknown" : "not-registered";
3097
+ await new Promise((resolve2) => setTimeout(resolve2, START_CONFIRM_POLL_MS));
3098
+ }
3099
+ }
3100
+ function tailDaemonLog(configDir, maxLines = 5) {
2690
3101
  try {
2691
- process.kill(pid, 0);
2692
- return true;
3102
+ const lines = readFileSync3(daemonLogIn(configDir), "utf8").trimEnd().split("\n");
3103
+ return lines.slice(-maxLines).join(" / ") || null;
2693
3104
  } catch {
2694
- return false;
3105
+ return null;
2695
3106
  }
2696
3107
  }
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;
3108
+ var DEFAULT_PORTAL_PORT, DEFAULT_CORS_ORIGINS, START_CONFIRM_TIMEOUT_MS, REGISTRATION_CONFIRM_TIMEOUT_MS, START_CONFIRM_POLL_MS, PortalServer;
2703
3109
  var init_portal_server = __esm({
2704
3110
  "src/daemon/portal-server.ts"() {
2705
3111
  "use strict";
2706
3112
  init_daemon_config();
3113
+ init_daemon_lock();
2707
3114
  init_portal_instances();
2708
3115
  init_spawn();
2709
3116
  DEFAULT_PORTAL_PORT = 23491;
2710
3117
  DEFAULT_CORS_ORIGINS = ["*"];
3118
+ START_CONFIRM_TIMEOUT_MS = 5e3;
3119
+ REGISTRATION_CONFIRM_TIMEOUT_MS = 5e3;
3120
+ START_CONFIRM_POLL_MS = 200;
2711
3121
  PortalServer = class {
2712
3122
  port;
2713
3123
  server = null;
@@ -2848,14 +3258,13 @@ var init_portal_server = __esm({
2848
3258
  if (failed)
2849
3259
  return;
2850
3260
  const configDir = target?.configDir ?? stateDir();
2851
- const pid = readPidFrom(pidFileIn(configDir));
2852
- const running = isPidAlive(pid);
3261
+ const probe = probeDaemonInstance(configDir);
2853
3262
  const runtimeId = target ? target.runtimeId() : this.store ? loadDaemonConfig(this.store)?.runtimeId : void 0;
2854
3263
  const self = target ? { context: target.context, baseUrl: target.baseUrl } : this.selfEnvironment();
2855
3264
  this.json(res, 200, {
2856
3265
  ok: true,
2857
- daemonRunning: running,
2858
- pid: running ? pid : null,
3266
+ daemonRunning: probe.running,
3267
+ pid: probe.pid,
2859
3268
  runtimeId: runtimeId ?? null,
2860
3269
  // 支持按请求声明目标环境(前端据此决定是否走多环境路径;缺省 = 旧版 portal)
2861
3270
  envRouting: true,
@@ -2870,10 +3279,10 @@ var init_portal_server = __esm({
2870
3279
  if (!await this.authorize(req, res, target))
2871
3280
  return;
2872
3281
  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 });
3282
+ const where = target ? `\uFF08\u73AF\u5883 ${target.context}\uFF0C\u76EE\u5F55 ${configDir}\uFF09` : "";
3283
+ const existing = probeDaemonInstance(configDir);
3284
+ if (existing.running) {
3285
+ this.json(res, 200, { ok: true, pid: existing.pid, alreadyRunning: true });
2877
3286
  return;
2878
3287
  }
2879
3288
  const result = spawnDaemonProcess({
@@ -2884,18 +3293,54 @@ var init_portal_server = __esm({
2884
3293
  this.json(res, 500, { ok: false, error: result.error ?? `spawn \u5931\u8D25\uFF08pid=${result.pid}\uFF09` });
2885
3294
  return;
2886
3295
  }
3296
+ const confirmed = await waitForDaemonStart(configDir, result.pid);
3297
+ if (!confirmed.running) {
3298
+ if (!isProcessAlive(readPidFile(pidFileIn(configDir))))
3299
+ rmSync5(pidFileIn(configDir), { force: true });
3300
+ this.log(`[portal] daemon \u672A\u80FD\u542F\u52A8 pid=${result.pid}${where}\uFF1A${confirmed.reason}`);
3301
+ this.json(res, 500, {
3302
+ ok: false,
3303
+ error: confirmed.reason === "exited" ? "DAEMON_EXITED" : "DAEMON_START_TIMEOUT",
3304
+ 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"}`,
3305
+ logFile: daemonLogIn(configDir)
3306
+ });
3307
+ return;
3308
+ }
3309
+ if (confirmed.pid !== result.pid) {
3310
+ 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`);
3311
+ this.json(res, 200, { ok: true, pid: confirmed.pid, alreadyRunning: true });
3312
+ return;
3313
+ }
3314
+ const daemonId = target?.daemonId();
3315
+ if (target && daemonId) {
3316
+ const reg = await waitForDaemonRegistration(target.isDeviceOnline, daemonId, confirmed.pid);
3317
+ if (reg === "exited" || reg === "not-registered") {
3318
+ if (!isProcessAlive(readPidFile(pidFileIn(configDir))))
3319
+ rmSync5(pidFileIn(configDir), { force: true });
3320
+ this.log(`[portal] daemon \u8FDB\u7A0B\u5DF2\u8D77\u6765\u4F46\u672A\u6CE8\u518C\u4E0A\u7EBF pid=${confirmed.pid}${where}\uFF1A${reg}`);
3321
+ this.json(res, 500, {
3322
+ ok: false,
3323
+ error: "DAEMON_NOT_REGISTERED",
3324
+ 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"}`,
3325
+ logFile: daemonLogIn(configDir)
3326
+ });
3327
+ return;
3328
+ }
3329
+ if (reg === "unknown")
3330
+ 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`);
3331
+ }
2887
3332
  try {
2888
- writeFileSync4(pidFile, String(result.pid), { mode: 384 });
3333
+ writeFileSync4(pidFileIn(configDir), String(confirmed.pid), { mode: 384 });
2889
3334
  } catch {
2890
3335
  try {
2891
- process.kill(result.pid, "SIGTERM");
3336
+ process.kill(confirmed.pid, "SIGTERM");
2892
3337
  } catch {
2893
3338
  }
2894
3339
  this.json(res, 500, { ok: false, error: "pid \u6587\u4EF6\u5199\u5165\u5931\u8D25" });
2895
3340
  return;
2896
3341
  }
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 });
3342
+ this.log(`[portal] daemon \u5DF2\u62C9\u8D77 pid=${confirmed.pid}${where}`);
3343
+ this.json(res, 200, { ok: true, pid: confirmed.pid, alreadyRunning: false });
2899
3344
  }
2900
3345
  async handleStop(req, res, url) {
2901
3346
  const { target, failed } = this.resolveTarget(res, url);
@@ -2904,15 +3349,15 @@ var init_portal_server = __esm({
2904
3349
  if (!await this.authorize(req, res, target))
2905
3350
  return;
2906
3351
  const configDir = target?.configDir ?? stateDir();
2907
- const existing = readPidFrom(pidFileIn(configDir));
2908
- if (isPidAlive(existing)) {
3352
+ const existing = probeDaemonInstance(configDir);
3353
+ if (existing.running) {
2909
3354
  try {
2910
- process.kill(existing, "SIGTERM");
3355
+ process.kill(existing.pid, "SIGTERM");
2911
3356
  } catch {
2912
3357
  }
2913
- this.log(`[portal] daemon \u505C\u6B62\u4FE1\u53F7\u5DF2\u53D1\u9001 pid=${existing}${target ? `\uFF08\u73AF\u5883 ${target.context}\uFF09` : ""}`);
3358
+ this.log(`[portal] daemon \u505C\u6B62\u4FE1\u53F7\u5DF2\u53D1\u9001 pid=${existing.pid}${target ? `\uFF08\u73AF\u5883 ${target.context}\uFF09` : ""}`);
2914
3359
  }
2915
- this.json(res, 200, { ok: true, pid: existing || null });
3360
+ this.json(res, 200, { ok: true, pid: existing.pid });
2916
3361
  }
2917
3362
  /**
2918
3363
  * start/stop 鉴权:目标环境用**它自己的**服务端校验 token(这正是跨环境 401 的修复点);
@@ -3020,6 +3465,7 @@ __export(acp_connection_exports, {
3020
3465
  AcpConnection: () => AcpConnection,
3021
3466
  DEFAULT_ACP_INIT_TIMEOUT_MS: () => DEFAULT_ACP_INIT_TIMEOUT_MS,
3022
3467
  countDimAcpProcesses: () => countDimAcpProcesses,
3468
+ normalizeAcpConfigOptions: () => normalizeAcpConfigOptions,
3023
3469
  resolveAcpInitTimeoutMs: () => resolveAcpInitTimeoutMs
3024
3470
  });
3025
3471
  import { execFileSync as execFileSync4, spawn as spawn3 } from "node:child_process";
@@ -3048,6 +3494,16 @@ function countDimAcpProcesses() {
3048
3494
  return null;
3049
3495
  }
3050
3496
  }
3497
+ function normalizeAcpConfigOptions(raw) {
3498
+ if (Array.isArray(raw))
3499
+ return raw;
3500
+ if (raw && typeof raw === "object") {
3501
+ const nested = raw.configOptions;
3502
+ if (Array.isArray(nested))
3503
+ return nested;
3504
+ }
3505
+ return null;
3506
+ }
3051
3507
  var DEFAULT_ACP_INIT_TIMEOUT_MS, cachedDimVersion, requestSeq, AcpConnection;
3052
3508
  var init_acp_connection = __esm({
3053
3509
  "src/daemon/acp/acp-connection.ts"() {
@@ -3063,6 +3519,7 @@ var init_acp_connection = __esm({
3063
3519
  outcome: { outcome: "selected", optionId: "allow" }
3064
3520
  }));
3065
3521
  this.onExit = options.onExit;
3522
+ this.elicitationHandler = options.onElicitation;
3066
3523
  }
3067
3524
  options;
3068
3525
  child = null;
@@ -3070,6 +3527,7 @@ var init_acp_connection = __esm({
3070
3527
  pending = /* @__PURE__ */ new Map();
3071
3528
  exited = false;
3072
3529
  permissionHandler;
3530
+ elicitationHandler;
3073
3531
  onExit;
3074
3532
  /** sessionId → 当前 update 处理器(同一 session 单活跃 prompt) */
3075
3533
  updateHandlers = /* @__PURE__ */ new Map();
@@ -3103,9 +3561,9 @@ var init_acp_connection = __esm({
3103
3561
  process.stderr.write(`[daemon] dim spawn \u5931\u8D25 ${dimPath}\uFF08\u5C1D\u8BD5\u4E0B\u4E00\u4E2A\u5019\u9009\uFF09: ${message}
3104
3562
  `);
3105
3563
  try {
3106
- const { existsSync: existsSync19 } = await import("node:fs");
3564
+ const { existsSync: existsSync18 } = await import("node:fs");
3107
3565
  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 ?? "-"}
3566
+ `[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
3567
  `
3110
3568
  );
3111
3569
  } catch {
@@ -3155,7 +3613,12 @@ var init_acp_connection = __esm({
3155
3613
  child.stdout.on("data", (d) => this.onStdout(d.toString()));
3156
3614
  const init = {
3157
3615
  protocolVersion: 1,
3158
- clientCapabilities: { auth: { terminal: true } },
3616
+ // elicitation.form 只在**有处理器时**声明:声明了却不应答会让 dim 挂起等用户输入。
3617
+ // 形状是对象而非 boolean(V2 实测:boolean 被 dim 以 -32602 拒绝)。
3618
+ clientCapabilities: {
3619
+ auth: { terminal: true },
3620
+ ...this.elicitationHandler ? { elicitation: { form: {} } } : {}
3621
+ },
3159
3622
  clientInfo: { name: "morula-runtime", version: "1.0.0" }
3160
3623
  };
3161
3624
  const initTimeoutMs = resolveAcpInitTimeoutMs();
@@ -3200,26 +3663,38 @@ var init_acp_connection = __esm({
3200
3663
  return this.request("session/set_config_option", { sessionId, configId, value }).then(() => {
3201
3664
  });
3202
3665
  }
3666
+ /**
3667
+ * 设置会话配置项并**全量返回** configOptions(ACP 规范:set_config_option 响应即全量配置)。
3668
+ * 会话宿主用(§4.4 配置变更回写);任务链路只需 fire-and-forget,继续用 setConfigOption。
3669
+ *
3670
+ * **返回已归一化为数组**:ACP 的响应体是 `{ configOptions: ConfigOption[] }`(对象),
3671
+ * 而 `session/new` / `resume` 的 `configOptions` 字段本身就是数组——两处形状不同,
3672
+ * 调用方(会话宿主 → 服务端落库 → 前端输入区值域)必须拿到同一种形状,否则前端拿到对象会崩。
3673
+ */
3674
+ setConfigOptionFull(sessionId, configId, value) {
3675
+ return this.request("session/set_config_option", { sessionId, configId, value }).then((res) => normalizeAcpConfigOptions(res) ?? []);
3676
+ }
3203
3677
  /**
3204
3678
  * 发起 prompt:立即返回;事件流经 updateHandler 推送;返回的 Promise 在
3205
3679
  * turn 结束(prompt 响应)时 resolve。
3206
3680
  *
3207
3681
  * @param sessionId ACP 会话 ID
3208
- * @param text 用户输入文本
3682
+ * @param prompt 用户输入。字符串 = 纯文本;数组 = 多模态 content block
3683
+ * (`image` block 用于对话附件,见 steward-core-acp-migration §五 P2-3)
3209
3684
  * @param updateHandler 会话事件回调
3210
- * @param timeoutMs prompt 整轮 RPC 超时。注意:dim 直到整轮 agent 循环结束
3211
- * (stopReason)才回复 prompt,长时间的多步工具执行/模型生成都会占满该窗口,
3212
- * 因此调用方必须传入 ≥ 任务总超时(如 executor 的 timeoutMs)的值,否则长任务
3213
- * 会被 RPC 层提前误杀。默认 300s 仅为向后兼容的保守值。
3685
+ * @param timeoutMs prompt 整轮 RPC 超时。**未设 / <= 0 = 不设超时(默认)**:dim 直到整轮
3686
+ * agent 循环结束(stopReason)才回复 prompt,多步工具执行/长模型生成都会占满窗口,
3687
+ * 给 RPC 层设超时等于给「合法的长回合」埋误杀点。限时与否由调用方(executor 的
3688
+ * 单回合预算)决定,executor 默认即传 0(不设超时)。
3214
3689
  */
3215
- prompt(sessionId, text, updateHandler, timeoutMs = 3e5) {
3690
+ prompt(sessionId, prompt, updateHandler, timeoutMs = 0) {
3216
3691
  if (this.updateHandlers.has(sessionId)) {
3217
3692
  return Promise.reject(new Error(`session ${sessionId} \u5DF2\u6709\u6D3B\u8DC3 prompt\uFF08ACP \u5355\u6D3B\u8DC3\u7EA6\u675F\uFF09`));
3218
3693
  }
3219
3694
  this.updateHandlers.set(sessionId, updateHandler);
3220
3695
  const params = {
3221
3696
  sessionId,
3222
- prompt: [{ type: "text", text }],
3697
+ prompt: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt,
3223
3698
  messageId: `m-${randomUUID2().slice(0, 8)}`
3224
3699
  };
3225
3700
  return this.request("session/prompt", params, timeoutMs).then(
@@ -3278,17 +3753,20 @@ var init_acp_connection = __esm({
3278
3753
  return Promise.reject(new Error(`dim acp \u8FDB\u7A0B\u4E0D\u53EF\u7528\uFF08${method}\uFF09`));
3279
3754
  const id = ++requestSeq;
3280
3755
  return new Promise((resolve2, reject) => {
3281
- const timer = setTimeout(() => {
3756
+ const onTimeout = () => {
3282
3757
  this.pending.delete(id);
3283
3758
  reject(new Error(`dim acp ${method} \u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)}s\uFF09`));
3284
- }, timeoutMs);
3759
+ };
3760
+ const timer = timeoutMs > 0 ? setTimeout(onTimeout, timeoutMs) : null;
3285
3761
  this.pending.set(id, {
3286
3762
  resolve: (v) => {
3287
- clearTimeout(timer);
3763
+ if (timer)
3764
+ clearTimeout(timer);
3288
3765
  resolve2(v);
3289
3766
  },
3290
3767
  reject: (e) => {
3291
- clearTimeout(timer);
3768
+ if (timer)
3769
+ clearTimeout(timer);
3292
3770
  reject(e);
3293
3771
  }
3294
3772
  });
@@ -3359,6 +3837,21 @@ var init_acp_connection = __esm({
3359
3837
  );
3360
3838
  return;
3361
3839
  }
3840
+ if (method === "elicitation/create") {
3841
+ const id = msg.id;
3842
+ const params = msg.params;
3843
+ const handler = this.elicitationHandler;
3844
+ if (!handler) {
3845
+ this.respond(id, { action: "decline" });
3846
+ return;
3847
+ }
3848
+ void handler(params).then(
3849
+ (response) => this.respond(id, response),
3850
+ // 处理器异常(含超时):回 cancel,让 agent 走「用户没答」的分支,而不是挂死
3851
+ () => this.respond(id, { action: "cancel" })
3852
+ );
3853
+ return;
3854
+ }
3362
3855
  if (method === "$/cancel_request") {
3363
3856
  return;
3364
3857
  }
@@ -3378,7 +3871,7 @@ __export(dim_home_exports, {
3378
3871
  resolveIsolatedDimEnv: () => resolveIsolatedDimEnv
3379
3872
  });
3380
3873
  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";
3874
+ import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync6, readFileSync as readFileSync4, rmSync as rmSync6 } from "node:fs";
3382
3875
  import { homedir as homedir8 } from "node:os";
3383
3876
  import { join as join10 } from "node:path";
3384
3877
  function isolatedDimHome() {
@@ -3405,11 +3898,11 @@ __export(dim_model_exports, {
3405
3898
  import { createRequire } from "node:module";
3406
3899
  import { spawnSync } from "node:child_process";
3407
3900
  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";
3901
+ import { homedir as homedir11, tmpdir } from "node:os";
3902
+ import { join as join14 } from "node:path";
3410
3903
  function dimSqlitePath(env) {
3411
3904
  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")];
3905
+ const candidates = home && home !== "" ? [join14(home, "v2", "dimcode.sqlite"), join14(home, "dimcode.sqlite")] : [join14(homedir11(), ".dimcode", "v2", "dimcode.sqlite")];
3413
3906
  return candidates.find((p) => existsSync10(p)) ?? null;
3414
3907
  }
3415
3908
  function listDimModels(dimPath = "dim", env) {
@@ -3444,12 +3937,12 @@ function listDimModels(dimPath = "dim", env) {
3444
3937
  function hostDimEnv(env) {
3445
3938
  const home = env?.DIMCODE_HOME?.trim();
3446
3939
  if (home && home !== "" && home.includes(".morula") && home.includes("dimcode")) {
3447
- return { ...process.env, DIMCODE_HOME: join13(homedir10(), ".dimcode", "v2") };
3940
+ return { ...process.env, DIMCODE_HOME: join14(homedir11(), ".dimcode", "v2") };
3448
3941
  }
3449
3942
  return env ? { ...env } : null;
3450
3943
  }
3451
3944
  function listDimModelsFromCli(dimPath, env) {
3452
- const tmpFile = join13(tmpdir(), `${DIM_MODELS_TMP_PREFIX}${process.pid}-${Date.now()}.json`);
3945
+ const tmpFile = join14(tmpdir(), `${DIM_MODELS_TMP_PREFIX}${process.pid}-${Date.now()}.json`);
3453
3946
  const fd = openSync3(tmpFile, "w");
3454
3947
  let collected = "";
3455
3948
  try {
@@ -3486,8 +3979,8 @@ function listDimModelsFromCli(dimPath, env) {
3486
3979
  }
3487
3980
  function dimOAuthTokenPath(env) {
3488
3981
  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")]) {
3982
+ const dir = home && home !== "" ? home : join14(homedir11(), ".dimcode", "v2");
3983
+ for (const p of [join14(dir, "auth.json"), join14(dir, "v2", "auth.json")]) {
3491
3984
  if (existsSync10(p))
3492
3985
  return p;
3493
3986
  }
@@ -3632,14 +4125,14 @@ init_dist2();
3632
4125
  init_dist2();
3633
4126
 
3634
4127
  // src/build-env.ts
3635
- var BUILD_VERSION2 = "0.1.0-alpha.7";
4128
+ var BUILD_VERSION2 = "0.1.0-alpha.8";
3636
4129
 
3637
4130
  // src/commands/daemon.ts
3638
4131
  init_dist();
3639
4132
  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";
4133
+ import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync11, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "node:fs";
4134
+ import { homedir as homedir13 } from "node:os";
4135
+ import { dirname as dirname10, join as join22 } from "node:path";
3643
4136
 
3644
4137
  // src/daemon/daemon-autostart.ts
3645
4138
  init_portal_autostart();
@@ -3708,9 +4201,9 @@ init_portal_server();
3708
4201
  // src/daemon/runner.ts
3709
4202
  init_dist2();
3710
4203
  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";
4204
+ import { existsSync as existsSync15, mkdirSync as mkdirSync13, statSync as statSync4, writeFileSync as writeFileSync11 } from "node:fs";
3712
4205
  import { arch, hostname } from "node:os";
3713
- import { dirname as dirname7, join as join18 } from "node:path";
4206
+ import { dirname as dirname8, join as join20 } from "node:path";
3714
4207
 
3715
4208
  // src/daemon/acp/acp-connection-pool.ts
3716
4209
  var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
@@ -3954,7 +4447,7 @@ var DaemonApi = class {
3954
4447
  recordEvent(taskId, body) {
3955
4448
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/events`, { body });
3956
4449
  }
3957
- /** ACP 流式事件(Phase 1):daemon → Server → WS 前端 */
4450
+ /** ACP 流式事件(Phase 1 → P2-2):daemon → Server → WS 前端(`delta` 为 transient,不落库) */
3958
4451
  recordStreamChunk(taskId, body) {
3959
4452
  return this.taskRequest(taskId, `/agent-tasks/${taskId}/stream-chunk`, { body });
3960
4453
  }
@@ -4015,19 +4508,200 @@ var DaemonApi = class {
4015
4508
  getGitHubToken(taskId, repo) {
4016
4509
  return this.client.get("/vcs/daemon-token", { taskId, repo });
4017
4510
  }
4511
+ // ── 会话宿主机器协议(steward-core-acp-migration §4.3/§4.4,P0 骨架) ──
4512
+ /** 会话级 token 缓存(每会话独立凭证,注入工作区 env MORULA_SESSION_TOKEN) */
4513
+ sessionTokens = /* @__PURE__ */ new Map();
4514
+ setSessionToken(sessionId, token) {
4515
+ this.sessionTokens.set(sessionId, token);
4516
+ }
4517
+ clearSessionToken(sessionId) {
4518
+ this.sessionTokens.delete(sessionId);
4519
+ }
4520
+ /** 会话级机器协议请求(session token 通道;无 token 时回退设备级 client 凭证) */
4521
+ sessionRequest(sessionId, path, options = {}) {
4522
+ const token = this.sessionTokens.get(sessionId);
4523
+ return this.client.request(path, {
4524
+ method: "POST",
4525
+ ...options,
4526
+ ...token ? { token } : {}
4527
+ });
4528
+ }
4529
+ /** 会话级机器协议请求(可指定 method;elicitation 查询用 GET + 长轮询) */
4530
+ sessionRequestWithMethod(sessionId, method, path, options = {}) {
4531
+ const token = this.sessionTokens.get(sessionId);
4532
+ return this.client.request(path, {
4533
+ method,
4534
+ ...options,
4535
+ ...token ? { token } : {}
4536
+ });
4537
+ }
4538
+ /** 会话宿主事件上报(→ agent_events owner=session + WS exec:session 房间) */
4539
+ recordSessionEvent(sessionId, body) {
4540
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/events`, { body });
4541
+ }
4018
4542
  /**
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)——调用方需容忍失败。
4543
+ * 会话流式增量上报(P2-2:transient,**不落库**;服务端转推 `agent-task:delta` 到 exec:session 房间)。
4544
+ * 与 `recordSessionEvent` 的语义差别:这里是「可丢的过程信号」,失败不重试、不占 seq。
4545
+ * `kind`:`text` = assistant 正文,`thinking` = 推理原文(durable 对应 `agent_thought_delta`)。
4546
+ */
4547
+ recordSessionStreamChunk(sessionId, body) {
4548
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/stream-chunk`, { body });
4549
+ }
4550
+ /** 会话绑定与指针回传(dimSessionId / workspaceDir / runtimeId;只写绑定列不改状态) */
4551
+ reportSessionBinding(sessionId, body) {
4552
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/session`, { body });
4553
+ }
4554
+ /**
4555
+ * 会话关闭上报。
4556
+ * `reason='idle'`(空闲回收)→ 服务端只回到 `idle`(保留 dimSessionId,下次可 resume);
4557
+ * `reason='command'`(用户/平台显式关闭)→ 终态 `closed`(§4.4/§4.1 的衔接)。
4558
+ */
4559
+ closeAgentSession(sessionId, reason = "command") {
4560
+ return this.sessionRequest(sessionId, `/agent-sessions/${sessionId}/close`, { body: { reason } });
4561
+ }
4562
+ /**
4563
+ * 结构化提问登记(§4.6 ②):服务端落库 + 推前端确认卡,立即返回 elicitationId。
4564
+ * 用户应答由 `getElicitation` 长轮询取回(见 session-host.handleElicitation)。
4565
+ */
4566
+ createElicitation(sessionId, body) {
4567
+ return this.sessionRequest(
4568
+ sessionId,
4569
+ `/agent-sessions/${sessionId}/elicitation`,
4570
+ { body }
4571
+ );
4572
+ }
4573
+ /** 结构化提问结果查询(waitMs 内被回答则提前返回;服务端最多持 waitMs) */
4574
+ getElicitation(sessionId, elicitationId, waitMs) {
4575
+ return this.sessionRequestWithMethod(sessionId, "GET", `/agent-sessions/${sessionId}/elicitations/${elicitationId}`, {
4576
+ query: { waitMs },
4577
+ // 服务端最多持 waitMs,客户端留 10s 余量(避免「服务端刚返回、客户端已超时」的假失败)
4578
+ timeoutMs: waitMs + 1e4
4579
+ });
4580
+ }
4581
+ /**
4582
+ * A2(issues/multi-repo-task-missing-workcontext-link.md):**不传 repo** 的平台凭证
4583
+ * ——服务端按「任务空间内 active 的连接」择一签发(凭证是平台凭据,不是某仓库的凭据)。
4584
+ *
4585
+ * 用于无主仓库 / 多仓库任务(daemon 走 scratch 隔离目录、`resolveRepo(detail) = null`):
4586
+ * 这些任务的 agent 同样需要 MORULA_GITLAB_TOKEN / MORULA_GITLAB_URL 才能自己走 GitLab API
4587
+ * 兜底建 MR(否则链路两端都没有自愈路径)。旧版服务端强制校验 repo(400)——调用方需容忍失败。
4025
4588
  */
4026
4589
  getSpaceDaemonToken(taskId) {
4027
4590
  return this.client.get("/vcs/daemon-token", { taskId });
4028
4591
  }
4029
4592
  };
4030
4593
 
4594
+ // src/daemon/assistant-text-stream.ts
4595
+ var TEXT_DELTA_WINDOW_MS = 200;
4596
+ var TEXT_SEGMENT_MAX_CHARS = 2e3;
4597
+ var PARAGRAPH_BREAK = "\n\n";
4598
+ var AssistantTextStream = class {
4599
+ constructor(sink, options = {}) {
4600
+ this.sink = sink;
4601
+ this.windowMs = options.windowMs ?? TEXT_DELTA_WINDOW_MS;
4602
+ this.segmentMaxChars = Math.max(1, options.segmentMaxChars ?? TEXT_SEGMENT_MAX_CHARS);
4603
+ this.log = options.log ?? (() => {
4604
+ });
4605
+ }
4606
+ sink;
4607
+ windowMs;
4608
+ segmentMaxChars;
4609
+ log;
4610
+ /** 当前段的完整文本(未落段部分) */
4611
+ buffer = "";
4612
+ /** 当前段已通过 onDelta 发出的字符数 */
4613
+ sent = 0;
4614
+ /** 当前段序号(落段后 +1) */
4615
+ seg = 0;
4616
+ timer = null;
4617
+ disposed = false;
4618
+ /** 段落落库串行链:段落事件必须按 seg 顺序落库(上行 HTTP 并发会乱序 → 回放正文错序) */
4619
+ paragraphChain = Promise.resolve();
4620
+ /** 当前段落序号(诊断/测试用) */
4621
+ get currentSeg() {
4622
+ return this.seg;
4623
+ }
4624
+ /** 喂入一个文本增量 */
4625
+ push(text) {
4626
+ if (this.disposed || !text)
4627
+ return;
4628
+ this.buffer += text;
4629
+ let idx = this.buffer.lastIndexOf(PARAGRAPH_BREAK);
4630
+ while (idx >= 0) {
4631
+ this.cutParagraph(idx + PARAGRAPH_BREAK.length);
4632
+ idx = this.buffer.lastIndexOf(PARAGRAPH_BREAK);
4633
+ }
4634
+ while (this.buffer.length >= this.segmentMaxChars)
4635
+ this.cutParagraph(this.segmentMaxChars);
4636
+ if (this.buffer.length > 0)
4637
+ this.scheduleDelta();
4638
+ }
4639
+ /** 把当前 buffer 的前 `length` 个字符作为一段落切出去(同步改状态,落库走串行链) */
4640
+ cutParagraph(length) {
4641
+ const head = this.buffer.slice(0, length);
4642
+ const rest = this.buffer.slice(length);
4643
+ this.buffer = head;
4644
+ this.sent = Math.min(this.sent, head.length);
4645
+ void this.flushParagraph();
4646
+ this.buffer = rest;
4647
+ this.sent = 0;
4648
+ }
4649
+ /** 强制结束当前段落(工具调用前 / 回合结束前调用并 await) */
4650
+ async flushParagraph() {
4651
+ if (this.timer) {
4652
+ clearTimeout(this.timer);
4653
+ this.timer = null;
4654
+ }
4655
+ this.flushDelta();
4656
+ if (!this.buffer)
4657
+ return this.paragraphChain;
4658
+ const text = this.buffer;
4659
+ const seg = this.seg;
4660
+ this.buffer = "";
4661
+ this.sent = 0;
4662
+ this.seg += 1;
4663
+ this.paragraphChain = this.paragraphChain.then(async () => {
4664
+ try {
4665
+ await this.sink.onParagraph(seg, text);
4666
+ } catch (err) {
4667
+ this.log(`[text-stream] \u6BB5\u843D ${seg} \u843D\u5E93\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4668
+ }
4669
+ });
4670
+ return this.paragraphChain;
4671
+ }
4672
+ /** 释放定时器(会话关闭 / 任务收尾) */
4673
+ dispose() {
4674
+ this.disposed = true;
4675
+ if (this.timer) {
4676
+ clearTimeout(this.timer);
4677
+ this.timer = null;
4678
+ }
4679
+ this.buffer = "";
4680
+ this.sent = 0;
4681
+ }
4682
+ scheduleDelta() {
4683
+ if (this.timer || this.disposed)
4684
+ return;
4685
+ this.timer = setTimeout(() => {
4686
+ this.timer = null;
4687
+ this.flushDelta();
4688
+ }, this.windowMs);
4689
+ this.timer.unref?.();
4690
+ }
4691
+ /** 把「未发出的新增文本」作为一次 transient 增量直通 */
4692
+ flushDelta() {
4693
+ if (this.buffer.length <= this.sent)
4694
+ return;
4695
+ const text = this.buffer.slice(this.sent);
4696
+ this.sent = this.buffer.length;
4697
+ try {
4698
+ this.sink.onDelta(this.seg, text);
4699
+ } catch (err) {
4700
+ this.log(`[text-stream] \u589E\u91CF\u76F4\u901A\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
4701
+ }
4702
+ }
4703
+ };
4704
+
4031
4705
  // src/daemon/contract-skill.ts
4032
4706
  var COLLABORATION_SKILL_NAME = "morula-working-on-issues";
4033
4707
  var COLLABORATION_SKILL_FRONTMATTER = `---
@@ -4099,11 +4773,13 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4099
4773
 
4100
4774
  ## 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
4775
 
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
4776
+ \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
4777
+ **\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
4778
+ \`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
4779
 
4105
4780
  - **\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
4781
  - **\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
4782
+ - **\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
4783
  - **\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
4784
 
4109
4785
  > **\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
@@ -4137,7 +4813,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4137
4813
 
4138
4814
  ## 0.3 \u5206\u652F\u547D\u540D\uFF1A\u7531\u4F60\uFF08Agent\uFF09\u53D6\u540D\u5E76\u4E0A\u62A5
4139
4815
 
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
4816
+ - **\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
4817
  - **\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
4818
  - **\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
4819
  - **\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 +4866,13 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4190
4866
  \u251C\u2500\u2500 .agents/skills/ \u2190 \u534F\u4F5C\u5951\u7EA6 + \u672C\u4EFB\u52A1\u81EA\u5B9A\u4E49 skill
4191
4867
  \u251C\u2500\u2500 .agent_context/task-context.md \u2190 \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08\u89C1 0 \u8282\uFF09
4192
4868
  \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
4869
+ \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
4870
  \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
4871
  \`\`\`
4196
4872
 
4197
4873
  - **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
4874
  \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
4875
+ - **\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
4876
  - **\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
4877
  - \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
4878
  - \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 +4880,7 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4204
4880
  - **\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
4881
  \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
4882
  - **\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
4883
+ - **\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
4884
  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
4885
  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
4886
  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 +4902,8 @@ var COLLABORATION_SKILL_BODY = `# Working on Morula issues\uFF08\u534F\u4F5C\u59
4226
4902
  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
4903
  # \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
4904
  \`\`\`
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
4905
+ - **\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
4906
+ \`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
4907
  - **\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
4908
  - **\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
4909
  - **\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 +5090,7 @@ morula task inbox --peek # \u53EA\u770B\u4E0D\u53D6\uFF08\u786E\u8BA4\u6709\
4414
5090
 
4415
5091
  - **\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
5092
  - **\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
5093
+ - **\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
5094
  - **\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
5095
  - **\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
5096
  - \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 +5145,7 @@ var COLLABORATION_SKILL_SOURCE_MAP = `# morula-working-on-issues \u2014 \u5951\u
4469
5145
  | 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
5146
  | 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
5147
  | 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\` |
5148
+ | \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
5149
  | \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
5150
  | \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
5151
 
@@ -4545,7 +5221,7 @@ function isFlagEnabled(name, defaultValue = true) {
4545
5221
 
4546
5222
  // src/daemon/git-env.ts
4547
5223
  import { execFileSync as execFileSync6 } from "node:child_process";
4548
- import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "node:fs";
5224
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "node:fs";
4549
5225
  import { join as join11 } from "node:path";
4550
5226
  var GIT_CANDIDATE_DIRS = ["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin", "/opt/local/bin"];
4551
5227
  function prependToPath(dirs) {
@@ -4562,14 +5238,14 @@ function ensureGitOnPath() {
4562
5238
  return;
4563
5239
  } catch {
4564
5240
  }
4565
- const missing = prependToPath(GIT_CANDIDATE_DIRS.filter((d) => existsSync8(join11(d, "git"))));
5241
+ const missing = prependToPath(GIT_CANDIDATE_DIRS.filter((d) => existsSync7(join11(d, "git"))));
4566
5242
  if (missing.length) {
4567
5243
  console.warn(`[daemon] PATH \u7F3A\u5C11 git\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
4568
5244
  }
4569
5245
  }
4570
5246
  function ensureNodeOnPath() {
4571
5247
  const nodeDir = process.execPath ? join11(process.execPath, "..") : "";
4572
- const candidates = [nodeDir, "/usr/local/bin", "/opt/homebrew/bin"].filter((d) => d && existsSync8(join11(d, "node")));
5248
+ const candidates = [nodeDir, "/usr/local/bin", "/opt/homebrew/bin"].filter((d) => d && existsSync7(join11(d, "node")));
4573
5249
  const missing = prependToPath(candidates);
4574
5250
  if (missing.length) {
4575
5251
  console.warn(`[daemon] PATH \u7F3A\u5C11 node\uFF0C\u5DF2\u8865\u5145: ${missing.join(", ")}`);
@@ -4615,7 +5291,7 @@ function buildDaemonRepoUrl(repoFullName, baseUrl) {
4615
5291
  function ensureRepo(workdir, tenantId, repo, env, remoteUrl, daemonRemoteUrl) {
4616
5292
  const repoDir = join11(workdir, tenantId, repo);
4617
5293
  const fetchUrl = daemonRemoteUrl ?? remoteUrl ?? buildDaemonRepoUrl(repo);
4618
- if (existsSync8(join11(repoDir, ".git"))) {
5294
+ if (existsSync7(join11(repoDir, ".git"))) {
4619
5295
  if (remoteUrl)
4620
5296
  syncRemoteUrl(repoDir, remoteUrl, env);
4621
5297
  try {
@@ -4660,7 +5336,7 @@ function syncRemoteUrl(repoDir, remoteUrl, env) {
4660
5336
  function isLinkedWorktree(repoDir) {
4661
5337
  try {
4662
5338
  const gitDir = execFileSync6("git", ["-C", repoDir, "rev-parse", "--absolute-git-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
4663
- return !!gitDir && existsSync8(join11(gitDir, "commondir"));
5339
+ return !!gitDir && existsSync7(join11(gitDir, "commondir"));
4664
5340
  } catch {
4665
5341
  return false;
4666
5342
  }
@@ -4698,7 +5374,7 @@ function ensureGitIdentity(repoDir, personaName) {
4698
5374
  }
4699
5375
 
4700
5376
  // 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";
5377
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync7, writeFileSync as writeFileSync5 } from "node:fs";
4702
5378
  import { homedir as homedir9 } from "node:os";
4703
5379
  import { join as join12 } from "node:path";
4704
5380
  var OUTBOX_RETRY_SCHEDULE = [1e3, 4e3, 16e3, 6e4, 3e5];
@@ -4714,14 +5390,14 @@ function writeOutbox(entry, configDir) {
4714
5390
  }
4715
5391
  function readOutboxEntries(configDir) {
4716
5392
  const dir = outboxDir(configDir);
4717
- if (!existsSync9(dir))
5393
+ if (!existsSync8(dir))
4718
5394
  return [];
4719
5395
  const entries = [];
4720
5396
  for (const file of readdirSync2(dir)) {
4721
5397
  if (!file.endsWith(".json"))
4722
5398
  continue;
4723
5399
  try {
4724
- const raw = readFileSync6(join12(dir, file), "utf8");
5400
+ const raw = readFileSync5(join12(dir, file), "utf8");
4725
5401
  const parsed = JSON.parse(raw);
4726
5402
  if (parsed && typeof parsed.taskId === "string")
4727
5403
  entries.push(parsed);
@@ -4731,7 +5407,7 @@ function readOutboxEntries(configDir) {
4731
5407
  return entries;
4732
5408
  }
4733
5409
  function removeOutbox(taskId, configDir) {
4734
- rmSync6(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
5410
+ rmSync7(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
4735
5411
  }
4736
5412
  function staleDir(configDir) {
4737
5413
  return join12(outboxDir(configDir), "stale");
@@ -4754,13 +5430,113 @@ function outboxAge(entry) {
4754
5430
  return Number.isFinite(created) ? Date.now() - created : Number.POSITIVE_INFINITY;
4755
5431
  }
4756
5432
 
5433
+ // src/daemon/plugin-register.ts
5434
+ 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";
5435
+ import { homedir as homedir10 } from "node:os";
5436
+ import { dirname as dirname6, join as join13 } from "node:path";
5437
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5438
+ var PLUGIN_NAME = "morula-cli";
5439
+ var MANIFEST_DIR_NAME = ".codex-plugin";
5440
+ function legacyAgentsPluginDir(home = homedir10()) {
5441
+ return join13(home, ".agents", "plugins", PLUGIN_NAME);
5442
+ }
5443
+ function resolveDimcodeHome(env = process.env, home = homedir10()) {
5444
+ const fromEnv = env.DIMCODE_HOME?.trim();
5445
+ return fromEnv || join13(home, ".dimcode", "v2");
5446
+ }
5447
+ function defaultPluginsRoot(env = process.env, home = homedir10()) {
5448
+ return join13(resolveDimcodeHome(env, home), "plugins");
5449
+ }
5450
+ function resolvePackagePaths(entryUrl) {
5451
+ const distDir = dirname6(fileURLToPath2(entryUrl));
5452
+ return {
5453
+ packageRoot: dirname6(distDir),
5454
+ distDir,
5455
+ assetsDir: join13(distDir, "plugin")
5456
+ };
5457
+ }
5458
+ function buildLauncherScript(opts) {
5459
+ const entry = join13(opts.packageRoot, "dist", "morula.js");
5460
+ return `#!/bin/sh
5461
+ # 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
5462
+ # \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
5463
+ NODE=${JSON.stringify(opts.nodePath)}
5464
+ [ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
5465
+ if [ -z "$NODE" ]; then
5466
+ echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
5467
+ exit 127
5468
+ fi
5469
+ exec "$NODE" ${JSON.stringify(entry)} "$@"
5470
+ `;
5471
+ }
5472
+ function assetsReady(paths) {
5473
+ return existsSync9(join13(paths.assetsDir, "plugin.json")) && existsSync9(join13(paths.assetsDir, "skills"));
5474
+ }
5475
+ function listSkillNames(assetsDir) {
5476
+ const dir = join13(assetsDir, "skills");
5477
+ if (!existsSync9(dir))
5478
+ return [];
5479
+ return readdirSync3(dir).filter((name) => {
5480
+ try {
5481
+ return statSync2(join13(dir, name)).isDirectory();
5482
+ } catch {
5483
+ return false;
5484
+ }
5485
+ }).sort();
5486
+ }
5487
+ function pluginBinDir(env = process.env, home = homedir10()) {
5488
+ return join13(defaultPluginsRoot(env, home), PLUGIN_NAME, "bin");
5489
+ }
5490
+ function pluginBinPath(env = process.env, home = homedir10()) {
5491
+ return join13(pluginBinDir(env, home), "morula");
5492
+ }
5493
+ function prependPluginBinToPath(currentPath, env = process.env, home = homedir10()) {
5494
+ const binDir = pluginBinDir(env, home);
5495
+ const segments = (currentPath ?? "").split(":").filter(Boolean);
5496
+ if (segments[0] === binDir)
5497
+ return currentPath ?? binDir;
5498
+ return [binDir, ...segments.filter((s) => s !== binDir)].join(":");
5499
+ }
5500
+ function registerPlugin(opts) {
5501
+ const { pluginsRoot, paths, version } = opts;
5502
+ const nodePath = opts.nodePath ?? process.execPath;
5503
+ const pluginDir = join13(pluginsRoot, PLUGIN_NAME);
5504
+ if (!assetsReady(paths))
5505
+ throw new Error(`plugin assets not found at ${paths.assetsDir}; run \`pnpm run build:dist\` (dev) or reinstall the package`);
5506
+ const skillsSrc = join13(paths.assetsDir, "skills");
5507
+ const skills = listSkillNames(paths.assetsDir);
5508
+ const manifestDir = join13(pluginDir, MANIFEST_DIR_NAME);
5509
+ mkdirSync8(manifestDir, { recursive: true });
5510
+ const manifest = JSON.parse(readFileSync6(join13(paths.assetsDir, "plugin.json"), "utf8"));
5511
+ writeFileSync6(join13(manifestDir, "plugin.json"), `${JSON.stringify({ ...manifest, version }, null, 2)}
5512
+ `);
5513
+ const skillsDest = join13(pluginDir, "skills");
5514
+ rmSync8(skillsDest, { recursive: true, force: true });
5515
+ mkdirSync8(skillsDest, { recursive: true });
5516
+ cpSync(skillsSrc, skillsDest, { recursive: true });
5517
+ const binDir = join13(pluginDir, "bin");
5518
+ mkdirSync8(binDir, { recursive: true });
5519
+ const binPath = join13(binDir, "morula");
5520
+ writeFileSync6(binPath, buildLauncherScript({ nodePath, packageRoot: paths.packageRoot }));
5521
+ chmodSync3(binPath, 493);
5522
+ const legacy = legacyAgentsPluginDir(opts.home);
5523
+ return {
5524
+ pluginDir,
5525
+ binPath,
5526
+ skills,
5527
+ version,
5528
+ node: nodePath,
5529
+ legacyDir: existsSync9(join13(legacy, MANIFEST_DIR_NAME)) ? legacy : null
5530
+ };
5531
+ }
5532
+
4757
5533
  // src/daemon/acp/acp-event-mapper.ts
4758
5534
  var AcpEventMapper = class {
4759
5535
  lastUsage = void 0;
4760
5536
  reset() {
4761
5537
  this.lastUsage = void 0;
4762
5538
  }
4763
- /** 取当前 usage(run:ended 附着用) */
5539
+ /** 取当前 usage(回放/观测用;**不再**作为 tokenUsage 附着——token 总量以 prompt 响应的 usage 为准) */
4764
5540
  get latestUsage() {
4765
5541
  return this.lastUsage;
4766
5542
  }
@@ -4786,6 +5562,13 @@ var AcpEventMapper = class {
4786
5562
  }
4787
5563
  break;
4788
5564
  }
5565
+ case "user_message_chunk": {
5566
+ const text = update.content?.type === "text" ? update.content.text : "";
5567
+ if (text) {
5568
+ messages.push({ eventType: "user_message:delta", payload: { delta: text } });
5569
+ }
5570
+ break;
5571
+ }
4789
5572
  case "tool_call": {
4790
5573
  messages.push({
4791
5574
  eventType: "tool:started",
@@ -4813,11 +5596,13 @@ var AcpEventMapper = class {
4813
5596
  break;
4814
5597
  }
4815
5598
  case "usage_update": {
4816
- this.lastUsage = {
4817
- inputTokens: void 0,
4818
- outputTokens: void 0,
4819
- totalTokens: typeof update.used === "number" ? update.used : void 0
5599
+ const usage2 = {
5600
+ contextUsed: typeof update.used === "number" ? update.used : 0,
5601
+ contextSize: typeof update.size === "number" ? update.size : 0,
5602
+ cost: update.cost ?? null
4820
5603
  };
5604
+ this.lastUsage = usage2;
5605
+ messages.push({ eventType: "usage:context", payload: { usage: usage2 } });
4821
5606
  break;
4822
5607
  }
4823
5608
  default:
@@ -4913,14 +5698,9 @@ function normalizeOptionId(decision) {
4913
5698
  }
4914
5699
 
4915
5700
  // 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
5701
  var DEFAULT_STEER_SAFETY_MS = 30 * 1e3;
4922
5702
  var PROMPT_RPC_TIMEOUT_GRACE_MS = 1e4;
4923
- var WATCHDOG_TICK_MS = 15e3;
5703
+ var STEER_POLL_TICK_MS = 15e3;
4924
5704
  var KILL_GRACE_MS = 3e3;
4925
5705
  function resolveSteerSafetyMs(raw = process.env.MORULA_STEER_SAFETY_MS) {
4926
5706
  if (raw === void 0 || raw === "")
@@ -5063,7 +5843,7 @@ var DimAcpExecutor = class {
5063
5843
  /** 每次 run 前由调用方注入(daemon 单例池) */
5064
5844
  pool = null;
5065
5845
  async run(opts) {
5066
- const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5846
+ const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
5067
5847
  const mapper = new AcpEventMapper();
5068
5848
  const pool = opts.pool ?? this.pool;
5069
5849
  return await new Promise((resolve2) => {
@@ -5076,9 +5856,7 @@ var DimAcpExecutor = class {
5076
5856
  let turnNo = 1;
5077
5857
  const inFlightToolCalls = /* @__PURE__ */ new Map();
5078
5858
  const inFlightTools = () => inFlightToolCalls.size;
5079
- let lastActivityAt = Date.now();
5080
- let watchdogTimer = null;
5081
- let watchdogFired = false;
5859
+ let steerPollTimer = null;
5082
5860
  let resumeFailed;
5083
5861
  const killTree = (c) => {
5084
5862
  if (!c)
@@ -5089,10 +5867,10 @@ var DimAcpExecutor = class {
5089
5867
  c.kill();
5090
5868
  }
5091
5869
  };
5092
- const stopWatchdog = () => {
5093
- if (watchdogTimer) {
5094
- clearInterval(watchdogTimer);
5095
- watchdogTimer = null;
5870
+ const stopSteerPoll = () => {
5871
+ if (steerPollTimer) {
5872
+ clearInterval(steerPollTimer);
5873
+ steerPollTimer = null;
5096
5874
  }
5097
5875
  };
5098
5876
  let abortHandler = () => {
@@ -5103,7 +5881,7 @@ var DimAcpExecutor = class {
5103
5881
  settled = true;
5104
5882
  if (timer)
5105
5883
  clearTimeout(timer);
5106
- stopWatchdog();
5884
+ stopSteerPoll();
5107
5885
  if (ownsConnection && conn) {
5108
5886
  void conn.close().catch(() => {
5109
5887
  });
@@ -5128,45 +5906,17 @@ var DimAcpExecutor = class {
5128
5906
  finish({ ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88", sessionId: sessionId || void 0 });
5129
5907
  }
5130
5908
  };
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(() => {
5909
+ const startSteerPoll = () => {
5910
+ steerPollTimer = setInterval(() => {
5138
5911
  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();
5912
+ }, STEER_POLL_TICK_MS);
5913
+ steerPollTimer.unref?.();
5159
5914
  };
5160
5915
  let activeSteer = null;
5161
5916
  let steerWaitingSince = 0;
5162
- let wallClockFired = false;
5163
5917
  let interruptedTurns = 0;
5164
5918
  let turnKind = "initial";
5165
5919
  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
5920
  function maybeInterruptAtSafePoint() {
5171
5921
  if (!opts.steer || activeSteer || !conn || !sessionId)
5172
5922
  return;
@@ -5190,15 +5940,6 @@ var DimAcpExecutor = class {
5190
5940
  `);
5191
5941
  conn.cancel(sessionId);
5192
5942
  }
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
5943
  function emitAbortedTools() {
5203
5944
  const aborted = [];
5204
5945
  for (const [callId, toolName] of inFlightToolCalls) {
@@ -5237,8 +5978,12 @@ var DimAcpExecutor = class {
5237
5978
  });
5238
5979
  setup().then(async () => {
5239
5980
  const armTurnTimer = () => {
5240
- if (timer)
5981
+ if (timer) {
5241
5982
  clearTimeout(timer);
5983
+ timer = void 0;
5984
+ }
5985
+ if (timeout <= 0)
5986
+ return;
5242
5987
  timer = setTimeout(() => {
5243
5988
  if (sessionId && conn)
5244
5989
  conn.cancel(sessionId);
@@ -5254,7 +5999,7 @@ var DimAcpExecutor = class {
5254
5999
  };
5255
6000
  armTurnTimer();
5256
6001
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
5257
- startWatchdog();
6002
+ startSteerPoll();
5258
6003
  const bridge = new AcpPermissionBridge();
5259
6004
  conn.setPermissionHandler(bridge.createHandler(opts.approval, opts.approvalMode === "manual"));
5260
6005
  const sessionParams = { cwd: opts.cwd, mcpServers: [] };
@@ -5304,7 +6049,7 @@ var DimAcpExecutor = class {
5304
6049
  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
6050
  `);
5306
6051
  } else {
5307
- await conn.setModel(sess.sessionId, modelId).catch((err) => {
6052
+ await conn.setConfigOption(sess.sessionId, "model", modelId).catch((err) => {
5308
6053
  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
6054
  `);
5310
6055
  });
@@ -5314,7 +6059,6 @@ var DimAcpExecutor = class {
5314
6059
  });
5315
6060
  sessionId = sess.sessionId;
5316
6061
  pool?.touch();
5317
- markActivity();
5318
6062
  opts.onEvent?.(mapper.readyMessage(sessionId), "");
5319
6063
  const reopenSessionAfterInterrupt = async () => {
5320
6064
  try {
@@ -5345,7 +6089,6 @@ var DimAcpExecutor = class {
5345
6089
  activeSteer = null;
5346
6090
  opts.steer?.onTurnStarted?.({ turnNo, kind: turnKind });
5347
6091
  const result = await conn.prompt(sessionId, promptText, (update) => {
5348
- markActivity();
5349
6092
  pool?.touch();
5350
6093
  const { messages } = mapper.map(sessionId, update);
5351
6094
  for (const msg of messages) {
@@ -5378,37 +6121,18 @@ var DimAcpExecutor = class {
5378
6121
  }
5379
6122
  }
5380
6123
  maybeInterruptAtSafePoint();
5381
- }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
6124
+ }, timeout > 0 ? timeout + PROMPT_RPC_TIMEOUT_GRACE_MS : 0);
5382
6125
  if (settled)
5383
6126
  return;
5384
6127
  pool?.touch();
5385
- markActivity();
5386
- const usage2 = result.usage ?? mapper.latestUsage;
6128
+ const usage2 = result.usage ?? void 0;
5387
6129
  const cancelled = result.stopReason === "cancelled";
5388
6130
  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
6131
  if (cancelled && activeSteer) {
5401
6132
  interruptedTurns += 1;
5402
6133
  const abortedTools = emitAbortedTools();
5403
6134
  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
- }
6135
+ opts.steer?.onTurnInterrupted?.({ turnNo, reason: "steer", abortedTools });
5412
6136
  turnKind = "after-interrupt";
5413
6137
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
5414
6138
  turnNo = nextTurn;
@@ -5431,7 +6155,7 @@ var DimAcpExecutor = class {
5431
6155
  return;
5432
6156
  }
5433
6157
  const leftover = activeSteer ?? opts.steer?.peek() ?? null;
5434
- if (leftover && turnNo + 1 <= maxTurns && !wallClockExceeded()) {
6158
+ if (leftover) {
5435
6159
  const nextTurn = turnNo + 1;
5436
6160
  turnKind = "appended";
5437
6161
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
@@ -5532,7 +6256,15 @@ function createDimEventNormalizer(opts) {
5532
6256
  let currentMessageId = null;
5533
6257
  let currentText = "";
5534
6258
  let hasBoundarySignal = false;
6259
+ let pendingUserText = "";
6260
+ const flushUserMessage = () => {
6261
+ if (pendingUserText.trim().length > 0) {
6262
+ opts.onEvent({ type: "user_message", text: pendingUserText });
6263
+ }
6264
+ pendingUserText = "";
6265
+ };
5535
6266
  const startMessage = (messageId) => {
6267
+ flushUserMessage();
5536
6268
  if (currentMessageId !== null && currentText.trim().length > 0) {
5537
6269
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5538
6270
  }
@@ -5545,7 +6277,22 @@ function createDimEventNormalizer(opts) {
5545
6277
  const eventType = msg.eventType;
5546
6278
  if (!eventType)
5547
6279
  return;
6280
+ if (eventType !== "user_message:delta")
6281
+ flushUserMessage();
5548
6282
  switch (eventType) {
6283
+ case "user_message:delta": {
6284
+ const delta = msg.payload?.delta ?? "";
6285
+ if (delta)
6286
+ pendingUserText += delta;
6287
+ break;
6288
+ }
6289
+ case "usage:context": {
6290
+ const usage2 = msg.payload?.usage;
6291
+ if (usage2 && typeof usage2 === "object") {
6292
+ opts.onEvent({ type: "usage", usage: normalizeUsage(usage2) });
6293
+ }
6294
+ break;
6295
+ }
5549
6296
  case "message:started": {
5550
6297
  const messageId = extractMessageId(msg.payload) ?? `msg-${msg.payload?.messageId ?? "run"}`;
5551
6298
  startMessage(messageId);
@@ -5560,9 +6307,12 @@ function createDimEventNormalizer(opts) {
5560
6307
  currentText = "";
5561
6308
  break;
5562
6309
  }
5563
- case "thinking:delta":
6310
+ case "thinking:delta": {
6311
+ const delta = msg.payload?.delta ?? "";
6312
+ if (delta)
6313
+ opts.onEvent({ type: "assistant_thinking_delta", text: delta });
5564
6314
  break;
5565
- // 思考过程不进最终回复
6315
+ }
5566
6316
  case "text:delta": {
5567
6317
  const delta = msg.payload?.delta ?? "";
5568
6318
  if (!delta)
@@ -5636,6 +6386,7 @@ function createDimEventNormalizer(opts) {
5636
6386
  break;
5637
6387
  }
5638
6388
  case "run:ended": {
6389
+ flushUserMessage();
5639
6390
  if (currentMessageId !== null && currentText.trim().length > 0) {
5640
6391
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5641
6392
  } else if (!hasBoundarySignal && finalText.trim()) {
@@ -5673,7 +6424,11 @@ function normalizeUsage(raw) {
5673
6424
  return {
5674
6425
  inputTokens: typeof u.inputTokens === "number" ? u.inputTokens : void 0,
5675
6426
  outputTokens: typeof u.outputTokens === "number" ? u.outputTokens : void 0,
5676
- totalTokens: typeof u.totalTokens === "number" ? u.totalTokens : void 0
6427
+ totalTokens: typeof u.totalTokens === "number" ? u.totalTokens : void 0,
6428
+ // ACP usage_update(上下文占用语义,P0-8):透传给前端状态行
6429
+ contextUsed: typeof u.contextUsed === "number" ? u.contextUsed : void 0,
6430
+ contextSize: typeof u.contextSize === "number" ? u.contextSize : void 0,
6431
+ cost: u.cost && typeof u.cost === "object" && typeof u.cost.amount === "number" ? u.cost : null
5677
6432
  };
5678
6433
  }
5679
6434
  var DIM_CAPABILITIES = {
@@ -5728,8 +6483,8 @@ var DimAgentAdapter = class {
5728
6483
  }
5729
6484
  }
5730
6485
  async probe() {
5731
- const { existsSync: existsSync19 } = await import("node:fs");
5732
- const installed = existsSync19(this.dimPath) || this.dimPath === "dim";
6486
+ const { existsSync: existsSync18 } = await import("node:fs");
6487
+ const installed = existsSync18(this.dimPath) || this.dimPath === "dim";
5733
6488
  let authStatus = installed ? "unknown" : "error";
5734
6489
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
5735
6490
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -6021,19 +6776,19 @@ function pickLegacyDimModels(inventory) {
6021
6776
 
6022
6777
  // src/daemon/repo-cache.ts
6023
6778
  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";
6779
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "node:fs";
6780
+ import { dirname as dirname7, join as join15 } from "node:path";
6026
6781
  function ownerOf(repoFullName) {
6027
6782
  const idx = repoFullName.indexOf("/");
6028
6783
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
6029
6784
  }
6030
6785
  function buildRepoCachePath(workdir, tenantId, repoFullName) {
6031
- return join14(workdir, "repos", tenantId, ownerOf(repoFullName), repoFullName);
6786
+ return join15(workdir, "repos", tenantId, ownerOf(repoFullName), repoFullName);
6032
6787
  }
6033
6788
  function openRepoCache(workdir, tenantId, repoFullName, env, agentRemoteUrl, daemonRemoteUrl) {
6034
6789
  const repoCachePath = buildRepoCachePath(workdir, tenantId, repoFullName);
6035
6790
  const currentOrigin = () => {
6036
- if (!existsSync11(join14(repoCachePath, ".git")))
6791
+ if (!existsSync11(join15(repoCachePath, ".git")))
6037
6792
  return null;
6038
6793
  try {
6039
6794
  return execFileSync7("git", ["-C", repoCachePath, "remote", "get-url", "origin"], { encoding: "utf8", env, stdio: "pipe" }).trim() || null;
@@ -6046,11 +6801,11 @@ function openRepoCache(workdir, tenantId, repoFullName, env, agentRemoteUrl, dae
6046
6801
  return {
6047
6802
  repoCachePath,
6048
6803
  ensure() {
6049
- if (existsSync11(join14(repoCachePath, ".git"))) {
6804
+ if (existsSync11(join15(repoCachePath, ".git"))) {
6050
6805
  syncRemoteUrl(repoCachePath, originUrl(), env);
6051
6806
  return;
6052
6807
  }
6053
- mkdirSync8(dirname6(repoCachePath), { recursive: true });
6808
+ mkdirSync9(dirname7(repoCachePath), { recursive: true });
6054
6809
  const cloneUrl = fetchUrl();
6055
6810
  try {
6056
6811
  execFileSync7("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
@@ -6086,24 +6841,24 @@ function gitErrorText2(err) {
6086
6841
  // src/daemon/repo-checkout-service.ts
6087
6842
  import { createServer as createServer3 } from "node:http";
6088
6843
  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";
6844
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, readdirSync as readdirSync5, writeFileSync as writeFileSync9 } from "node:fs";
6845
+ import { basename as basename2, join as join18 } from "node:path";
6091
6846
 
6092
6847
  // src/daemon/worktree-manager.ts
6093
6848
  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";
6849
+ 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";
6850
+ import { basename, join as join17, sep as sep2 } from "node:path";
6096
6851
 
6097
6852
  // 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";
6853
+ import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
6854
+ import { join as join16 } from "node:path";
6100
6855
  var WORKTREE_MARKER_FILE = ".morula-worktree.json";
6101
6856
  var WORKTREE_MARKER_SCHEMA = 1;
6102
6857
  function markerPath(worktreePath) {
6103
- return join15(worktreePath, WORKTREE_MARKER_FILE);
6858
+ return join16(worktreePath, WORKTREE_MARKER_FILE);
6104
6859
  }
6105
6860
  function writeWorktreeMarker(worktreePath, marker) {
6106
- writeFileSync6(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6861
+ writeFileSync7(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6107
6862
  `, "utf8");
6108
6863
  }
6109
6864
  function readWorktreeMarker(worktreePath) {
@@ -6135,14 +6890,14 @@ function buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code) {
6135
6890
  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
6891
  }
6137
6892
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6138
- return join16(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6893
+ return join17(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6139
6894
  }
6140
6895
  function buildWorktreePath(workdir, tenantId, runtimeId, workContextId, code, repoFullName) {
6141
- return join16(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6896
+ return join17(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6142
6897
  }
6143
6898
  function buildLegacyWorktreePath(workdir, tenantId, workContextId, code) {
6144
6899
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6145
- return join16(workdir, "worktrees", tenantId, dirName);
6900
+ return join17(workdir, "worktrees", tenantId, dirName);
6146
6901
  }
6147
6902
  var legacyPathWarned = /* @__PURE__ */ new Set();
6148
6903
  function warnLegacyWorktreeResidue(legacyPath) {
@@ -6153,13 +6908,13 @@ function warnLegacyWorktreeResidue(legacyPath) {
6153
6908
  }
6154
6909
  var legacyLayoutWarned = /* @__PURE__ */ new Set();
6155
6910
  function warnLegacyLayoutWorktreeResidue(taskRoot, worktreePath) {
6156
- if (legacyLayoutWarned.has(taskRoot) || existsSync13(worktreePath) || !existsSync13(join16(taskRoot, WORKTREE_MARKER_FILE)))
6911
+ if (legacyLayoutWarned.has(taskRoot) || existsSync13(worktreePath) || !existsSync13(join17(taskRoot, WORKTREE_MARKER_FILE)))
6157
6912
  return;
6158
6913
  legacyLayoutWarned.add(taskRoot);
6159
6914
  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
6915
  }
6161
6916
  function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6162
- const gitignorePath = join16(worktreePath, ".gitignore");
6917
+ const gitignorePath = join17(worktreePath, ".gitignore");
6163
6918
  const existing = existsSync13(gitignorePath) ? readFileSync9(gitignorePath, "utf8") : "";
6164
6919
  const requiredLines = [WORKTREE_MARKER_FILE];
6165
6920
  const missing = requiredLines.filter((l) => !existing.split("\n").some((line) => line.trim() === l));
@@ -6167,7 +6922,7 @@ function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6167
6922
  const next = `${existing.trimEnd() ? `${existing.trimEnd()}
6168
6923
  ` : ""}${missing.join("\n")}
6169
6924
  `;
6170
- writeFileSync7(gitignorePath, next, "utf8");
6925
+ writeFileSync8(gitignorePath, next, "utf8");
6171
6926
  try {
6172
6927
  execFileSync8("git", ["-C", worktreePath, "add", ".gitignore"], { env, stdio: "pipe" });
6173
6928
  execFileSync8(
@@ -6257,7 +7012,7 @@ function isIndexLockConflict(err) {
6257
7012
  return /index\.lock/i.test(text) || /另一个 git 进程|another git process/i.test(text);
6258
7013
  }
6259
7014
  function worktreeIndexLockPath(repoCachePath, worktreePath) {
6260
- return join16(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
7015
+ return join17(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
6261
7016
  }
6262
7017
  function indexLockPathFromError(text) {
6263
7018
  const m = text.match(/[^\s'‘’"]*index\.lock/);
@@ -6311,7 +7066,7 @@ var foreignResidueHits = /* @__PURE__ */ new Map();
6311
7066
  function inspectForeignDirectory(dir) {
6312
7067
  let entries = [];
6313
7068
  try {
6314
- entries = readdirSync3(dir, { withFileTypes: true });
7069
+ entries = readdirSync4(dir, { withFileTypes: true });
6315
7070
  } catch {
6316
7071
  }
6317
7072
  const topNames = entries.slice(0, 20).map((e) => `${e.name}${e.isDirectory() ? "/" : ""}`);
@@ -6325,7 +7080,7 @@ function inspectForeignDirectory(dir) {
6325
7080
  return;
6326
7081
  let children = [];
6327
7082
  try {
6328
- children = readdirSync3(p, { withFileTypes: true });
7083
+ children = readdirSync4(p, { withFileTypes: true });
6329
7084
  } catch {
6330
7085
  return;
6331
7086
  }
@@ -6333,9 +7088,9 @@ function inspectForeignDirectory(dir) {
6333
7088
  if (count >= FOREIGN_RESIDUE_SCAN_LIMIT)
6334
7089
  return;
6335
7090
  count++;
6336
- const full = join16(p, e.name);
7091
+ const full = join17(p, e.name);
6337
7092
  try {
6338
- const st = statSync2(full);
7093
+ const st = statSync3(full);
6339
7094
  bytes += st.size;
6340
7095
  minMs = Math.min(minMs, st.mtimeMs);
6341
7096
  maxMs = Math.max(maxMs, st.mtimeMs);
@@ -6421,7 +7176,7 @@ function prepareWorktree(input) {
6421
7176
  if (!isGitRepo) {
6422
7177
  const forensics = inspectForeignDirectory(worktreePath);
6423
7178
  try {
6424
- rmSync7(worktreePath, { recursive: true, force: true });
7179
+ rmSync9(worktreePath, { recursive: true, force: true });
6425
7180
  } catch {
6426
7181
  reportForeignResidue(worktreePath, forensics, false);
6427
7182
  throw new Error(`WORKTREE_RESIDUE: \u76EE\u5F55 ${worktreePath} \u6B8B\u7559\u4E14\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u4EBA\u5DE5\u5904\u7406`);
@@ -6477,7 +7232,7 @@ function prepareWorktree(input) {
6477
7232
  }
6478
7233
  }
6479
7234
  if (!existsSync13(worktreePath)) {
6480
- mkdirSync9(join16(worktreePath, ".."), { recursive: true });
7235
+ mkdirSync10(join17(worktreePath, ".."), { recursive: true });
6481
7236
  addWorktreeWithRetry(cache.repoCachePath, worktreePath, `origin/${input.baseBranch}`);
6482
7237
  writeWorktreeMarker(worktreePath, {
6483
7238
  schemaVersion: 1,
@@ -6494,7 +7249,7 @@ function prepareWorktree(input) {
6494
7249
  {
6495
7250
  const dotMorulaRescue = `${worktreePath}.morula-rescue`;
6496
7251
  if (existsSync13(dotMorulaRescue)) {
6497
- const dotMorulaDir = join16(worktreePath, ".morula");
7252
+ const dotMorulaDir = join17(worktreePath, ".morula");
6498
7253
  if (!existsSync13(dotMorulaDir)) {
6499
7254
  try {
6500
7255
  renameSync2(dotMorulaRescue, dotMorulaDir);
@@ -6503,7 +7258,7 @@ function prepareWorktree(input) {
6503
7258
  console.warn(`[daemon] worktree \u6F02\u79FB\u81EA\u52A8\u6062\u590D: .morula/ \u5F52\u8FD8\u5931\u8D25\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${dotMorulaRescue}`);
6504
7259
  }
6505
7260
  } else {
6506
- rmSync7(dotMorulaRescue, { recursive: true, force: true });
7261
+ rmSync9(dotMorulaRescue, { recursive: true, force: true });
6507
7262
  }
6508
7263
  }
6509
7264
  }
@@ -6523,10 +7278,10 @@ function prepareWorktree(input) {
6523
7278
  };
6524
7279
  }
6525
7280
  function resolveWorkspaceLayout(root, expected) {
6526
- const primaryDir = join16(root, safeDirName(expected.repoFullName));
6527
- if (existsSync13(join16(primaryDir, WORKTREE_MARKER_FILE)))
7281
+ const primaryDir = join17(root, safeDirName(expected.repoFullName));
7282
+ if (existsSync13(join17(primaryDir, WORKTREE_MARKER_FILE)))
6528
7283
  return { primaryDir, legacy: false };
6529
- if (existsSync13(join16(root, WORKTREE_MARKER_FILE)))
7284
+ if (existsSync13(join17(root, WORKTREE_MARKER_FILE)))
6530
7285
  return { primaryDir: root, legacy: true };
6531
7286
  return null;
6532
7287
  }
@@ -6550,7 +7305,7 @@ function validateRemovable(taskRoot, expected, repoCachePath) {
6550
7305
  ).join("\n");
6551
7306
  if (statusRelevant.length > 0)
6552
7307
  return "WORKTREE_DIRTY";
6553
- const additionalActivity = legacy ? findActivityInReposDir(join16(primaryDir, ".morula", "repos")) : findAdditionalRepoActivity(taskRoot, { excludeDirs: [primaryDir] });
7308
+ const additionalActivity = legacy ? findActivityInReposDir(join17(primaryDir, ".morula", "repos")) : findAdditionalRepoActivity(taskRoot, { excludeDirs: [primaryDir] });
6554
7309
  if (additionalActivity)
6555
7310
  return "WORKTREE_DIRTY";
6556
7311
  try {
@@ -6593,13 +7348,13 @@ function findActivityInReposDir(reposDir) {
6593
7348
  return null;
6594
7349
  let names = [];
6595
7350
  try {
6596
- names = readdirSync3(reposDir);
7351
+ names = readdirSync4(reposDir);
6597
7352
  } catch {
6598
7353
  return null;
6599
7354
  }
6600
7355
  for (const name of names) {
6601
- const repoPath = join16(reposDir, name);
6602
- if (!existsSync13(join16(repoPath, ".git")))
7356
+ const repoPath = join17(reposDir, name);
7357
+ if (!existsSync13(join17(repoPath, ".git")))
6603
7358
  continue;
6604
7359
  if (repoHasActivity(repoPath))
6605
7360
  return repoPath;
@@ -6610,20 +7365,20 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6610
7365
  const excluded = new Set(options.excludeDirs ?? []);
6611
7366
  let entries = [];
6612
7367
  try {
6613
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7368
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6614
7369
  } catch {
6615
7370
  return null;
6616
7371
  }
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");
7372
+ const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join17(taskRoot, e.name)).filter((dir) => !excluded.has(dir));
7373
+ const legacyReposDir = join17(taskRoot, ".morula", "repos");
6619
7374
  if (existsSync13(legacyReposDir)) {
6620
7375
  try {
6621
- candidates.push(...readdirSync3(legacyReposDir).map((n) => join16(legacyReposDir, n)));
7376
+ candidates.push(...readdirSync4(legacyReposDir).map((n) => join17(legacyReposDir, n)));
6622
7377
  } catch {
6623
7378
  }
6624
7379
  }
6625
7380
  for (const repoPath of candidates) {
6626
- if (!existsSync13(join16(repoPath, ".git")))
7381
+ if (!existsSync13(join17(repoPath, ".git")))
6627
7382
  continue;
6628
7383
  if (repoHasActivity(repoPath))
6629
7384
  return repoPath;
@@ -6631,18 +7386,18 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6631
7386
  return null;
6632
7387
  }
6633
7388
  function cleanupInjectedSkills(dir) {
6634
- const injectedSkillsRoot = join16(dir, ".agents", "skills");
7389
+ const injectedSkillsRoot = join17(dir, ".agents", "skills");
6635
7390
  if (!existsSync13(injectedSkillsRoot))
6636
7391
  return;
6637
7392
  try {
6638
- for (const name of readdirSync3(injectedSkillsRoot)) {
7393
+ for (const name of readdirSync4(injectedSkillsRoot)) {
6639
7394
  if (name === COLLABORATION_SKILL_NAME || /^[\w-]{1,64}$/.test(name)) {
6640
- rmSync7(join16(injectedSkillsRoot, name), { recursive: true, force: true });
7395
+ rmSync9(join17(injectedSkillsRoot, name), { recursive: true, force: true });
6641
7396
  }
6642
7397
  }
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 });
7398
+ if (readdirSync4(injectedSkillsRoot).length === 0) {
7399
+ rmSync9(join17(dir, ".agents", "skills"), { recursive: true, force: true });
7400
+ rmSync9(join17(dir, ".agents"), { recursive: true, force: true });
6646
7401
  }
6647
7402
  } catch {
6648
7403
  }
@@ -6651,15 +7406,15 @@ function collectSiblingGitCommonDirs(taskRoot, excludeDir) {
6651
7406
  const dirs = /* @__PURE__ */ new Set();
6652
7407
  let entries = [];
6653
7408
  try {
6654
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7409
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6655
7410
  } catch {
6656
7411
  return [];
6657
7412
  }
6658
7413
  for (const e of entries) {
6659
7414
  if (!e.isDirectory() || e.name.startsWith("."))
6660
7415
  continue;
6661
- const repoPath = join16(taskRoot, e.name);
6662
- if (repoPath === excludeDir || !existsSync13(join16(repoPath, ".git")))
7416
+ const repoPath = join17(taskRoot, e.name);
7417
+ if (repoPath === excludeDir || !existsSync13(join17(repoPath, ".git")))
6663
7418
  continue;
6664
7419
  try {
6665
7420
  const commonDir = execFileSync8("git", ["-C", repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
@@ -6676,13 +7431,13 @@ function removeTaskWorkspace(repoCachePath, taskRoot, expected) {
6676
7431
  throw new Error("MARKER_MISMATCH");
6677
7432
  const { primaryDir, legacy } = layout;
6678
7433
  if (!legacy) {
6679
- if (primaryDir !== join16(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
7434
+ if (primaryDir !== join17(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
6680
7435
  throw new Error("GIT_REMOVE_FAILED");
6681
7436
  }
6682
7437
  cleanupInjectedSkills(legacy ? primaryDir : taskRoot);
6683
7438
  const siblingCommonDirs = legacy ? [] : collectSiblingGitCommonDirs(taskRoot, primaryDir);
6684
7439
  try {
6685
- rmSync7(taskRoot, { recursive: true, force: true });
7440
+ rmSync9(taskRoot, { recursive: true, force: true });
6686
7441
  } catch {
6687
7442
  throw new Error("GIT_REMOVE_FAILED");
6688
7443
  }
@@ -6706,7 +7461,7 @@ function removeWorktree(repoCachePath, worktreePath) {
6706
7461
  }
6707
7462
 
6708
7463
  // src/daemon/repo-checkout-service.ts
6709
- var CHECKOUT_JSON_FILE = join17(".morula", "checkout.json");
7464
+ var CHECKOUT_JSON_FILE = join18(".morula", "checkout.json");
6710
7465
  var LocalRepoCheckoutService = class {
6711
7466
  constructor(opts) {
6712
7467
  this.opts = opts;
@@ -6794,9 +7549,9 @@ var LocalRepoCheckoutService = class {
6794
7549
  return;
6795
7550
  this.tasks.set(taskId, { workdir, checkToken, primaryRepoDir });
6796
7551
  try {
6797
- const dir = join17(workdir, ".morula");
6798
- mkdirSync10(dir, { recursive: true, mode: 448 });
6799
- writeFileSync8(join17(dir, "checkout.json"), JSON.stringify({
7552
+ const dir = join18(workdir, ".morula");
7553
+ mkdirSync11(dir, { recursive: true, mode: 448 });
7554
+ writeFileSync9(join18(dir, "checkout.json"), JSON.stringify({
6800
7555
  taskId,
6801
7556
  port: this.boundPort,
6802
7557
  checkToken
@@ -6960,11 +7715,11 @@ function isReadOnlyRepoDir(dirName) {
6960
7715
  return READ_ONLY_DIR_RE.test(dirName) || dirName.startsWith(LEGACY_READ_ONLY_DIR_PREFIX);
6961
7716
  }
6962
7717
  function resolveRepoTargetDir(taskRoot, base) {
6963
- const direct = join17(taskRoot, base);
6964
- if (!existsSync14(direct) || existsSync14(join17(direct, ".git")))
7718
+ const direct = join18(taskRoot, base);
7719
+ if (!existsSync14(direct) || existsSync14(join18(direct, ".git")))
6965
7720
  return direct;
6966
7721
  for (let i = 2; i < 100; i++) {
6967
- const candidate = join17(taskRoot, `${base}-${i}`);
7722
+ const candidate = join18(taskRoot, `${base}-${i}`);
6968
7723
  if (!existsSync14(candidate))
6969
7724
  return candidate;
6970
7725
  }
@@ -6973,7 +7728,7 @@ function resolveRepoTargetDir(taskRoot, base) {
6973
7728
  function materializeReadWorktree(input) {
6974
7729
  const worktreePath = resolveRepoTargetDir(input.taskRoot, `${safeRepoDirName(input.repoFullName)}${READ_ONLY_REPO_DIR_SUFFIX}`);
6975
7730
  const targetRef = resolveReadRef(input.repoCachePath, input.env, input.ref, input.baseBranch);
6976
- if (existsSync14(join17(worktreePath, ".git"))) {
7731
+ if (existsSync14(join18(worktreePath, ".git"))) {
6977
7732
  const current = currentBranchName(worktreePath);
6978
7733
  if (current)
6979
7734
  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 +7736,7 @@ function materializeReadWorktree(input) {
6981
7736
  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
7737
  execFileSync9("git", ["-C", worktreePath, "checkout", "--detach", targetRef], { env: input.env, stdio: "pipe" });
6983
7738
  } else {
6984
- mkdirSync10(join17(worktreePath, ".."), { recursive: true });
7739
+ mkdirSync11(join18(worktreePath, ".."), { recursive: true });
6985
7740
  execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", worktreePath, targetRef], {
6986
7741
  env: input.env,
6987
7742
  stdio: "pipe"
@@ -7049,11 +7804,11 @@ function listMaterializedRepoDirs(taskRoot, options = {}) {
7049
7804
  const excluded = new Set(options.excludeDirs ?? []);
7050
7805
  let entries;
7051
7806
  try {
7052
- entries = readdirSync4(taskRoot, { withFileTypes: true });
7807
+ entries = readdirSync5(taskRoot, { withFileTypes: true });
7053
7808
  } catch {
7054
7809
  return [];
7055
7810
  }
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();
7811
+ 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
7812
  }
7058
7813
  function parseRepoFullNameFromRemoteUrl(url) {
7059
7814
  const raw = url?.trim();
@@ -7075,7 +7830,7 @@ function parseRepoFullNameFromRemoteUrl(url) {
7075
7830
  }
7076
7831
  function collectMaterializedRepoDeliveryTargets(input) {
7077
7832
  const excludedNames = new Set(input.excludeRepoFullNames?.filter(Boolean) ?? []);
7078
- const excludedDirs = [...excludedNames].map((name) => join17(input.taskRoot, safeRepoDirName(name)));
7833
+ const excludedDirs = [...excludedNames].map((name) => join18(input.taskRoot, safeRepoDirName(name)));
7079
7834
  const recordsByDir = /* @__PURE__ */ new Map();
7080
7835
  const recordsByDirName = /* @__PURE__ */ new Map();
7081
7836
  for (const [repoFullName, record] of input.prepared ?? []) {
@@ -7138,7 +7893,7 @@ function syncGitIdentity(primaryDir, targetDir, env) {
7138
7893
  }
7139
7894
  }
7140
7895
  function materializeAdditionalRepo(input) {
7141
- if (existsSync14(join17(input.targetPath, ".git"))) {
7896
+ if (existsSync14(join18(input.targetPath, ".git"))) {
7142
7897
  const current = currentBranchName(input.targetPath);
7143
7898
  if (!current && !hasLocalChanges2(input.targetPath, input.env)) {
7144
7899
  const targetRef = resolveReadRef(input.repoCachePath, input.env, void 0, input.baseBranch);
@@ -7158,7 +7913,7 @@ function materializeAdditionalRepo(input) {
7158
7913
  }
7159
7914
  if (!baseRef)
7160
7915
  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 });
7916
+ mkdirSync11(join18(input.targetPath, ".."), { recursive: true });
7162
7917
  execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", input.targetPath, baseRef], {
7163
7918
  env: input.env,
7164
7919
  stdio: "pipe"
@@ -7304,6 +8059,811 @@ function buildStageContract(stageCode, source) {
7304
8059
  return STAGE_CONTRACTS[stageCode] ?? "";
7305
8060
  }
7306
8061
 
8062
+ // src/daemon/session-host.ts
8063
+ init_dist2();
8064
+ init_acp_connection();
8065
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "node:fs";
8066
+ import { join as join19 } from "node:path";
8067
+
8068
+ // src/daemon/steward-instructions.ts
8069
+ var STEWARD_SYSTEM_INSTRUCTIONS = `\u4F60\u662F\u300C\u7A7A\u95F4\u7BA1\u5BB6\u667A\u80FD\u4F53\u300D\uFF08Project Steward\uFF09\u3002
8070
+
8071
+ \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
8072
+ \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
8073
+
8074
+ ## \u4F60\u7684\u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177
8075
+
8076
+ - \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
8077
+ - \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
8078
+ - \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
8079
+ \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`;
8080
+ function buildStewardContextMd(facts) {
8081
+ const lines = [
8082
+ "# \u7BA1\u5BB6\u4F1A\u8BDD\u4E0A\u4E0B\u6587",
8083
+ "",
8084
+ "| \u4E8B\u5B9E | \u503C |",
8085
+ "|------|-----|",
8086
+ `| \u7A7A\u95F4 ID\uFF08\`--space\` \u7528\u5B83\uFF09 | \`${facts.projectId}\` |`,
8087
+ `| \u7A7A\u95F4\u540D\u79F0 | ${facts.projectName?.trim() || "\uFF08\u672A\u547D\u540D\uFF09"} |`,
8088
+ `| \u79DF\u6237 | \`${facts.tenantId}\` |`,
8089
+ `| \u5F53\u524D\u7528\u6237 | ${facts.userName?.trim() || "\uFF08\u672A\u77E5\uFF09"} |`
8090
+ ];
8091
+ if (facts.stewardPrincipalId)
8092
+ lines.push(`| \u7BA1\u5BB6\u7F72\u540D\u8EAB\u4EFD | \`${facts.stewardPrincipalId}\` |`);
8093
+ lines.push("");
8094
+ if (facts.repos?.length) {
8095
+ lines.push("## \u672C\u7A7A\u95F4\u7ED1\u5B9A\u4ED3\u5E93", "");
8096
+ for (const repo of facts.repos)
8097
+ lines.push(`- \`${repo}\``);
8098
+ 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");
8099
+ } else {
8100
+ lines.push("## \u672C\u7A7A\u95F4\u7ED1\u5B9A\u4ED3\u5E93", "", "\uFF08\u672A\u7ED1\u5B9A\u4ED3\u5E93\uFF09");
8101
+ }
8102
+ 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");
8103
+ return `${lines.join("\n")}
8104
+ `;
8105
+ }
8106
+
8107
+ // src/daemon/steward-skill.ts
8108
+ var STEWARD_SKILL_NAME = "steward-management";
8109
+ var STEWARD_SKILL_FRONTMATTER = `---
8110
+ name: steward-management
8111
+ 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."
8112
+ user-invocable: false
8113
+ allowed-tools: Bash(morula *)
8114
+ ---
8115
+ `;
8116
+ var STEWARD_SKILL_BODY = `# \u7A7A\u95F4\u7BA1\u5BB6\u5951\u7EA6\uFF08Morula\uFF09
8117
+
8118
+ \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**
8119
+ \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
8120
+
8121
+ ## 0. \u804C\u8D23\u8FB9\u754C\u4E0E\u786C\u7EA6\u675F
8122
+
8123
+ ### 0.1 \u804C\u8D23\u8FB9\u754C
8124
+
8125
+ - **\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
8126
+ - **\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
8127
+ - **\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
8128
+ - **\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
8129
+ - \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
8130
+
8131
+ ### 0.2 \u786C\u7EA6\u675F
8132
+
8133
+ - **\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
8134
+ - **\u4E0D\u8981\u7528\u4F9D\u8D56\u4EFB\u52A1\u4E0A\u4E0B\u6587\u7684\u547D\u4EE4**\uFF1A\u672C\u4F1A\u8BDD**\u6CA1\u6709 \`MORULA_TASK_ID\`**\u3002
8135
+ \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
8136
+ \`morula task get\`\uFF08\u65E0\u53C2\u5F62\u5F0F\uFF09\u3001\`morula task branch\`\u3001\`morula task state\`\u3001
8137
+ \`morula task analysis-verdict\`\u3001\`morula task inbox\`\u3001\`morula review submit\`\u3001
8138
+ \`morula repo checkout\`\u3001\`morula repo prepare\`\u3001\`morula repo mr *\`\u3001\`morula attachment upload\`\u3002
8139
+ \u540C\u7406\uFF0C**CI \u547D\u4EE4\u4E00\u5F8B\u7528 \`--space\` \u5F62\u5F0F**\uFF08\`morula ci status --space <id>\`\uFF09\uFF0C
8140
+ \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
8141
+ - **\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
8142
+ - **\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
8143
+ - \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
8144
+ - **\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
8145
+
8146
+ ## 1. \u5E38\u7528\u547D\u4EE4\uFF08\u5168\u90E8\u663E\u5F0F\u53C2\u6570\uFF09
8147
+
8148
+ ### \u770B\u7A7A\u95F4\u4E0E\u4EFB\u52A1
8149
+ \`\`\`bash
8150
+ morula steward get --space <spaceId> # \u7BA1\u5BB6\u72B6\u6001 / \u7A7A\u95F4\u8BF4\u660E\uFF08AGENTS.md\uFF09/ \u9636\u6BB5\u6267\u884C\u7ED1\u5B9A
8151
+ morula task list --space <spaceId> [--state <code>] [--assignee <principalId>] [--search <text>]
8152
+ morula task get <objectId> # \u5355\u4E2A\u4E1A\u52A1\u4EFB\u52A1\u8BE6\u60C5\uFF08\u5FC5\u987B\u5E26 id\uFF09
8153
+ 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
8154
+ morula comment list --object <objectId> # \u4EFB\u52A1\u8BC4\u8BBA\uFF08\u5206\u6790\u7ED3\u8BBA / \u8BC4\u5BA1\u610F\u89C1\u90FD\u5728\u8FD9\u91CC\uFF09
8155
+ morula space list # \u53EF\u89C1\u7A7A\u95F4
8156
+ morula space-member list --space <spaceId> # \u7A7A\u95F4\u6210\u5458\uFF08\u627E\u8D1F\u8D23\u4EBA principalId\uFF09
8157
+ morula dashboard get --space <spaceId> # \u6982\u89C8\uFF1A\u4EFB\u52A1\u603B\u6570/\u5B8C\u6210\u7387/\u72B6\u6001\u5206\u5E03/\u6210\u5458\u8D1F\u8377
8158
+ morula dashboard statistics --space <spaceId> # \u6309\u4F18\u5148\u7EA7\u7EDF\u8BA1
8159
+ morula dashboard workload --space <spaceId> # \u6210\u5458\u8D1F\u8377\u660E\u7EC6
8160
+ \`\`\`
8161
+
8162
+ ### \u770B\u6D41\u6C34\u7EBF\uFF08CI/CD\uFF09
8163
+ \`\`\`bash
8164
+ 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
8165
+ morula ci deployments --space <spaceId> # \u90E8\u7F72\u8BB0\u5F55\uFF08\u53EA\u8BFB\u5C55\u793A\uFF09
8166
+ 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
8167
+ 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
8168
+ 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**
8169
+ \`\`\`
8170
+
8171
+ - **\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
8172
+ **\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
8173
+ - **\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
8174
+ \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
8175
+ - \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
8176
+ - \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
8177
+ - \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
8178
+ - \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
8179
+ - \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
8180
+
8181
+ ### \u6D3E\u5355
8182
+ \`\`\`bash
8183
+ morula steward dispatch --space <spaceId> --title "<\u4E00\u53E5\u8BDD\u76EE\u6807>" \\
8184
+ [--description "<\u80CC\u666F/\u52A8\u673A/\u76EE\u6807/\u7EA6\u675F>"] \\
8185
+ [--state backlog|todo|in_progress] \\
8186
+ [--assignee <principalId>] \\
8187
+ [--repo <owner/name>]
8188
+ \`\`\`
8189
+
8190
+ - \`--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
8191
+ - \`--state backlog\`\uFF1A\u53EA\u843D\u300C\u5F85\u89C4\u5212\u300D\uFF0C**\u4E0D\u6D3E\u53D1\u6267\u884C**\uFF1B\u7B49\u4EBA\u6216\u4F60\u540E\u7EED\u7528
8192
+ \`morula task update <objectId> --state todo\` \u63A8\u8FDB\u3002
8193
+ - \`--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
8194
+ - \`--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
8195
+ - \`--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
8196
+ \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
8197
+
8198
+ ### \u6D3E\u5355\u6D41\u7A0B\uFF08\u5206\u6790\u81EA\u52A8\u8FDB\u5F00\u53D1\uFF09
8199
+
8200
+ 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
8201
+ 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
8202
+ 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
8203
+
8204
+ ### \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
8205
+
8206
+ - **\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
8207
+ - **\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
8208
+ - \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
8209
+ - **\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
8210
+
8211
+ ### \u591A\u4ED3\u5E93\u7A7A\u95F4\uFF08\u7A7A\u95F4\u7ED1\u5B9A\u4E86\u591A\u4E2A\u4ED3\u5E93\u65F6\uFF09
8212
+
8213
+ - \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
8214
+ - **\u6D3E\u5355\u65F6\u5FC5\u987B\u628A\u6D89\u53CA\u4ED3\u5E93\u4F20\u7ED9 \`--repo\`**\uFF08\u5199\u4ED3\u5E93\u540D\uFF0C\u5982 \`nextim/morula-web\`\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
8215
+
8216
+ ### \u7BA1\u7406\u4EFB\u52A1\uFF08\u6539\u5C5E\u6027 / \u63A8\u8FDB\u72B6\u6001 / \u5EFA\u5355 / \u5220\u5355\uFF09
8217
+ \`\`\`bash
8218
+ morula task create --space <spaceId> --title "<\u6807\u9898>" [--description "<\u63CF\u8FF0>"] \\
8219
+ [--priority <low|medium|high|urgent>] [--state backlog|todo]
8220
+ morula task update <objectId> [--title <t>] [--description <d>] [--priority <p>] [--assignee <principalId>] [--state <code>]
8221
+ morula task delete <objectId> # \u5220\u9664\u4EFB\u52A1\uFF08\u7834\u574F\u6027\uFF1A\u5148\u8981\u786E\u8BA4\uFF09
8222
+ \`\`\`
8223
+
8224
+ - \`--state\` \u7528\u72B6\u6001 code\uFF08\`backlog\` / \`todo\` / \`in_progress\` / \`review\` / \`merged\` / \`released\` / \`closed\`\uFF09\uFF0C\u4E0D\u8981\u4F20 UUID\u3002
8225
+ - **\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
8226
+ - **\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
8227
+ - \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
8228
+ - \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
8229
+
8230
+ ### \u8BC4\u8BBA\u4E0E @ \u4EBA\uFF08\u50AC\u529E\uFF09
8231
+ \`\`\`bash
8232
+ morula comment add --object <objectId> --content "<\u5185\u5BB9>" [--format markdown] [--parent <commentId>] [--mention <principalId>]...
8233
+ morula comment update <commentId> --content "<\u65B0\u5185\u5BB9>"
8234
+ morula comment delete <commentId>
8235
+ \`\`\`
8236
+
8237
+ - \`--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
8238
+ - **\u4E0D\u8981 @ \u8FD0\u884C\u65F6**\uFF08\u89C1 \xA70.2\uFF09\u3002
8239
+ - \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
8240
+
8241
+ ### \u8FD0\u884C\u5E72\u9884\uFF08\u505C\u6389\u5361\u4F4F\u7684\u6267\u884C / \u91CD\u8DD1\u5931\u8D25\u7684\uFF09
8242
+ \`\`\`bash
8243
+ morula task agent-tasks --object <objectId> --status running # \u5148\u627E\u5230\u5728\u8DD1\u7684 AgentTask id
8244
+ morula task cancel <agentTaskId> # \u53D6\u6D88\uFF08\u7834\u574F\u6027\uFF1A\u5148\u8981\u786E\u8BA4\uFF09
8245
+ morula task rerun <agentTaskId> # \u91CD\u8DD1\uFF08\u4EC5 failed / cancelled \u53EF\u91CD\u8DD1\uFF1B\u5148\u8981\u786E\u8BA4\uFF09
8246
+ \`\`\`
8247
+
8248
+ - \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
8249
+ - **\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
8250
+ - \u505C/\u91CD\u8DD1\u540E\u56DE\u8BFB\u4E00\u6B21\u72B6\u6001\u786E\u8BA4\u7ED3\u679C\uFF0C\u518D\u5411\u7528\u6237\u6C47\u62A5\u3002
8251
+
8252
+ ## 2. \u88C1\u5B9A\u8F93\u51FA\u683C\u5F0F\uFF08\u524D\u7AEF\u6309\u6B64\u6E32\u67D3\uFF0C\u5FC5\u987B\u9075\u5B88\uFF09
8253
+
8254
+ \u56DE\u590D\u7528\u6237\u65F6\uFF0C\u7ED3\u8BBA\u884C\u5FC5\u987B\u662F\u4E0B\u5217\u56DB\u8005\u4E4B\u4E00\uFF0C\u5355\u72EC\u6210\u884C\uFF1A
8255
+
8256
+ - \`\u7ED3\u8BBA\uFF1A\u2705 \u5EFA\u8BAE\u505A\`
8257
+ - \`\u7ED3\u8BBA\uFF1A\u274C \u4E0D\u5EFA\u8BAE\u505A\`
8258
+ - \`\u7ED3\u8BBA\uFF1A\u23F8 \u6682\u7F13\`
8259
+ - \`\u7ED3\u8BBA\uFF1A\u2753 \u9700\u6F84\u6E05\`
8260
+
8261
+ \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
8262
+ \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
8263
+
8264
+ ## 3. \u4E8B\u5B9E\u7EAA\u5F8B\u4E0E\u8FDB\u5C55\u540C\u6B65
8265
+
8266
+ - **\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
8267
+ \u8FD4\u56DE\u503C\u91CC\u6CA1\u6709\u7F16\u53F7\u5C31\u4E0D\u8981\u5199\u7F16\u53F7\uFF0C**\u4E0D\u5F97\u81EA\u884C\u7F16\u9020**\u3002
8268
+ - \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
8269
+ - **\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
8270
+ - \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
8271
+ \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
8272
+ - \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
8273
+ \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
8274
+ `;
8275
+ function buildStewardSkillFile() {
8276
+ return `${STEWARD_SKILL_FRONTMATTER}
8277
+ ${STEWARD_SKILL_BODY}`;
8278
+ }
8279
+
8280
+ // src/daemon/session-host.ts
8281
+ var SESSION_IDLE_DEFAULT_MS = 30 * 60 * 1e3;
8282
+ var ELICITATION_DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
8283
+ var ATTACHMENT_FETCH_TIMEOUT_MS = 3e4;
8284
+ function safeAttachmentName(fileName) {
8285
+ const base = (fileName || "attachment").split(/[\\/]/).pop() ?? "attachment";
8286
+ const cleaned = base.replace(/[\u0000-\u001F<>:"|?*]/g, "_").replace(/^\.+/, "_").trim();
8287
+ return cleaned.slice(0, 120) || "attachment";
8288
+ }
8289
+ function resolveSessionIdleMs(env = process.env) {
8290
+ const raw = Number(env.MORULA_SESSION_IDLE_MS);
8291
+ return Number.isFinite(raw) && raw > 0 ? raw : SESSION_IDLE_DEFAULT_MS;
8292
+ }
8293
+ function resolveElicitationTimeoutMs(env = process.env) {
8294
+ const raw = Number(env.MORULA_DIM_APPROVAL_TIMEOUT_MS);
8295
+ return Number.isFinite(raw) && raw > 0 ? raw : ELICITATION_DEFAULT_TIMEOUT_MS;
8296
+ }
8297
+ var ELICITATION_POLL_WAIT_MS = 15e3;
8298
+ var SessionHost = class {
8299
+ constructor(options) {
8300
+ this.options = options;
8301
+ this.log = options.log ?? ((line) => console.log(line));
8302
+ this.idleMs = resolveSessionIdleMs();
8303
+ }
8304
+ options;
8305
+ sessions = /* @__PURE__ */ new Map();
8306
+ log;
8307
+ idleMs;
8308
+ /** 当前打开的会话(morula AgentSession.id 列表;诊断/测试用) */
8309
+ get openSessionIds() {
8310
+ return [...this.sessions.keys()];
8311
+ }
8312
+ /**
8313
+ * 打开会话:建/复用工作区 → 起**独立** dim acp 进程 → session/new(或 resume 续跑)
8314
+ * → 应用配置 → 指针回传。工作区保留(close 不清理)。
8315
+ *
8316
+ * **幂等**:已打开且进程存活 → 只刷新凭证/注入物与变化过的配置,**不重建进程**
8317
+ * (服务端每轮 prompt 前都会补发 open 做兜底:daemon 可能重启或空闲回收过,
8318
+ * 用同一条命令覆盖「首次打开」与「兜底确认」两种语义,避免两端各维护一套「是否已打开」状态)。
8319
+ */
8320
+ async open(cmd) {
8321
+ if (!cmd?.sessionId || !cmd?.tenantId || !cmd?.projectId)
8322
+ return { ok: false, error: "SESSION_OPEN_INVALID_ARGS" };
8323
+ const workspaceDir = join19(this.options.workdir, "steward", cmd.tenantId, cmd.projectId);
8324
+ mkdirSync12(workspaceDir, { recursive: true });
8325
+ this.writeInjections(workspaceDir, cmd);
8326
+ const existing = this.sessions.get(cmd.sessionId);
8327
+ if (existing?.conn.isAlive) {
8328
+ if (cmd.sessionToken) {
8329
+ this.options.api.setSessionToken(cmd.sessionId, cmd.sessionToken);
8330
+ existing.env.MORULA_SESSION_TOKEN = cmd.sessionToken;
8331
+ }
8332
+ this.touch(existing);
8333
+ const changed = this.diffConfig(existing.appliedConfig, this.configFrom(cmd));
8334
+ for (const [configId, value] of Object.entries(changed)) {
8335
+ await existing.conn.setConfigOptionFull(existing.dimSessionId, configId, value).then((raw) => {
8336
+ const full = normalizeAcpConfigOptions(raw) ?? [];
8337
+ if (full.length > 0) {
8338
+ void this.options.api.reportSessionBinding(cmd.sessionId, { configOptions: full }).catch(() => {
8339
+ });
8340
+ }
8341
+ }).catch((err) => this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u914D\u7F6E ${configId} \u589E\u91CF\u4E0B\u53D1\u5931\u8D25: ${err.message}`));
8342
+ }
8343
+ existing.appliedConfig = { ...existing.appliedConfig, ...changed };
8344
+ return { ok: true, dimSessionId: existing.dimSessionId, workspaceDir: existing.workspaceDir, alreadyOpen: true };
8345
+ }
8346
+ if (existing)
8347
+ await this.close({ sessionId: cmd.sessionId }, "reopen");
8348
+ if (cmd.sessionToken)
8349
+ this.options.api.setSessionToken(cmd.sessionId, cmd.sessionToken);
8350
+ const env = {
8351
+ ...cmd.sessionToken ? { MORULA_SESSION_TOKEN: cmd.sessionToken } : {},
8352
+ ...this.options.baseUrl ? { MORULA_API_URL: this.options.baseUrl } : {},
8353
+ // 会话上下文(对齐任务链路的 MORULA_TASK_ID):让 `morula` CLI 知道「我在哪个会话里」,
8354
+ // 派单幂等与 elicitation 回传需要它;管家会话**没有** MORULA_TASK_ID(任务专属命令会因此拒绝执行)
8355
+ MORULA_SESSION_ID: cmd.sessionId,
8356
+ MORULA_SPACE_ID: cmd.projectId,
8357
+ // 插件 bin 前置 + 显式 CLI 路径:否则 agent 会解析到宿主/桌面应用自带的旧 `morula`——
8358
+ // 新命令不存在,且旧 CLI 不认 MORULA_SESSION_TOKEN(回落用户凭证)→ 写操作署名错、
8359
+ // 写面作用域校验被绕过。PATH 只是第一道保险(dim 会重写 agent shell 的 PATH),
8360
+ // 可靠的是 `MORULA_CLI` 绝对路径(契约要求 agent 用它)。
8361
+ // 见 issues/steward-cli-resolves-stale-bundled-morula.md。
8362
+ PATH: prependPluginBinToPath(process.env.PATH),
8363
+ MORULA_CLI: pluginBinPath(),
8364
+ ...this.options.isolatedDimEnv ?? {}
8365
+ };
8366
+ const onExit = (code) => {
8367
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u7684 dim acp \u8FDB\u7A0B\u9000\u51FA\uFF08code=${code}\uFF09`);
8368
+ const managed2 = this.sessions.get(cmd.sessionId);
8369
+ if (managed2 && managed2.conn === conn) {
8370
+ this.sessions.delete(cmd.sessionId);
8371
+ this.options.api.clearSessionToken(cmd.sessionId);
8372
+ }
8373
+ };
8374
+ const connOptions = {
8375
+ dimPath: this.options.dimPath,
8376
+ cwd: workspaceDir,
8377
+ // process.env 值域含 undefined(ProcessEnv),显式收敛为 string 索引(spawn env 语义)
8378
+ env: { ...process.env, ...env },
8379
+ // P1-7:审批默认全自动放行(管家人在环收敛到 elicitation 一处,见 §4.7 取舍)。
8380
+ // 注意:dim 的权限引擎决定 allow/deny/ask,client 无法强制——`permission` 只选 preset。
8381
+ onPermissionRequest: async () => ({ outcome: { outcome: "selected", optionId: "allow" } }),
8382
+ onElicitation: async (request) => this.handleElicitation(cmd.sessionId, request),
8383
+ onExit
8384
+ };
8385
+ const conn = this.options.acpConnectionFactory ? this.options.acpConnectionFactory(connOptions) : new AcpConnection(connOptions);
8386
+ try {
8387
+ await conn.start();
8388
+ } catch (err) {
8389
+ return { ok: false, error: `dim acp \u542F\u52A8\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}` };
8390
+ }
8391
+ const sessionParams = { cwd: workspaceDir, mcpServers: [] };
8392
+ let dimSessionId;
8393
+ let resumed = false;
8394
+ let resumeFailed = null;
8395
+ let configOptions;
8396
+ if (cmd.dimSessionId) {
8397
+ try {
8398
+ const r = await conn.resumeSession({ ...sessionParams, sessionId: cmd.dimSessionId });
8399
+ configOptions = normalizeAcpConfigOptions(r.configOptions) ?? void 0;
8400
+ dimSessionId = cmd.dimSessionId;
8401
+ resumed = true;
8402
+ } catch {
8403
+ try {
8404
+ const r = await conn.loadSession({ ...sessionParams, sessionId: cmd.dimSessionId });
8405
+ configOptions = normalizeAcpConfigOptions(r.configOptions) ?? void 0;
8406
+ dimSessionId = cmd.dimSessionId;
8407
+ resumed = true;
8408
+ } catch (err) {
8409
+ resumeFailed = err instanceof Error ? err.message : String(err);
8410
+ const created = await conn.newSession(sessionParams);
8411
+ configOptions = normalizeAcpConfigOptions(created.configOptions) ?? void 0;
8412
+ dimSessionId = created.sessionId;
8413
+ }
8414
+ }
8415
+ } else {
8416
+ const created = await conn.newSession(sessionParams);
8417
+ configOptions = normalizeAcpConfigOptions(created.configOptions) ?? void 0;
8418
+ dimSessionId = created.sessionId;
8419
+ }
8420
+ const appliedConfig = this.configFrom(cmd);
8421
+ const permission = appliedConfig.permission || "workspace-write";
8422
+ const applyConfig = async (configId, value) => {
8423
+ try {
8424
+ const full = normalizeAcpConfigOptions(await conn.setConfigOptionFull(dimSessionId, configId, value)) ?? [];
8425
+ if (full.length > 0)
8426
+ configOptions = full;
8427
+ } catch (err) {
8428
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u914D\u7F6E ${configId}=${value} \u4E0B\u53D1\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8429
+ }
8430
+ };
8431
+ await applyConfig("permission", permission);
8432
+ if (appliedConfig.modelId)
8433
+ await applyConfig("model", appliedConfig.modelId);
8434
+ if (appliedConfig.thoughtLevel)
8435
+ await applyConfig("thought_level", appliedConfig.thoughtLevel);
8436
+ if (appliedConfig.agentMode)
8437
+ await applyConfig("mode", appliedConfig.agentMode);
8438
+ void this.options.api.reportSessionBinding(cmd.sessionId, {
8439
+ dimSessionId,
8440
+ workspaceDir,
8441
+ ...cmd.runtimeId ? { runtimeId: cmd.runtimeId } : {},
8442
+ ...cmd.providerKey ? { providerKey: cmd.providerKey } : {},
8443
+ ...configOptions !== void 0 ? { configOptions } : {}
8444
+ }).catch((err) => {
8445
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u6307\u9488\u56DE\u4F20\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8446
+ });
8447
+ const managed = {
8448
+ sessionId: cmd.sessionId,
8449
+ conn,
8450
+ dimSessionId,
8451
+ workspaceDir,
8452
+ env,
8453
+ turnNo: 0,
8454
+ promptInFlight: null,
8455
+ cancelRequested: false,
8456
+ // resume 成功 → dim 会话里已有上一进程注入的管家指令,不重复前置
8457
+ instructed: resumed,
8458
+ appliedConfig: { ...appliedConfig, permission },
8459
+ idleTimer: setTimeout(() => {
8460
+ void this.close({ sessionId: cmd.sessionId }, "idle");
8461
+ }, this.idleMs),
8462
+ lastActiveAt: Date.now()
8463
+ };
8464
+ managed.idleTimer.unref?.();
8465
+ this.sessions.set(cmd.sessionId, managed);
8466
+ 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`);
8467
+ return { ok: true, dimSessionId, workspaceDir };
8468
+ }
8469
+ /**
8470
+ * 送一轮 prompt(同一会话单活跃——ACP 硬约束;忙时返回 SESSION_BUSY 不排队,
8471
+ * 排队语义由服务端消息投递状态机承载,见 §4.2)。
8472
+ * 事件映射复用任务链路同款 mapper + normalizer(含 P0-8 修正:user_message / usage 上下文语义)。
8473
+ *
8474
+ * **管家指令前置**(§4.4):新建 dim 会话的**首条** prompt 前置 `STEWARD_SYSTEM_INSTRUCTIONS`
8475
+ * (ACP 无 system prompt 通道)。resume 的会话里已有,不重复注入。
8476
+ * 该常量只含身份/边界/目标/工作区与工具入口(提示词极简,见 steward-instructions.ts 文件头);
8477
+ * 行为规范与命令用法在 `.agents/skills/steward-management/SKILL.md`,由 agent 按需读取。
8478
+ */
8479
+ async prompt(cmd) {
8480
+ const managed = this.sessions.get(cmd.sessionId);
8481
+ if (!managed)
8482
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8483
+ if (managed.promptInFlight)
8484
+ return { ok: false, error: "SESSION_BUSY" };
8485
+ if (!cmd.prompt?.trim())
8486
+ return { ok: false, error: "SESSION_PROMPT_EMPTY" };
8487
+ managed.turnNo += 1;
8488
+ managed.cancelRequested = false;
8489
+ this.touch(managed);
8490
+ const turnNo = managed.turnNo;
8491
+ const mapper = new AcpEventMapper();
8492
+ let seqHint = 0;
8493
+ const nextSeq = () => ++seqHint;
8494
+ const textStream = new AssistantTextStream({
8495
+ onDelta: (seg, delta) => {
8496
+ void this.options.api.recordSessionStreamChunk(managed.sessionId, {
8497
+ chunkType: "delta",
8498
+ data: { kind: "text", seg, text: delta }
8499
+ }).catch(() => {
8500
+ });
8501
+ },
8502
+ onParagraph: (seg, text2) => this.options.api.recordSessionEvent(managed.sessionId, {
8503
+ type: "agent_message_delta",
8504
+ output: { text: redactValue(text2), seg },
8505
+ turnNo,
8506
+ seqHint: nextSeq()
8507
+ }).then(() => void 0).catch((err) => {
8508
+ this.log(`[session-host] \u4F1A\u8BDD ${managed.sessionId} \u6BB5\u843D\u4E8B\u4EF6\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8509
+ })
8510
+ }, { log: this.log });
8511
+ const thinkingStream = new AssistantTextStream({
8512
+ onDelta: (seg, delta) => {
8513
+ void this.options.api.recordSessionStreamChunk(managed.sessionId, {
8514
+ chunkType: "delta",
8515
+ data: { kind: "thinking", seg, text: delta }
8516
+ }).catch(() => {
8517
+ });
8518
+ },
8519
+ onParagraph: (seg, text2) => this.options.api.recordSessionEvent(managed.sessionId, {
8520
+ type: "agent_thought_delta",
8521
+ output: { text: redactValue(text2), seg },
8522
+ turnNo,
8523
+ seqHint: nextSeq()
8524
+ }).then(() => void 0).catch((err) => {
8525
+ this.log(`[session-host] \u4F1A\u8BDD ${managed.sessionId} \u601D\u8003\u6BB5\u843D\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8526
+ })
8527
+ }, { log: this.log });
8528
+ let textChain = Promise.resolve();
8529
+ let thinkingPending = false;
8530
+ const gateThinking = () => {
8531
+ if (thinkingPending) {
8532
+ thinkingPending = false;
8533
+ textChain = textChain.then(() => thinkingStream.flushParagraph());
8534
+ }
8535
+ return textChain;
8536
+ };
8537
+ const onEvent = (event) => {
8538
+ this.touch(managed);
8539
+ if (event.type === "assistant_thinking_delta") {
8540
+ thinkingPending = true;
8541
+ thinkingStream.push(event.text);
8542
+ } else if (event.type === "assistant_text_delta") {
8543
+ void gateThinking().then(() => textStream.push(event.text));
8544
+ } else if (event.type === "tool_call_started") {
8545
+ void gateThinking().then(() => textStream.flushParagraph()).then(() => this.options.api.recordSessionEvent(managed.sessionId, {
8546
+ type: "tool_call_started",
8547
+ toolName: event.toolName,
8548
+ callId: event.callId,
8549
+ input: event.input !== void 0 ? redactInputSummary(event.input) : void 0,
8550
+ output: void 0,
8551
+ turnNo,
8552
+ seqHint: nextSeq()
8553
+ })).catch(() => {
8554
+ });
8555
+ } else if (event.type === "tool_call_completed") {
8556
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8557
+ type: "tool_call_completed",
8558
+ callId: event.callId,
8559
+ toolName: event.toolName,
8560
+ output: {
8561
+ callId: event.callId,
8562
+ toolName: event.toolName,
8563
+ status: event.aborted ? "aborted" : "completed",
8564
+ ...event.outputSummary !== void 0 ? { summary: String(redactValue(String(event.outputSummary).slice(0, 200))) } : {}
8565
+ },
8566
+ turnNo,
8567
+ seqHint: nextSeq()
8568
+ }).catch(() => {
8569
+ });
8570
+ } else if (event.type === "user_message") {
8571
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8572
+ type: "user_message",
8573
+ output: { text: event.text },
8574
+ turnNo
8575
+ }).catch(() => {
8576
+ });
8577
+ } else if (event.type === "usage") {
8578
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8579
+ type: "usage",
8580
+ output: event.usage,
8581
+ turnNo
8582
+ }).catch(() => {
8583
+ });
8584
+ } else if (event.type === "session" && event.providerSessionId) {
8585
+ void this.options.api.reportSessionBinding(managed.sessionId, { dimSessionId: event.providerSessionId }).catch(() => {
8586
+ });
8587
+ }
8588
+ };
8589
+ const normalizer = createDimEventNormalizer({ onEvent });
8590
+ const updateHandler = (update) => {
8591
+ const { messages } = mapper.map(managed.dimSessionId, update);
8592
+ for (const message of messages)
8593
+ normalizer.handle(message, "");
8594
+ };
8595
+ const text = managed.instructed ? cmd.prompt : `${STEWARD_SYSTEM_INSTRUCTIONS}
8596
+
8597
+ ---
8598
+
8599
+ ${cmd.prompt}`;
8600
+ const run3 = (async () => {
8601
+ try {
8602
+ const { blocks, fileRefs } = await this.materializeAttachments(managed.workspaceDir, cmd.attachments ?? []);
8603
+ const promptBlocks = [{ type: "text", text: fileRefs.length > 0 ? `${text}
8604
+
8605
+ ---
8606
+
8607
+ \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
8608
+ ${fileRefs.join("\n")}` : text }];
8609
+ promptBlocks.push(...blocks);
8610
+ const pending = managed.conn.prompt(managed.dimSessionId, promptBlocks, updateHandler);
8611
+ managed.instructed = true;
8612
+ void this.options.api.recordSessionEvent(managed.sessionId, {
8613
+ type: "turn_started",
8614
+ output: { ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8615
+ turnNo
8616
+ }).catch(() => {
8617
+ });
8618
+ const result = await pending;
8619
+ await gateThinking();
8620
+ await textStream.flushParagraph();
8621
+ normalizer.finish({ eventType: "run:ended", payload: { stopReason: result.stopReason } }, "");
8622
+ await this.options.api.recordSessionEvent(managed.sessionId, {
8623
+ type: "turn_completed",
8624
+ output: { stopReason: result.stopReason, ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8625
+ turnNo
8626
+ }).catch(() => {
8627
+ });
8628
+ return { ok: true, stopReason: result.stopReason };
8629
+ } catch (err) {
8630
+ await gateThinking();
8631
+ await textStream.flushParagraph();
8632
+ const message = err instanceof Error ? err.message : String(err);
8633
+ await this.options.api.recordSessionEvent(managed.sessionId, {
8634
+ type: "turn_failed",
8635
+ output: { error: message, ...cmd.messageId ? { messageId: cmd.messageId } : {} },
8636
+ turnNo
8637
+ }).catch(() => {
8638
+ });
8639
+ return { ok: false, error: message };
8640
+ } finally {
8641
+ textStream.dispose();
8642
+ thinkingStream.dispose();
8643
+ managed.promptInFlight = null;
8644
+ this.touch(managed);
8645
+ }
8646
+ })();
8647
+ managed.promptInFlight = run3;
8648
+ return run3;
8649
+ }
8650
+ /**
8651
+ * 附件物化(§五 P2-3):
8652
+ * - **图片** → ACP `image` content block(base64 + mimeType),直接进模型上下文;
8653
+ * - **其他文件** → 下载到 `<workspace>/attachments/<安全文件名>`,返回路径引用文本
8654
+ * (doc 明确「文件走工作区文件 + 路径引用」:不进上下文,由 agent 按需读取)。
8655
+ *
8656
+ * 下载失败不抛:记日志并跳过该附件(本轮照常发出)。
8657
+ */
8658
+ async materializeAttachments(workspaceDir, attachments) {
8659
+ const blocks = [];
8660
+ const fileRefs = [];
8661
+ if (attachments.length === 0)
8662
+ return { blocks, fileRefs };
8663
+ const dir = join19(workspaceDir, "attachments");
8664
+ for (const attachment of attachments) {
8665
+ try {
8666
+ const res = await fetch(attachment.downloadUrl, { signal: AbortSignal.timeout(ATTACHMENT_FETCH_TIMEOUT_MS) });
8667
+ if (!res.ok)
8668
+ throw new Error(`HTTP ${res.status}`);
8669
+ const bytes = Buffer.from(await res.arrayBuffer());
8670
+ if (attachment.isImage) {
8671
+ blocks.push({ type: "image", data: bytes.toString("base64"), mimeType: attachment.fileType || "image/png" });
8672
+ } else {
8673
+ mkdirSync12(dir, { recursive: true });
8674
+ const target = join19(dir, safeAttachmentName(attachment.fileName));
8675
+ writeFileSync10(target, bytes);
8676
+ fileRefs.push(`- ${attachment.fileName} \u2192 ${target}`);
8677
+ }
8678
+ } catch (err) {
8679
+ 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)}`);
8680
+ }
8681
+ }
8682
+ return { blocks, fileRefs };
8683
+ }
8684
+ /** 取消当前回合(ACP session/cancel;会话保持打开——长驻语义) */
8685
+ cancel(cmd) {
8686
+ const managed = this.sessions.get(cmd.sessionId);
8687
+ if (!managed)
8688
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8689
+ managed.cancelRequested = true;
8690
+ managed.conn.cancel(managed.dimSessionId);
8691
+ this.touch(managed);
8692
+ return { ok: true };
8693
+ }
8694
+ /**
8695
+ * 配置变更(configId ∈ model / thought_level / mode / permission)。
8696
+ * ACP `set_config_option` 全量返回 configOptions——回写由服务端(配置端点)消费,
8697
+ * 这里原样返回给调用方。
8698
+ */
8699
+ async config(cmd) {
8700
+ const managed = this.sessions.get(cmd.sessionId);
8701
+ if (!managed)
8702
+ return { ok: false, error: "SESSION_NOT_OPEN" };
8703
+ this.touch(managed);
8704
+ try {
8705
+ const result = normalizeAcpConfigOptions(await managed.conn.setConfigOptionFull(managed.dimSessionId, cmd.configId, cmd.value)) ?? [];
8706
+ managed.appliedConfig = { ...managed.appliedConfig, [this.configKey(cmd.configId)]: cmd.value };
8707
+ if (result.length > 0) {
8708
+ 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)}`));
8709
+ }
8710
+ return { ok: true, configOptions: result };
8711
+ } catch (err) {
8712
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
8713
+ }
8714
+ }
8715
+ /**
8716
+ * 关闭会话:ACP session/close → 进程回收 → 服务端上报。
8717
+ * **工作区保留**(steward/<tenant>/<project> 可复用;清理属服务端清理命令域)。
8718
+ *
8719
+ * @param reason `idle` = 空闲回收(服务端回到 idle,保留 dimSessionId 以便 resume);
8720
+ * `command` = 用户/平台显式关闭(服务端终态 closed);
8721
+ * `reopen` = daemon 内部重建(**不上报**,紧接着的 open 会重新绑定)。
8722
+ */
8723
+ async close(cmd, reason = "command") {
8724
+ const managed = this.sessions.get(cmd.sessionId);
8725
+ if (!managed)
8726
+ return { ok: true };
8727
+ this.sessions.delete(cmd.sessionId);
8728
+ clearTimeout(managed.idleTimer);
8729
+ try {
8730
+ await managed.conn.close();
8731
+ } catch {
8732
+ }
8733
+ if (reason === "reopen") {
8734
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u8FDE\u63A5\u91CD\u5EFA\uFF08\u5DE5\u4F5C\u533A\u4FDD\u7559 ${managed.workspaceDir}\uFF09`);
8735
+ return { ok: true };
8736
+ }
8737
+ this.options.api.clearSessionToken(cmd.sessionId);
8738
+ void this.options.api.closeAgentSession(cmd.sessionId, reason).catch((err) => {
8739
+ this.log(`[session-host] \u4F1A\u8BDD ${cmd.sessionId} \u5173\u95ED\u4E0A\u62A5\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8740
+ });
8741
+ this.log(`[session-host] \u4F1A\u8BDD\u5DF2\u5173\u95ED ${cmd.sessionId}\uFF08\u539F\u56E0=${reason}\uFF1B\u5DE5\u4F5C\u533A\u4FDD\u7559 ${managed.workspaceDir}\uFF09`);
8742
+ return { ok: true };
8743
+ }
8744
+ /**
8745
+ * 关闭全部会话(daemon 停机时收尾)。
8746
+ * 用 `idle` 而非 `command`:daemon 停机不是用户关闭会话,dim 会话应当可 resume
8747
+ * (否则服务端会把会话打成终态 `closed`,下次上线再也接不回来)。
8748
+ */
8749
+ async closeAll() {
8750
+ for (const sessionId of [...this.sessions.keys()])
8751
+ await this.close({ sessionId }, "idle").catch(() => {
8752
+ });
8753
+ }
8754
+ // ────────────────────────── 内部 ──────────────────────────
8755
+ /** 活跃信号:刷新空闲回收计时(事件/prompt/取消都算活跃)。定时器 unref——回收器不应单独钉住进程(daemon 有 WS/心跳常驻句柄) */
8756
+ touch(managed) {
8757
+ managed.lastActiveAt = Date.now();
8758
+ clearTimeout(managed.idleTimer);
8759
+ managed.idleTimer = setTimeout(() => {
8760
+ void this.close({ sessionId: managed.sessionId }, "idle");
8761
+ }, this.idleMs);
8762
+ managed.idleTimer.unref?.();
8763
+ }
8764
+ /** 工作区注入物(幂等覆盖;daemon 侧组装,P1-6) */
8765
+ writeInjections(workspaceDir, cmd) {
8766
+ writeProjectAgentsMd(workspaceDir, cmd.agentsMd, this.log);
8767
+ const skillDir = join19(workspaceDir, ".agents", "skills", STEWARD_SKILL_NAME);
8768
+ mkdirSync12(skillDir, { recursive: true });
8769
+ writeFileSync10(join19(skillDir, "SKILL.md"), buildStewardSkillFile(), "utf8");
8770
+ const contextDir = join19(workspaceDir, ".agent_context");
8771
+ mkdirSync12(contextDir, { recursive: true });
8772
+ writeFileSync10(
8773
+ join19(contextDir, "steward-context.md"),
8774
+ buildStewardContextMd(cmd.facts ?? { projectId: cmd.projectId, tenantId: cmd.tenantId }),
8775
+ "utf8"
8776
+ );
8777
+ }
8778
+ configFrom(cmd) {
8779
+ return {
8780
+ ...cmd.modelId?.trim() ? { modelId: cmd.modelId.trim() } : {},
8781
+ ...cmd.thoughtLevel?.trim() ? { thoughtLevel: cmd.thoughtLevel.trim() } : {},
8782
+ ...cmd.agentMode?.trim() ? { agentMode: cmd.agentMode.trim() } : {},
8783
+ ...cmd.permission?.trim() ? { permission: cmd.permission.trim() } : {}
8784
+ };
8785
+ }
8786
+ /** 配置差异 → ACP configId 映射(只返回变化项;open 幂等路径用) */
8787
+ diffConfig(applied, next) {
8788
+ const out = {};
8789
+ for (const key of ["modelId", "thoughtLevel", "agentMode", "permission"]) {
8790
+ const value = next[key];
8791
+ if (value && value !== applied[key])
8792
+ out[this.configIdOf(key)] = value;
8793
+ }
8794
+ return out;
8795
+ }
8796
+ /** 本地字段名 → ACP configId(§4.4:model / thought_level / mode / permission) */
8797
+ configIdOf(key) {
8798
+ if (key === "modelId")
8799
+ return "model";
8800
+ if (key === "thoughtLevel")
8801
+ return "thought_level";
8802
+ if (key === "agentMode")
8803
+ return "mode";
8804
+ return key;
8805
+ }
8806
+ /** ACP configId → 本地字段名(config 变更回写用) */
8807
+ configKey(configId) {
8808
+ if (configId === "model")
8809
+ return "modelId";
8810
+ if (configId === "thought_level")
8811
+ return "thoughtLevel";
8812
+ if (configId === "mode")
8813
+ return "agentMode";
8814
+ return configId;
8815
+ }
8816
+ /**
8817
+ * 结构化提问上行(§4.6 ②):登记 → 长轮询切片等待用户应答 → 超时回 cancel。
8818
+ *
8819
+ * 为什么两段式(登记 + 轮询)而不是一个长连接:用户可能几分钟才填完,单个悬挂 HTTP 请求
8820
+ * 会撞上服务端 requestTimeout 与中间层空闲超时;切片等待把「无悬挂长连接」与
8821
+ * 「应答即达」两个目标同时拿到。
8822
+ */
8823
+ async handleElicitation(sessionId, request) {
8824
+ const timeoutMs = resolveElicitationTimeoutMs();
8825
+ let elicitationId;
8826
+ try {
8827
+ const created = await this.options.api.createElicitation(sessionId, {
8828
+ toolCallId: request.toolCallId,
8829
+ message: request.message,
8830
+ requestedSchema: request.requestedSchema
8831
+ });
8832
+ if (!created?.ok || !created.elicitationId) {
8833
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u767B\u8BB0\u5931\u8D25: ${created?.code ?? "unknown"}`);
8834
+ return { action: "decline" };
8835
+ }
8836
+ elicitationId = created.elicitationId;
8837
+ } catch (err) {
8838
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u767B\u8BB0\u5F02\u5E38: ${err instanceof Error ? err.message : String(err)}`);
8839
+ return { action: "decline" };
8840
+ }
8841
+ const deadline = Date.now() + timeoutMs;
8842
+ while (Date.now() < deadline) {
8843
+ const remaining = deadline - Date.now();
8844
+ try {
8845
+ const result = await this.options.api.getElicitation(sessionId, elicitationId, Math.min(ELICITATION_POLL_WAIT_MS, remaining));
8846
+ if (result?.status === "answered")
8847
+ return { action: result.action ?? "accept", content: result.content ?? {} };
8848
+ if (result?.status === "cancelled")
8849
+ return { action: "cancel" };
8850
+ if (result?.status === "expired") {
8851
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u5DF2\u5728\u670D\u52A1\u7AEF\u8FC7\u671F\uFF08${elicitationId}\uFF09`);
8852
+ return { action: "cancel" };
8853
+ }
8854
+ } catch (err) {
8855
+ this.log(`[session-host] \u4F1A\u8BDD ${sessionId} \u7ED3\u6784\u5316\u63D0\u95EE\u67E5\u8BE2\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
8856
+ return { action: "cancel" };
8857
+ }
8858
+ const managed = this.sessions.get(sessionId);
8859
+ if (managed)
8860
+ this.touch(managed);
8861
+ }
8862
+ 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`);
8863
+ return { action: "cancel" };
8864
+ }
8865
+ };
8866
+
7307
8867
  // src/daemon/ws-client.ts
7308
8868
  import { createHash as createHash2 } from "node:crypto";
7309
8869
  import { io } from "socket.io-client";
@@ -7440,6 +9000,12 @@ var DaemonWsClient = class {
7440
9000
  if (event)
7441
9001
  handlers.onTaskMessage(event);
7442
9002
  });
9003
+ for (const kind of ["open", "prompt", "cancel", "config", "close"]) {
9004
+ socket.on(`agent-session:${kind}`, (payload) => {
9005
+ if (payload && typeof payload === "object")
9006
+ handlers.onSessionCommand?.(kind, payload);
9007
+ });
9008
+ }
7443
9009
  socket.on("connect_error", (error) => {
7444
9010
  this.onConnectError(socket, error);
7445
9011
  });
@@ -7622,6 +9188,12 @@ var DaemonRunner = class {
7622
9188
  isolatedDimEnv = {};
7623
9189
  /** ACP 连接池(ACP-only:daemon 启动即建,懒 spawn,首个任务才起 dim acp 进程) */
7624
9190
  acpPool = null;
9191
+ /**
9192
+ * 会话宿主(steward-core-acp-migration §4.4,P0 骨架):长驻会话(管家对话)的执行载体,
9193
+ * 每会话独立 dim acp 进程(不走连接池)。P0 只接骨架(WS 命令转发 + 机器协议上报),
9194
+ * 服务端发射端(agent-session:*)在 P1-5 接入。
9195
+ */
9196
+ sessionHost = null;
7625
9197
  /** LOCAL-AGENT-REPO-READ-001:本地 repo checkout 端点(agent 本轮内读仓库) */
7626
9198
  repoCheckout = null;
7627
9199
  async start() {
@@ -7657,6 +9229,15 @@ var DaemonRunner = class {
7657
9229
  env: this.isolatedDimEnv
7658
9230
  });
7659
9231
  this.log("[daemon] dim executor: acp-v1\uFF08ACP-only\uFF0C\u957F\u9A7B dim acp \u8FDB\u7A0B\uFF09");
9232
+ this.sessionHost = new SessionHost({
9233
+ api: this.api,
9234
+ workdir: cfg2?.workdir ?? process.cwd(),
9235
+ dimPath: this.dimPath,
9236
+ isolatedDimEnv: this.isolatedDimEnv,
9237
+ baseUrl: this.client.baseUrl,
9238
+ log: this.log
9239
+ });
9240
+ this.log(`[daemon] session host: ready\uFF08\u7A7A\u95F2\u56DE\u6536 ${Math.round(resolveSessionIdleMs() / 6e4)}min\uFF0CMORULA_SESSION_IDLE_MS \u8986\u76D6\uFF09`);
7660
9241
  this.repoCheckout = new LocalRepoCheckoutService({
7661
9242
  api: this.api,
7662
9243
  cfg: cfg2 ?? loadDaemonConfig(this.store),
@@ -7675,7 +9256,7 @@ var DaemonRunner = class {
7675
9256
  this.log(`[daemon] dim CLI: ${this.dimPath}`);
7676
9257
  let binaryMtime = "unknown";
7677
9258
  try {
7678
- binaryMtime = statSync3(process.execPath).mtime.toISOString();
9259
+ binaryMtime = statSync4(process.execPath).mtime.toISOString();
7679
9260
  } catch (_) {
7680
9261
  }
7681
9262
  this.log(`[daemon] binary=${process.execPath} mtime=${binaryMtime} v=${BUILD_VERSION2}`);
@@ -7741,6 +9322,11 @@ var DaemonRunner = class {
7741
9322
  });
7742
9323
  this.acpPool = null;
7743
9324
  }
9325
+ if (this.sessionHost) {
9326
+ void this.sessionHost.closeAll().catch(() => {
9327
+ });
9328
+ this.sessionHost = null;
9329
+ }
7744
9330
  this.repoCheckout?.close();
7745
9331
  this.repoCheckout = null;
7746
9332
  }
@@ -8077,9 +9663,68 @@ var DaemonRunner = class {
8077
9663
  // P1:重连后房间已重放 → 立即按 afterSeq 重新补齐(断连期间的帧不会补发,只能拉)
8078
9664
  onTaskResubscribed: (taskId) => {
8079
9665
  void this.catchUpTaskMessages(taskId);
9666
+ },
9667
+ // 会话宿主命令(steward-core-acp-migration §4.4,P1-5):转发给 SessionHost。
9668
+ // 服务端发到 **tenant 房间**(同租户每台 daemon 都收到,WS 无 per-runtime 房间),
9669
+ // 故与 `agent-task:dispatched` 同口径按 `runtimeId` 自过滤。
9670
+ onSessionCommand: (kind, payload) => {
9671
+ void this.handleSessionCommand(kind, payload);
8080
9672
  }
8081
9673
  };
8082
9674
  }
9675
+ /**
9676
+ * 会话宿主命令分发。
9677
+ *
9678
+ * 两道闸:
9679
+ * ① **runtimeId 自过滤**(tenant 广播 → 只有归属设备处理;缺失时不过滤,兼容旧服务端);
9680
+ * ② **按会话串行**:服务端每轮 prompt 前会补发 `open` 做兜底,而 socket.io 只保证「送达有序」,
9681
+ * 不保证「异步处理有序」——`void handleSessionCommand()` 会让 open/prompt 交错执行,
9682
+ * prompt 可能先于 open 完成而拿到 SESSION_NOT_OPEN。故按 sessionId 串成一条 promise 链。
9683
+ */
9684
+ sessionCommandChains = /* @__PURE__ */ new Map();
9685
+ handleSessionCommand(kind, payload) {
9686
+ const runtimeId = typeof payload.runtimeId === "string" ? payload.runtimeId : void 0;
9687
+ if (runtimeId && this.runtimeId && runtimeId !== this.runtimeId)
9688
+ return Promise.resolve();
9689
+ const sessionId = typeof payload.sessionId === "string" ? payload.sessionId : "";
9690
+ if (!sessionId) {
9691
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u7F3A\u5C11 sessionId\uFF08\u5FFD\u7565\uFF09`);
9692
+ return Promise.resolve();
9693
+ }
9694
+ const prev = this.sessionCommandChains.get(sessionId) ?? Promise.resolve();
9695
+ const next = prev.catch(() => {
9696
+ }).then(() => this.runSessionCommand(kind, payload)).catch((err) => {
9697
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
9698
+ });
9699
+ this.sessionCommandChains.set(sessionId, next);
9700
+ void next.finally(() => {
9701
+ if (this.sessionCommandChains.get(sessionId) === next)
9702
+ this.sessionCommandChains.delete(sessionId);
9703
+ });
9704
+ return next;
9705
+ }
9706
+ /** 单条会话命令执行(畸形命令记日志不抛——单条坏命令不该打断 daemon 主循环) */
9707
+ async runSessionCommand(kind, payload) {
9708
+ const host = this.sessionHost;
9709
+ if (!host) {
9710
+ this.log(`[daemon] \u6536\u5230\u4F1A\u8BDD\u547D\u4EE4 ${kind}\uFF0C\u4F46\u4F1A\u8BDD\u5BBF\u4E3B\u672A\u5C31\u7EEA\uFF08\u5FFD\u7565\uFF09`);
9711
+ return;
9712
+ }
9713
+ try {
9714
+ if (kind === "open")
9715
+ await host.open(payload);
9716
+ else if (kind === "prompt")
9717
+ await host.prompt(payload);
9718
+ else if (kind === "cancel")
9719
+ host.cancel(payload);
9720
+ else if (kind === "config")
9721
+ await host.config(payload);
9722
+ else if (kind === "close")
9723
+ await host.close(payload);
9724
+ } catch (err) {
9725
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
9726
+ }
9727
+ }
8083
9728
  /**
8084
9729
  * 派发事件入口(P0-b,issues/agent-task-false-failure-on-daemon-fetch-error.md):
8085
9730
  * 服务端把派发事件发到 **tenant 房间**(daemon-executor.service.ts `sendToTenant`,WS 无 per-runtime
@@ -8306,7 +9951,13 @@ var DaemonRunner = class {
8306
9951
  // 本任务的 AgentTask ID:`morula repo mr create`(服务端建 MR 通道)等机器协议据此定位任务;
8307
9952
  // agent 不需要也不应自己拼(issues/mr-created-outside-bound-project.md)
8308
9953
  ...taskId ? { MORULA_TASK_ID: taskId } : {},
8309
- ...this.client?.baseUrl ? { MORULA_API_URL: this.client.baseUrl } : {}
9954
+ ...this.client?.baseUrl ? { MORULA_API_URL: this.client.baseUrl } : {},
9955
+ // 插件 bin 前置 + 显式 CLI 路径(与会话链路同源):任务里的 agent 也必须用本包物化的 `morula`,
9956
+ // 而不是宿主/桌面应用自带的旧版(旧版不认任务/会话凭证,且缺部分命令旗标 → 契约指令直接失败)。
9957
+ // PATH 只是第一道保险(dim 会重写 agent shell 的 PATH),可靠的是 `MORULA_CLI` 绝对路径。
9958
+ // 见 issues/steward-cli-resolves-stale-bundled-morula.md。
9959
+ PATH: prependPluginBinToPath(process.env.PATH),
9960
+ MORULA_CLI: pluginBinPath()
8310
9961
  };
8311
9962
  let repoDir = cfg.workdir;
8312
9963
  let taskRoot = cfg.workdir;
@@ -8360,12 +10011,12 @@ var DaemonRunner = class {
8360
10011
  });
8361
10012
  if (spaceToken?.token)
8362
10013
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
8363
- repoDir = join18(cfg.workdir, "scratch", taskId);
10014
+ repoDir = join20(cfg.workdir, "scratch", taskId);
8364
10015
  taskRoot = repoDir;
8365
- mkdirSync11(repoDir, { recursive: true });
8366
- const readme = join18(repoDir, "README.md");
10016
+ mkdirSync13(repoDir, { recursive: true });
10017
+ const readme = join20(repoDir, "README.md");
8367
10018
  if (!existsSync15(readme)) {
8368
- writeFileSync9(
10019
+ writeFileSync11(
8369
10020
  readme,
8370
10021
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
8371
10022
 
@@ -8373,7 +10024,8 @@ var DaemonRunner = class {
8373
10024
  \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
10025
 
8375
10026
  ## \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
10027
+ \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
10028
+ \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
10029
  \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
10030
  \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
10031
  `,
@@ -8549,8 +10201,6 @@ var DaemonRunner = class {
8549
10201
  */
8550
10202
  async runOnce(taskId, detail, taskRoot, provider, providerKey, requestedModelId, env, repoBaseUrl, signal) {
8551
10203
  const streamChunks = [];
8552
- let textBuf = "";
8553
- let textTimer = null;
8554
10204
  let thoughtBuf = "";
8555
10205
  let thoughtTimer = null;
8556
10206
  let streamFlushing = false;
@@ -8649,6 +10299,16 @@ var DaemonRunner = class {
8649
10299
  void flushStreamChunks(false);
8650
10300
  }
8651
10301
  };
10302
+ const textStream = new AssistantTextStream({
10303
+ onDelta: (seg, text) => {
10304
+ streamChunks.push({ chunkType: "delta", data: { kind: "text", seg, text } });
10305
+ void flushStreamChunks(true);
10306
+ },
10307
+ onParagraph: (seg, text) => {
10308
+ streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text, seg } });
10309
+ void flushStreamChunks(true);
10310
+ }
10311
+ }, { log: (line) => this.log(`[daemon] ${line}`) });
8652
10312
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
8653
10313
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
8654
10314
  const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync15(resumeWorkDir);
@@ -8695,16 +10355,19 @@ var DaemonRunner = class {
8695
10355
  }
8696
10356
  }
8697
10357
  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
- }
10358
+ void textStream.flushParagraph().then(() => {
10359
+ streamChunks.push({
10360
+ chunkType: "tool",
10361
+ seqHint: nextChunkSeq(),
10362
+ data: {
10363
+ toolName: event.toolName,
10364
+ callId: event.callId,
10365
+ input: event.input !== void 0 ? redactInputSummary(event.input) : void 0
10366
+ }
10367
+ });
10368
+ void flushStreamChunks(true);
10369
+ }).catch(() => {
8706
10370
  });
8707
- void flushStreamChunks(true);
8708
10371
  }
8709
10372
  if (event.type === "tool_call_completed") {
8710
10373
  streamChunks.push({
@@ -8720,17 +10383,7 @@ var DaemonRunner = class {
8720
10383
  void flushStreamChunks(true);
8721
10384
  }
8722
10385
  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
- }
10386
+ textStream.push(event.text);
8734
10387
  }
8735
10388
  if (event.type === "usage") {
8736
10389
  streamChunks.push({ chunkType: "usage", seqHint: nextChunkSeq(), data: event.usage });
@@ -8748,14 +10401,8 @@ var DaemonRunner = class {
8748
10401
  }
8749
10402
  };
8750
10403
  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
- }
10404
+ await textStream.flushParagraph();
10405
+ textStream.dispose();
8759
10406
  if (thoughtTimer) {
8760
10407
  clearTimeout(thoughtTimer);
8761
10408
  thoughtTimer = null;
@@ -8925,7 +10572,7 @@ var DaemonRunner = class {
8925
10572
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
8926
10573
  return;
8927
10574
  }
8928
- const managedRoot = join18(cfg.workdir, "worktrees", this.tenantId);
10575
+ const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
8929
10576
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
8930
10577
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
8931
10578
  ok: false,
@@ -8973,38 +10620,38 @@ function isDebugLogEnabled() {
8973
10620
  var spaceTokenFailureLogged = false;
8974
10621
  function writeCustomSkills(targetDir, skills, log) {
8975
10622
  const NAME_RE = /^[\w-]{1,64}$/;
8976
- const base = join18(targetDir, ".agents", "skills");
10623
+ const base = join20(targetDir, ".agents", "skills");
8977
10624
  for (const skill of skills) {
8978
10625
  const name = skill.name.trim();
8979
10626
  if (!NAME_RE.test(name)) {
8980
10627
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
8981
10628
  continue;
8982
10629
  }
8983
- const dir = join18(base, name);
8984
- if (dirname7(dir) !== base) {
10630
+ const dir = join20(base, name);
10631
+ if (dirname8(dir) !== base) {
8985
10632
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
8986
10633
  continue;
8987
10634
  }
8988
- mkdirSync11(dir, { recursive: true });
10635
+ mkdirSync13(dir, { recursive: true });
8989
10636
  const meta = ["---", `name: ${name}`];
8990
10637
  if (skill.description?.trim()) {
8991
10638
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
8992
10639
  }
8993
10640
  meta.push("---", "");
8994
- writeFileSync9(join18(dir, "SKILL.md"), `${meta.join("\n")}
10641
+ writeFileSync11(join20(dir, "SKILL.md"), `${meta.join("\n")}
8995
10642
  ${skill.content ?? ""}`, "utf8");
8996
10643
  }
8997
10644
  }
8998
10645
  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 });
10646
+ const dir = join20(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
10647
+ const refsDir = join20(dir, "references");
10648
+ mkdirSync13(refsDir, { recursive: true });
9002
10649
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
9003
10650
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
9004
10651
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
9005
- writeFileSync9(join18(dir, "SKILL.md"), `${front}
10652
+ writeFileSync11(join20(dir, "SKILL.md"), `${front}
9006
10653
  ${body}`, "utf8");
9007
- writeFileSync9(join18(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
10654
+ writeFileSync11(join20(refsDir, "working-on-issues-source-map.md"), COLLABORATION_SKILL_SOURCE_MAP, "utf8");
9008
10655
  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
10656
  }
9010
10657
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -9035,8 +10682,8 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
9035
10682
  };
9036
10683
  }
9037
10684
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
9038
- const briefDir = join18(targetDir, ".agent_context");
9039
- mkdirSync11(briefDir, { recursive: true });
10685
+ const briefDir = join20(targetDir, ".agent_context");
10686
+ mkdirSync13(briefDir, { recursive: true });
9040
10687
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
9041
10688
 
9042
10689
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
@@ -9045,7 +10692,7 @@ function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
9045
10692
  ---
9046
10693
 
9047
10694
  ${buildTaskPromptWithContract(detail, void 0)}`;
9048
- writeFileSync9(join18(briefDir, "task-context.md"), brief, "utf8");
10695
+ writeFileSync11(join20(briefDir, "task-context.md"), brief, "utf8");
9049
10696
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
9050
10697
  }
9051
10698
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -9059,8 +10706,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
9059
10706
  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
10707
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
9061
10708
  }
9062
- mkdirSync11(targetDir, { recursive: true });
9063
- writeFileSync9(join18(targetDir, "AGENTS.md"), `${content}
10709
+ mkdirSync13(targetDir, { recursive: true });
10710
+ writeFileSync11(join20(targetDir, "AGENTS.md"), `${content}
9064
10711
  `, "utf8");
9065
10712
  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
10713
  return true;
@@ -9080,52 +10727,20 @@ function resolveRepo(detail) {
9080
10727
  }
9081
10728
  function buildTaskPromptWithContract(detail, repoBaseUrl) {
9082
10729
  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
10730
+ void repoBaseUrl;
10731
+ const snap = detail.contextSnapshot;
10732
+ const spaceId = typeof snap?.spaceId === "string" && snap.spaceId.trim() ? snap.spaceId.trim() : "";
10733
+ const keysHint = `## \u57FA\u672C\u4FE1\u606F
9120
10734
 
9121
- `;
9122
- }
9123
- }
10735
+ - \u4EFB\u52A1 ID\uFF08AgentTask\uFF0C\`morula task branch\` \u7B49\u547D\u4EE4\u7684 taskId \u7528\u5B83\uFF09\uFF1A\`${detail.id}\`
10736
+ ${spaceId ? `- \u7A7A\u95F4 ID\uFF08\u6240\u6709 \`--space\` \u7528\u5B83\uFF09\uFF1A\`${spaceId}\`
10737
+ ` : ""}`;
9124
10738
  const toolsHint = `## \u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177
9125
10739
 
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
10740
+ - \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
10741
  - \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
10742
+ - \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
10743
+ - \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
10744
 
9130
10745
  `;
9131
10746
  const taskSource = detail.contextSnapshot?.source;
@@ -9133,19 +10748,12 @@ ${multiRepoList}
9133
10748
  const stageContractBlock = stageContract ? `${stageContract}
9134
10749
 
9135
10750
  ` : "";
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()}**
10751
+ const handoff = typeof snap?.handoffNote === "string" ? snap.handoffNote.trim() : "";
10752
+ 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
10753
 
9146
- `;
9147
- }
9148
- const head = `${stageContractBlock}${toolsHint}${repoHint}${extraHint}`;
10754
+ ` : "";
10755
+ const head = `${stageContractBlock}${keysHint}
10756
+ ${toolsHint}${extraHint}`;
9149
10757
  return head ? `${head}========== \u4F60\u7684\u76EE\u6807 ==========
9150
10758
  ${prompt}` : `${prompt}`;
9151
10759
  }
@@ -9158,7 +10766,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
9158
10766
  }
9159
10767
  async function reportOneRepoTarget(taskId, target, env, ops) {
9160
10768
  const execEnv = mergeExecEnv(env);
9161
- if (!existsSync15(target.dir) || !existsSync15(join18(target.dir, ".git"))) {
10769
+ if (!existsSync15(target.dir) || !existsSync15(join20(target.dir, ".git"))) {
9162
10770
  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
10771
  return;
9164
10772
  }
@@ -9327,8 +10935,8 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
9327
10935
  // src/commands/doctor.ts
9328
10936
  init_dist2();
9329
10937
  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";
10938
+ import { homedir as homedir12 } from "node:os";
10939
+ import { dirname as dirname9, join as join21 } from "node:path";
9332
10940
  init_daemon_config();
9333
10941
  init_daemon_lock();
9334
10942
  init_dim_locate();
@@ -9348,10 +10956,10 @@ async function doctorCommand(context) {
9348
10956
  async function collectDoctor(context, probe) {
9349
10957
  const checks = [];
9350
10958
  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");
10959
+ const configDir = env.MORULA_CONFIG_DIR ?? join21(homedir12(), ".morula-cli");
10960
+ const configPath = env.MORULA_CONFIG ?? join21(configDir, "config.yaml");
10961
+ const daemonPath = join21(dirname9(configPath), "daemon");
10962
+ const logFile = join21(daemonPath, "daemon.log");
9355
10963
  const contextName = context.runtime.context ?? null;
9356
10964
  const baseUrl = context.runtime.baseUrl ?? null;
9357
10965
  const tokenPresent = Boolean(context.runtime.token);
@@ -9365,12 +10973,11 @@ async function collectDoctor(context, probe) {
9365
10973
  configReadable = false;
9366
10974
  }
9367
10975
  }
9368
- const lockPath = join19(daemonPath, DAEMON_LOCK_FILE);
10976
+ const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
9369
10977
  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;
10978
+ const pidFile = join21(daemonPath, DAEMON_PID_FILE);
10979
+ const filePid = readPidFile(pidFile);
10980
+ const daemonPid = probeDaemonInstance(configDir).pid;
9374
10981
  const cfg = loadDaemonConfig(context.store);
9375
10982
  const dim = safeResolveDim();
9376
10983
  const dimPath = dim.path;
@@ -9538,12 +11145,12 @@ function detectInstallMode() {
9538
11145
 
9539
11146
  // src/commands/daemon.ts
9540
11147
  function configFilePath(env) {
9541
- return env.MORULA_CONFIG ?? join20(env.MORULA_CONFIG_DIR ?? join20(homedir12(), ".morula-cli"), "config.yaml");
11148
+ return env.MORULA_CONFIG ?? join22(env.MORULA_CONFIG_DIR ?? join22(homedir13(), ".morula-cli"), "config.yaml");
9542
11149
  }
9543
11150
  function daemonDir(configPath) {
9544
- return join20(dirname9(configPath), "daemon");
11151
+ return join22(dirname10(configPath), "daemon");
9545
11152
  }
9546
- var PID_FILE = "daemon.pid";
11153
+ var PID_FILE = DAEMON_PID_FILE;
9547
11154
  var LOG_FILE = "daemon.log";
9548
11155
  async function daemonCommand(context) {
9549
11156
  const { action } = context.parsed;
@@ -9735,8 +11342,8 @@ async function run2(context) {
9735
11342
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
9736
11343
  }
9737
11344
  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);
11345
+ mkdirSync14(resolvedWorkdir, { recursive: true });
11346
+ const lockPath = join22(daemonDir(configFilePath(context.env)), DAEMON_LOCK_FILE);
9740
11347
  const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
9741
11348
  if (!acquired.ok) {
9742
11349
  const pid = acquired.existing?.pid;
@@ -9810,10 +11417,10 @@ async function start(context) {
9810
11417
  }
9811
11418
  const dir = daemonDir(configFilePath(context.env));
9812
11419
  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);
11420
+ mkdirSync14(dir, { recursive: true, mode: 448 });
11421
+ const pidFile = join22(dir, PID_FILE);
11422
+ const logFile = join22(dir, LOG_FILE);
11423
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
9817
11424
  const lock = probeDaemonLock(lockPath);
9818
11425
  if (lock.state === "active") {
9819
11426
  throw new CliError(
@@ -9832,7 +11439,7 @@ async function start(context) {
9832
11439
  });
9833
11440
  }
9834
11441
  if (!dryRun)
9835
- rmSync8(pidFile, { force: true });
11442
+ rmSync10(pidFile, { force: true });
9836
11443
  }
9837
11444
  const env = {
9838
11445
  ...process.env,
@@ -9870,12 +11477,12 @@ async function start(context) {
9870
11477
  const { spawnDaemonProcess: spawnDaemonProcess2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
9871
11478
  const result = spawnDaemonProcess2({ logFile, env });
9872
11479
  if (result.error || result.pid <= 0) {
9873
- rmSync8(pidFile, { force: true });
11480
+ rmSync10(pidFile, { force: true });
9874
11481
  throw new CliError("DAEMON_START_FAILED", EXIT.OTHER, `[daemon] \u542F\u52A8\u5931\u8D25: ${result.error ?? `pid=${result.pid}`}`, false, {
9875
11482
  hint: `tail -n 50 ${logFile}`
9876
11483
  });
9877
11484
  }
9878
- writeFileSync10(pidFile, String(result.pid), { mode: 384 });
11485
+ writeFileSync12(pidFile, String(result.pid), { mode: 384 });
9879
11486
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
9880
11487
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
9881
11488
  }
@@ -9883,8 +11490,8 @@ async function stop(context) {
9883
11490
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
9884
11491
  const dryRun = isDryRun(context);
9885
11492
  const dir = daemonDir(configFilePath(context.env));
9886
- const pidFile = join20(dir, PID_FILE);
9887
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
11493
+ const pidFile = join22(dir, PID_FILE);
11494
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
9888
11495
  const lock = probeDaemonLock(lockPath);
9889
11496
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
9890
11497
  const filePid = existsSync17(pidFile) ? Number(readFileSync11(pidFile, "utf8").trim()) : null;
@@ -9897,7 +11504,7 @@ async function stop(context) {
9897
11504
  };
9898
11505
  }
9899
11506
  if (!pid) {
9900
- rmSync8(pidFile, { force: true });
11507
+ rmSync10(pidFile, { force: true });
9901
11508
  releaseDaemonLock(lockPath);
9902
11509
  return { running: false, message: "daemon \u672A\u5728\u8FD0\u884C\uFF08\u65E0\u5B58\u6D3B pid\uFF09" };
9903
11510
  }
@@ -9906,7 +11513,7 @@ async function stop(context) {
9906
11513
  releaseDaemonLock(lockPath);
9907
11514
  else
9908
11515
  releaseDaemonLock(lockPath, pid);
9909
- rmSync8(pidFile, { force: true });
11516
+ rmSync10(pidFile, { force: true });
9910
11517
  if (result.outcome === "killed")
9911
11518
  context.err(`[daemon] pid=${pid} \u672A\u54CD\u5E94 SIGTERM\uFF0C\u5DF2 SIGKILL\uFF08\u9501\u5DF2\u91CA\u653E\uFF09`);
9912
11519
  return { running: false, stoppedPid: pid, outcome: result.outcome };
@@ -9915,21 +11522,17 @@ async function status2(context) {
9915
11522
  rejectUnknownFlags(context.parsed.flags, []);
9916
11523
  const cfg = loadDaemonConfig(context.store);
9917
11524
  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);
11525
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
11526
+ const probe = probeDaemonInstance(dirname10(configFilePath(context.env)));
9924
11527
  return {
9925
- running: pid !== null,
9926
- pid,
11528
+ running: probe.running,
11529
+ pid: probe.pid,
9927
11530
  daemonId: cfg?.daemonId ?? null,
9928
11531
  runtimeId: cfg?.runtimeId ?? null,
9929
11532
  name: cfg?.name ?? null,
9930
11533
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
9931
11534
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
9932
- logFile: join20(dir, LOG_FILE),
11535
+ logFile: join22(dir, LOG_FILE),
9933
11536
  lockFile: lockPath
9934
11537
  };
9935
11538
  }
@@ -9957,95 +11560,6 @@ function parseMaxConcurrent(context) {
9957
11560
 
9958
11561
  // src/commands/setup.ts
9959
11562
  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
11563
  var KNOWN_FLAGS = ["root", "help"];
10050
11564
  async function setupCommand(context) {
10051
11565
  rejectUnknownFlags(context.parsed.flags, KNOWN_FLAGS);