@arcships/morula-runtime 0.1.0-alpha.6 → 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 = "20260917063948.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.6";
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(", ")}`);
@@ -4605,24 +5281,49 @@ function buildGitlabSshRemote(repoFullName, baseUrl) {
4605
5281
  return `ssh://git@${host}:${url.port}/${repoFullName}.git`;
4606
5282
  return `git@${host}:${repoFullName}.git`;
4607
5283
  }
4608
- function ensureRepo(workdir, tenantId, repo, env, remoteUrl) {
5284
+ function buildDaemonRepoUrl(repoFullName, baseUrl) {
5285
+ if (!baseUrl)
5286
+ return `https://github.com/${repoFullName}.git`;
5287
+ const url = new URL(baseUrl);
5288
+ const path = repoFullName.replace(/^\/+/, "");
5289
+ return `${url.protocol}//${url.host}/${path}.git`;
5290
+ }
5291
+ function ensureRepo(workdir, tenantId, repo, env, remoteUrl, daemonRemoteUrl) {
4609
5292
  const repoDir = join11(workdir, tenantId, repo);
4610
- if (existsSync8(join11(repoDir, ".git"))) {
5293
+ const fetchUrl = daemonRemoteUrl ?? remoteUrl ?? buildDaemonRepoUrl(repo);
5294
+ if (existsSync7(join11(repoDir, ".git"))) {
4611
5295
  if (remoteUrl)
4612
5296
  syncRemoteUrl(repoDir, remoteUrl, env);
4613
5297
  try {
4614
- execFileSync6("git", ["-C", repoDir, "fetch", "origin", "--prune"], { env, stdio: "pipe" });
4615
- } catch {
5298
+ if (daemonRemoteUrl) {
5299
+ execFileSync6(
5300
+ "git",
5301
+ ["-C", repoDir, "fetch", "--prune", fetchUrl, "+refs/heads/*:refs/remotes/origin/*"],
5302
+ { env, stdio: "pipe" }
5303
+ );
5304
+ } else {
5305
+ execFileSync6("git", ["-C", repoDir, "fetch", "origin", "--prune"], { env, stdio: "pipe" });
5306
+ }
5307
+ } catch (err) {
5308
+ throw new Error(`REPO_SYNC_FAILED: \u62C9\u53D6\u4ED3\u5E93\u5931\u8D25 ${repo}: ${gitErrorText(err)}`);
4616
5309
  }
4617
5310
  return repoDir;
4618
5311
  }
4619
5312
  mkdirSync6(repoDir, { recursive: true });
4620
- execFileSync6("git", ["clone", "--filter=blob:none", remoteUrl ?? `https://github.com/${repo}`, repoDir], {
4621
- env,
4622
- stdio: "pipe"
4623
- });
5313
+ try {
5314
+ execFileSync6("git", ["clone", "--filter=blob:none", fetchUrl, repoDir], { env, stdio: "pipe" });
5315
+ } catch (err) {
5316
+ throw new Error(`REPO_SYNC_FAILED: \u514B\u9686\u4ED3\u5E93\u5931\u8D25 ${repo}: ${gitErrorText(err)}`);
5317
+ }
5318
+ if (remoteUrl && remoteUrl !== fetchUrl)
5319
+ syncRemoteUrl(repoDir, remoteUrl, env);
4624
5320
  return repoDir;
4625
5321
  }
5322
+ function gitErrorText(err) {
5323
+ const e = err;
5324
+ const stderr = e?.stderr ? e.stderr.toString().trim() : "";
5325
+ return stderr || e?.message || String(err);
5326
+ }
4626
5327
  function syncRemoteUrl(repoDir, remoteUrl, env) {
4627
5328
  try {
4628
5329
  const cur = execFileSync6("git", ["-C", repoDir, "remote", "get-url", "origin"], { env, stdio: "pipe" }).toString().trim();
@@ -4635,7 +5336,7 @@ function syncRemoteUrl(repoDir, remoteUrl, env) {
4635
5336
  function isLinkedWorktree(repoDir) {
4636
5337
  try {
4637
5338
  const gitDir = execFileSync6("git", ["-C", repoDir, "rev-parse", "--absolute-git-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
4638
- return !!gitDir && existsSync8(join11(gitDir, "commondir"));
5339
+ return !!gitDir && existsSync7(join11(gitDir, "commondir"));
4639
5340
  } catch {
4640
5341
  return false;
4641
5342
  }
@@ -4673,7 +5374,7 @@ function ensureGitIdentity(repoDir, personaName) {
4673
5374
  }
4674
5375
 
4675
5376
  // src/daemon/outbox.ts
4676
- 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";
4677
5378
  import { homedir as homedir9 } from "node:os";
4678
5379
  import { join as join12 } from "node:path";
4679
5380
  var OUTBOX_RETRY_SCHEDULE = [1e3, 4e3, 16e3, 6e4, 3e5];
@@ -4689,14 +5390,14 @@ function writeOutbox(entry, configDir) {
4689
5390
  }
4690
5391
  function readOutboxEntries(configDir) {
4691
5392
  const dir = outboxDir(configDir);
4692
- if (!existsSync9(dir))
5393
+ if (!existsSync8(dir))
4693
5394
  return [];
4694
5395
  const entries = [];
4695
5396
  for (const file of readdirSync2(dir)) {
4696
5397
  if (!file.endsWith(".json"))
4697
5398
  continue;
4698
5399
  try {
4699
- const raw = readFileSync6(join12(dir, file), "utf8");
5400
+ const raw = readFileSync5(join12(dir, file), "utf8");
4700
5401
  const parsed = JSON.parse(raw);
4701
5402
  if (parsed && typeof parsed.taskId === "string")
4702
5403
  entries.push(parsed);
@@ -4706,7 +5407,7 @@ function readOutboxEntries(configDir) {
4706
5407
  return entries;
4707
5408
  }
4708
5409
  function removeOutbox(taskId, configDir) {
4709
- rmSync6(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
5410
+ rmSync7(join12(outboxDir(configDir), `${taskId}.json`), { force: true });
4710
5411
  }
4711
5412
  function staleDir(configDir) {
4712
5413
  return join12(outboxDir(configDir), "stale");
@@ -4729,13 +5430,113 @@ function outboxAge(entry) {
4729
5430
  return Number.isFinite(created) ? Date.now() - created : Number.POSITIVE_INFINITY;
4730
5431
  }
4731
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
+
4732
5533
  // src/daemon/acp/acp-event-mapper.ts
4733
5534
  var AcpEventMapper = class {
4734
5535
  lastUsage = void 0;
4735
5536
  reset() {
4736
5537
  this.lastUsage = void 0;
4737
5538
  }
4738
- /** 取当前 usage(run:ended 附着用) */
5539
+ /** 取当前 usage(回放/观测用;**不再**作为 tokenUsage 附着——token 总量以 prompt 响应的 usage 为准) */
4739
5540
  get latestUsage() {
4740
5541
  return this.lastUsage;
4741
5542
  }
@@ -4761,6 +5562,13 @@ var AcpEventMapper = class {
4761
5562
  }
4762
5563
  break;
4763
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
+ }
4764
5572
  case "tool_call": {
4765
5573
  messages.push({
4766
5574
  eventType: "tool:started",
@@ -4788,11 +5596,13 @@ var AcpEventMapper = class {
4788
5596
  break;
4789
5597
  }
4790
5598
  case "usage_update": {
4791
- this.lastUsage = {
4792
- inputTokens: void 0,
4793
- outputTokens: void 0,
4794
- 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
4795
5603
  };
5604
+ this.lastUsage = usage2;
5605
+ messages.push({ eventType: "usage:context", payload: { usage: usage2 } });
4796
5606
  break;
4797
5607
  }
4798
5608
  default:
@@ -4888,14 +5698,9 @@ function normalizeOptionId(decision) {
4888
5698
  }
4889
5699
 
4890
5700
  // src/daemon/acp/dim-acp-executor.ts
4891
- var DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1e3;
4892
- var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
4893
- var DEFAULT_TOOL_IDLE_TIMEOUT_MS = 20 * 60 * 1e3;
4894
- var DEFAULT_MAX_TURNS = 8;
4895
- var DEFAULT_TASK_WALL_CLOCK_MS = 2 * 60 * 60 * 1e3;
4896
5701
  var DEFAULT_STEER_SAFETY_MS = 30 * 1e3;
4897
5702
  var PROMPT_RPC_TIMEOUT_GRACE_MS = 1e4;
4898
- var WATCHDOG_TICK_MS = 15e3;
5703
+ var STEER_POLL_TICK_MS = 15e3;
4899
5704
  var KILL_GRACE_MS = 3e3;
4900
5705
  function resolveSteerSafetyMs(raw = process.env.MORULA_STEER_SAFETY_MS) {
4901
5706
  if (raw === void 0 || raw === "")
@@ -5038,7 +5843,7 @@ var DimAcpExecutor = class {
5038
5843
  /** 每次 run 前由调用方注入(daemon 单例池) */
5039
5844
  pool = null;
5040
5845
  async run(opts) {
5041
- const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
5846
+ const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
5042
5847
  const mapper = new AcpEventMapper();
5043
5848
  const pool = opts.pool ?? this.pool;
5044
5849
  return await new Promise((resolve2) => {
@@ -5051,9 +5856,7 @@ var DimAcpExecutor = class {
5051
5856
  let turnNo = 1;
5052
5857
  const inFlightToolCalls = /* @__PURE__ */ new Map();
5053
5858
  const inFlightTools = () => inFlightToolCalls.size;
5054
- let lastActivityAt = Date.now();
5055
- let watchdogTimer = null;
5056
- let watchdogFired = false;
5859
+ let steerPollTimer = null;
5057
5860
  let resumeFailed;
5058
5861
  const killTree = (c) => {
5059
5862
  if (!c)
@@ -5064,10 +5867,10 @@ var DimAcpExecutor = class {
5064
5867
  c.kill();
5065
5868
  }
5066
5869
  };
5067
- const stopWatchdog = () => {
5068
- if (watchdogTimer) {
5069
- clearInterval(watchdogTimer);
5070
- watchdogTimer = null;
5870
+ const stopSteerPoll = () => {
5871
+ if (steerPollTimer) {
5872
+ clearInterval(steerPollTimer);
5873
+ steerPollTimer = null;
5071
5874
  }
5072
5875
  };
5073
5876
  let abortHandler = () => {
@@ -5078,7 +5881,7 @@ var DimAcpExecutor = class {
5078
5881
  settled = true;
5079
5882
  if (timer)
5080
5883
  clearTimeout(timer);
5081
- stopWatchdog();
5884
+ stopSteerPoll();
5082
5885
  if (ownsConnection && conn) {
5083
5886
  void conn.close().catch(() => {
5084
5887
  });
@@ -5103,45 +5906,17 @@ var DimAcpExecutor = class {
5103
5906
  finish({ ok: false, error: "\u4EFB\u52A1\u88AB\u53D6\u6D88", sessionId: sessionId || void 0 });
5104
5907
  }
5105
5908
  };
5106
- const startWatchdog = () => {
5107
- const idle = Number(process.env.MORULA_DIM_IDLE_TIMEOUT_MS) || DEFAULT_IDLE_TIMEOUT_MS;
5108
- if (idle <= 0)
5109
- return;
5110
- lastActivityAt = Date.now();
5111
- inFlightToolCalls.clear();
5112
- watchdogTimer = setInterval(() => {
5909
+ const startSteerPoll = () => {
5910
+ steerPollTimer = setInterval(() => {
5113
5911
  maybeInterruptAtSafePoint();
5114
- enforceTaskWallClock();
5115
- const toolBudget = Number(process.env.MORULA_DIM_TOOL_IDLE_TIMEOUT_MS) || DEFAULT_TOOL_IDLE_TIMEOUT_MS;
5116
- const budget = inFlightTools() > 0 ? toolBudget : idle;
5117
- if (Date.now() - lastActivityAt > budget) {
5118
- watchdogFired = true;
5119
- if (sessionId && conn)
5120
- conn.cancel(sessionId);
5121
- killTree(conn);
5122
- finish({
5123
- ok: false,
5124
- error: `daemon: idle watchdog fired\uFF08${Math.round(budget / 1e3)}s \u65E0\u6D3B\u52A8\uFF09`,
5125
- sessionId: sessionId || void 0,
5126
- turns: turnNo
5127
- });
5128
- }
5129
- }, WATCHDOG_TICK_MS);
5130
- watchdogTimer.unref?.();
5131
- };
5132
- const markActivity = () => {
5133
- lastActivityAt = Date.now();
5912
+ }, STEER_POLL_TICK_MS);
5913
+ steerPollTimer.unref?.();
5134
5914
  };
5135
5915
  let activeSteer = null;
5136
5916
  let steerWaitingSince = 0;
5137
- let wallClockFired = false;
5138
5917
  let interruptedTurns = 0;
5139
5918
  let turnKind = "initial";
5140
5919
  const steerSafetyMs = resolveSteerSafetyMs();
5141
- const taskStartedAt = Date.now();
5142
- const maxTurns = Math.max(1, Math.trunc(opts.steer?.maxTurns ?? DEFAULT_MAX_TURNS));
5143
- const taskWallClockMs = opts.steer?.wallClockMs ?? 0;
5144
- const wallClockExceeded = () => taskWallClockMs > 0 && Date.now() - taskStartedAt > taskWallClockMs;
5145
5920
  function maybeInterruptAtSafePoint() {
5146
5921
  if (!opts.steer || activeSteer || !conn || !sessionId)
5147
5922
  return;
@@ -5165,15 +5940,6 @@ var DimAcpExecutor = class {
5165
5940
  `);
5166
5941
  conn.cancel(sessionId);
5167
5942
  }
5168
- function enforceTaskWallClock() {
5169
- if (wallClockFired || !wallClockExceeded())
5170
- return;
5171
- wallClockFired = true;
5172
- 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
5173
- `);
5174
- if (sessionId && conn)
5175
- conn.cancel(sessionId);
5176
- }
5177
5943
  function emitAbortedTools() {
5178
5944
  const aborted = [];
5179
5945
  for (const [callId, toolName] of inFlightToolCalls) {
@@ -5212,8 +5978,12 @@ var DimAcpExecutor = class {
5212
5978
  });
5213
5979
  setup().then(async () => {
5214
5980
  const armTurnTimer = () => {
5215
- if (timer)
5981
+ if (timer) {
5216
5982
  clearTimeout(timer);
5983
+ timer = void 0;
5984
+ }
5985
+ if (timeout <= 0)
5986
+ return;
5217
5987
  timer = setTimeout(() => {
5218
5988
  if (sessionId && conn)
5219
5989
  conn.cancel(sessionId);
@@ -5229,7 +5999,7 @@ var DimAcpExecutor = class {
5229
5999
  };
5230
6000
  armTurnTimer();
5231
6001
  opts.signal?.addEventListener("abort", abortHandler, { once: true });
5232
- startWatchdog();
6002
+ startSteerPoll();
5233
6003
  const bridge = new AcpPermissionBridge();
5234
6004
  conn.setPermissionHandler(bridge.createHandler(opts.approval, opts.approvalMode === "manual"));
5235
6005
  const sessionParams = { cwd: opts.cwd, mcpServers: [] };
@@ -5279,7 +6049,7 @@ var DimAcpExecutor = class {
5279
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
5280
6050
  `);
5281
6051
  } else {
5282
- await conn.setModel(sess.sessionId, modelId).catch((err) => {
6052
+ await conn.setConfigOption(sess.sessionId, "model", modelId).catch((err) => {
5283
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}
5284
6054
  `);
5285
6055
  });
@@ -5289,7 +6059,6 @@ var DimAcpExecutor = class {
5289
6059
  });
5290
6060
  sessionId = sess.sessionId;
5291
6061
  pool?.touch();
5292
- markActivity();
5293
6062
  opts.onEvent?.(mapper.readyMessage(sessionId), "");
5294
6063
  const reopenSessionAfterInterrupt = async () => {
5295
6064
  try {
@@ -5320,7 +6089,6 @@ var DimAcpExecutor = class {
5320
6089
  activeSteer = null;
5321
6090
  opts.steer?.onTurnStarted?.({ turnNo, kind: turnKind });
5322
6091
  const result = await conn.prompt(sessionId, promptText, (update) => {
5323
- markActivity();
5324
6092
  pool?.touch();
5325
6093
  const { messages } = mapper.map(sessionId, update);
5326
6094
  for (const msg of messages) {
@@ -5353,37 +6121,18 @@ var DimAcpExecutor = class {
5353
6121
  }
5354
6122
  }
5355
6123
  maybeInterruptAtSafePoint();
5356
- }, timeout + PROMPT_RPC_TIMEOUT_GRACE_MS);
6124
+ }, timeout > 0 ? timeout + PROMPT_RPC_TIMEOUT_GRACE_MS : 0);
5357
6125
  if (settled)
5358
6126
  return;
5359
6127
  pool?.touch();
5360
- markActivity();
5361
- const usage2 = result.usage ?? mapper.latestUsage;
6128
+ const usage2 = result.usage ?? void 0;
5362
6129
  const cancelled = result.stopReason === "cancelled";
5363
6130
  const base = { sessionId, turns: turnNo, interrupted: interruptedTurns > 0 };
5364
- if (wallClockFired || wallClockExceeded()) {
5365
- const abortedTools = emitAbortedTools();
5366
- opts.steer?.onTurnInterrupted?.({ turnNo, reason: "wall-clock", abortedTools });
5367
- killTree(conn);
5368
- finish({
5369
- ...base,
5370
- ok: false,
5371
- 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`
5372
- });
5373
- return;
5374
- }
5375
6131
  if (cancelled && activeSteer) {
5376
6132
  interruptedTurns += 1;
5377
6133
  const abortedTools = emitAbortedTools();
5378
6134
  const nextTurn = turnNo + 1;
5379
- const withinBudget = nextTurn <= maxTurns;
5380
- opts.steer?.onTurnInterrupted?.({ turnNo, reason: withinBudget ? "steer" : "max-turns", abortedTools });
5381
- if (!withinBudget) {
5382
- process.stderr.write(`[daemon] \u5DF2\u8FBE\u6700\u5927\u56DE\u5408\u6570 ${maxTurns}\uFF0C\u5269\u4F59\u6253\u65AD\u8BF7\u6C42\u4EA4\u7531\u4E0B\u4E00\u8F6E
5383
- `);
5384
- finish({ ...base, ok: true, text: finalText, tokenUsage: usage2 });
5385
- return;
5386
- }
6135
+ opts.steer?.onTurnInterrupted?.({ turnNo, reason: "steer", abortedTools });
5387
6136
  turnKind = "after-interrupt";
5388
6137
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
5389
6138
  turnNo = nextTurn;
@@ -5406,7 +6155,7 @@ var DimAcpExecutor = class {
5406
6155
  return;
5407
6156
  }
5408
6157
  const leftover = activeSteer ?? opts.steer?.peek() ?? null;
5409
- if (leftover && turnNo + 1 <= maxTurns && !wallClockExceeded()) {
6158
+ if (leftover) {
5410
6159
  const nextTurn = turnNo + 1;
5411
6160
  turnKind = "appended";
5412
6161
  promptText = opts.steer.consume({ turnNo: nextTurn, sessionReset });
@@ -5507,7 +6256,15 @@ function createDimEventNormalizer(opts) {
5507
6256
  let currentMessageId = null;
5508
6257
  let currentText = "";
5509
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
+ };
5510
6266
  const startMessage = (messageId) => {
6267
+ flushUserMessage();
5511
6268
  if (currentMessageId !== null && currentText.trim().length > 0) {
5512
6269
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5513
6270
  }
@@ -5520,7 +6277,22 @@ function createDimEventNormalizer(opts) {
5520
6277
  const eventType = msg.eventType;
5521
6278
  if (!eventType)
5522
6279
  return;
6280
+ if (eventType !== "user_message:delta")
6281
+ flushUserMessage();
5523
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
+ }
5524
6296
  case "message:started": {
5525
6297
  const messageId = extractMessageId(msg.payload) ?? `msg-${msg.payload?.messageId ?? "run"}`;
5526
6298
  startMessage(messageId);
@@ -5535,9 +6307,12 @@ function createDimEventNormalizer(opts) {
5535
6307
  currentText = "";
5536
6308
  break;
5537
6309
  }
5538
- 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 });
5539
6314
  break;
5540
- // 思考过程不进最终回复
6315
+ }
5541
6316
  case "text:delta": {
5542
6317
  const delta = msg.payload?.delta ?? "";
5543
6318
  if (!delta)
@@ -5611,6 +6386,7 @@ function createDimEventNormalizer(opts) {
5611
6386
  break;
5612
6387
  }
5613
6388
  case "run:ended": {
6389
+ flushUserMessage();
5614
6390
  if (currentMessageId !== null && currentText.trim().length > 0) {
5615
6391
  opts.onEvent({ type: "assistant_message_completed", messageId: currentMessageId, text: currentText });
5616
6392
  } else if (!hasBoundarySignal && finalText.trim()) {
@@ -5648,7 +6424,11 @@ function normalizeUsage(raw) {
5648
6424
  return {
5649
6425
  inputTokens: typeof u.inputTokens === "number" ? u.inputTokens : void 0,
5650
6426
  outputTokens: typeof u.outputTokens === "number" ? u.outputTokens : void 0,
5651
- 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
5652
6432
  };
5653
6433
  }
5654
6434
  var DIM_CAPABILITIES = {
@@ -5703,8 +6483,8 @@ var DimAgentAdapter = class {
5703
6483
  }
5704
6484
  }
5705
6485
  async probe() {
5706
- const { existsSync: existsSync19 } = await import("node:fs");
5707
- 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";
5708
6488
  let authStatus = installed ? "unknown" : "error";
5709
6489
  let lastProbeErrorCode = installed ? void 0 : "DIM_CLI_NOT_FOUND";
5710
6490
  let lastProbeError = installed ? void 0 : `\u672A\u627E\u5230 dim CLI\uFF08${this.dimPath}\uFF09`;
@@ -5996,62 +6776,89 @@ function pickLegacyDimModels(inventory) {
5996
6776
 
5997
6777
  // src/daemon/repo-cache.ts
5998
6778
  import { execFileSync as execFileSync7 } from "node:child_process";
5999
- import { existsSync as existsSync11, mkdirSync as mkdirSync8 } from "node:fs";
6000
- 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";
6001
6781
  function ownerOf(repoFullName) {
6002
6782
  const idx = repoFullName.indexOf("/");
6003
6783
  return idx > 0 ? repoFullName.slice(0, idx) : "_default";
6004
6784
  }
6005
6785
  function buildRepoCachePath(workdir, tenantId, repoFullName) {
6006
- return join14(workdir, "repos", tenantId, ownerOf(repoFullName), repoFullName);
6786
+ return join15(workdir, "repos", tenantId, ownerOf(repoFullName), repoFullName);
6007
6787
  }
6008
- function openRepoCache(workdir, tenantId, repoFullName, env, remoteUrl) {
6788
+ function openRepoCache(workdir, tenantId, repoFullName, env, agentRemoteUrl, daemonRemoteUrl) {
6009
6789
  const repoCachePath = buildRepoCachePath(workdir, tenantId, repoFullName);
6010
- const url = remoteUrl ?? `https://github.com/${repoFullName}`;
6790
+ const currentOrigin = () => {
6791
+ if (!existsSync11(join15(repoCachePath, ".git")))
6792
+ return null;
6793
+ try {
6794
+ return execFileSync7("git", ["-C", repoCachePath, "remote", "get-url", "origin"], { encoding: "utf8", env, stdio: "pipe" }).trim() || null;
6795
+ } catch {
6796
+ return null;
6797
+ }
6798
+ };
6799
+ const fetchUrl = () => daemonRemoteUrl ?? currentOrigin() ?? agentRemoteUrl ?? `https://github.com/${repoFullName}.git`;
6800
+ const originUrl = () => agentRemoteUrl ?? fetchUrl();
6011
6801
  return {
6012
6802
  repoCachePath,
6013
6803
  ensure() {
6014
- if (existsSync11(join14(repoCachePath, ".git"))) {
6015
- if (url)
6016
- syncRemoteUrl(repoCachePath, url, env);
6804
+ if (existsSync11(join15(repoCachePath, ".git"))) {
6805
+ syncRemoteUrl(repoCachePath, originUrl(), env);
6017
6806
  return;
6018
6807
  }
6019
- mkdirSync8(dirname6(repoCachePath), { recursive: true });
6020
- execFileSync7("git", ["clone", "--filter=blob:none", url, repoCachePath], {
6021
- env,
6022
- stdio: "pipe"
6023
- });
6808
+ mkdirSync9(dirname7(repoCachePath), { recursive: true });
6809
+ const cloneUrl = fetchUrl();
6810
+ try {
6811
+ execFileSync7("git", ["clone", "--filter=blob:none", cloneUrl, repoCachePath], { env, stdio: "pipe" });
6812
+ } catch (err) {
6813
+ throw new Error(`REPO_SYNC_FAILED: \u514B\u9686\u4ED3\u5E93\u5931\u8D25 ${repoFullName}: ${gitErrorText2(err)}`);
6814
+ }
6815
+ if (originUrl() !== cloneUrl)
6816
+ syncRemoteUrl(repoCachePath, originUrl(), env);
6024
6817
  },
6025
6818
  fetch() {
6026
6819
  try {
6027
- execFileSync7("git", ["-C", repoCachePath, "fetch", "origin", "--prune"], { env, stdio: "pipe" });
6028
- } catch {
6820
+ if (daemonRemoteUrl) {
6821
+ execFileSync7(
6822
+ "git",
6823
+ ["-C", repoCachePath, "fetch", "--prune", daemonRemoteUrl, "+refs/heads/*:refs/remotes/origin/*"],
6824
+ { env, stdio: "pipe" }
6825
+ );
6826
+ } else {
6827
+ execFileSync7("git", ["-C", repoCachePath, "fetch", "origin", "--prune"], { env, stdio: "pipe" });
6828
+ }
6829
+ } catch (err) {
6830
+ throw new Error(`REPO_SYNC_FAILED: \u62C9\u53D6\u4ED3\u5E93\u5931\u8D25 ${repoFullName}: ${gitErrorText2(err)}`);
6029
6831
  }
6030
6832
  }
6031
6833
  };
6032
6834
  }
6835
+ function gitErrorText2(err) {
6836
+ const e = err;
6837
+ const stderr = e?.stderr ? e.stderr.toString().trim() : "";
6838
+ return stderr || e?.message || String(err);
6839
+ }
6033
6840
 
6034
6841
  // src/daemon/repo-checkout-service.ts
6035
6842
  import { createServer as createServer3 } from "node:http";
6036
6843
  import { execFileSync as execFileSync9 } from "node:child_process";
6037
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync8 } from "node:fs";
6038
- 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";
6039
6846
 
6040
6847
  // src/daemon/worktree-manager.ts
6041
6848
  import { execFileSync as execFileSync8 } from "node:child_process";
6042
- 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";
6043
- 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";
6044
6851
 
6045
6852
  // src/daemon/workspace-marker.ts
6046
- import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
6047
- 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";
6048
6855
  var WORKTREE_MARKER_FILE = ".morula-worktree.json";
6049
6856
  var WORKTREE_MARKER_SCHEMA = 1;
6050
6857
  function markerPath(worktreePath) {
6051
- return join15(worktreePath, WORKTREE_MARKER_FILE);
6858
+ return join16(worktreePath, WORKTREE_MARKER_FILE);
6052
6859
  }
6053
6860
  function writeWorktreeMarker(worktreePath, marker) {
6054
- writeFileSync6(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6861
+ writeFileSync7(markerPath(worktreePath), `${JSON.stringify(marker, null, 2)}
6055
6862
  `, "utf8");
6056
6863
  }
6057
6864
  function readWorktreeMarker(worktreePath) {
@@ -6083,14 +6890,14 @@ function buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code) {
6083
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");
6084
6891
  }
6085
6892
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6086
- return join16(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6893
+ return join17(workdir, "worktrees", tenantId, runtimeSegment, dirName);
6087
6894
  }
6088
6895
  function buildWorktreePath(workdir, tenantId, runtimeId, workContextId, code, repoFullName) {
6089
- return join16(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6896
+ return join17(buildTaskRootPath(workdir, tenantId, runtimeId, workContextId, code), safeDirName(repoFullName));
6090
6897
  }
6091
6898
  function buildLegacyWorktreePath(workdir, tenantId, workContextId, code) {
6092
6899
  const dirName = code ? `${safeDirName(code)}-${workContextId.slice(0, 8)}` : workContextId;
6093
- return join16(workdir, "worktrees", tenantId, dirName);
6900
+ return join17(workdir, "worktrees", tenantId, dirName);
6094
6901
  }
6095
6902
  var legacyPathWarned = /* @__PURE__ */ new Set();
6096
6903
  function warnLegacyWorktreeResidue(legacyPath) {
@@ -6101,13 +6908,13 @@ function warnLegacyWorktreeResidue(legacyPath) {
6101
6908
  }
6102
6909
  var legacyLayoutWarned = /* @__PURE__ */ new Set();
6103
6910
  function warnLegacyLayoutWorktreeResidue(taskRoot, worktreePath) {
6104
- 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)))
6105
6912
  return;
6106
6913
  legacyLayoutWarned.add(taskRoot);
6107
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`);
6108
6915
  }
6109
6916
  function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6110
- const gitignorePath = join16(worktreePath, ".gitignore");
6917
+ const gitignorePath = join17(worktreePath, ".gitignore");
6111
6918
  const existing = existsSync13(gitignorePath) ? readFileSync9(gitignorePath, "utf8") : "";
6112
6919
  const requiredLines = [WORKTREE_MARKER_FILE];
6113
6920
  const missing = requiredLines.filter((l) => !existing.split("\n").some((line) => line.trim() === l));
@@ -6115,7 +6922,7 @@ function ensureMarkerIgnoredAndCommitted(worktreePath, env) {
6115
6922
  const next = `${existing.trimEnd() ? `${existing.trimEnd()}
6116
6923
  ` : ""}${missing.join("\n")}
6117
6924
  `;
6118
- writeFileSync7(gitignorePath, next, "utf8");
6925
+ writeFileSync8(gitignorePath, next, "utf8");
6119
6926
  try {
6120
6927
  execFileSync8("git", ["-C", worktreePath, "add", ".gitignore"], { env, stdio: "pipe" });
6121
6928
  execFileSync8(
@@ -6195,17 +7002,17 @@ function sleepSync2(ms) {
6195
7002
  function backoffMs(baseMs, random = Math.random) {
6196
7003
  return Math.round(baseMs * (0.5 + random() * 0.5));
6197
7004
  }
6198
- function gitErrorText(err) {
7005
+ function gitErrorText3(err) {
6199
7006
  const e = err;
6200
7007
  const stderr = e?.stderr ? e.stderr.toString() : "";
6201
7008
  return `${stderr}${stderr ? "\n" : ""}${e?.message ?? String(err)}`;
6202
7009
  }
6203
7010
  function isIndexLockConflict(err) {
6204
- const text = gitErrorText(err);
7011
+ const text = gitErrorText3(err);
6205
7012
  return /index\.lock/i.test(text) || /另一个 git 进程|another git process/i.test(text);
6206
7013
  }
6207
7014
  function worktreeIndexLockPath(repoCachePath, worktreePath) {
6208
- return join16(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
7015
+ return join17(repoCachePath, ".git", "worktrees", basename(worktreePath), "index.lock");
6209
7016
  }
6210
7017
  function indexLockPathFromError(text) {
6211
7018
  const m = text.match(/[^\s'‘’"]*index\.lock/);
@@ -6242,7 +7049,7 @@ function addWorktreeWithRetry(repoCachePath, worktreePath, ref, deps = {}) {
6242
7049
  throw err;
6243
7050
  if (attempt === WORKTREE_ADD_MAX_ATTEMPTS)
6244
7051
  break;
6245
- const lockPath = indexLockPathFromError(gitErrorText(err)) ?? worktreeIndexLockPath(repoCachePath, worktreePath);
7052
+ const lockPath = indexLockPathFromError(gitErrorText3(err)) ?? worktreeIndexLockPath(repoCachePath, worktreePath);
6246
7053
  const released = waitForIndexLockRelease(lockPath, waitMs);
6247
7054
  const backoff = backoffMs(WORKTREE_ADD_BACKOFF_MS[attempt - 1]);
6248
7055
  console.warn(
@@ -6251,7 +7058,7 @@ function addWorktreeWithRetry(repoCachePath, worktreePath, ref, deps = {}) {
6251
7058
  sleepSync2(backoff);
6252
7059
  }
6253
7060
  }
6254
- throw new Error(`WORKTREE_ADD_FAILED: worktree add \u8FDE\u7EED ${WORKTREE_ADD_MAX_ATTEMPTS} \u6B21\u5931\u8D25\uFF08${gitErrorText(lastErr).trim()}\uFF09`);
7061
+ throw new Error(`WORKTREE_ADD_FAILED: worktree add \u8FDE\u7EED ${WORKTREE_ADD_MAX_ATTEMPTS} \u6B21\u5931\u8D25\uFF08${gitErrorText3(lastErr).trim()}\uFF09`);
6255
7062
  }
6256
7063
  var FOREIGN_RESIDUE_MAX_ENTRIES = 64;
6257
7064
  var FOREIGN_RESIDUE_SCAN_LIMIT = 2e3;
@@ -6259,7 +7066,7 @@ var foreignResidueHits = /* @__PURE__ */ new Map();
6259
7066
  function inspectForeignDirectory(dir) {
6260
7067
  let entries = [];
6261
7068
  try {
6262
- entries = readdirSync3(dir, { withFileTypes: true });
7069
+ entries = readdirSync4(dir, { withFileTypes: true });
6263
7070
  } catch {
6264
7071
  }
6265
7072
  const topNames = entries.slice(0, 20).map((e) => `${e.name}${e.isDirectory() ? "/" : ""}`);
@@ -6273,7 +7080,7 @@ function inspectForeignDirectory(dir) {
6273
7080
  return;
6274
7081
  let children = [];
6275
7082
  try {
6276
- children = readdirSync3(p, { withFileTypes: true });
7083
+ children = readdirSync4(p, { withFileTypes: true });
6277
7084
  } catch {
6278
7085
  return;
6279
7086
  }
@@ -6281,9 +7088,9 @@ function inspectForeignDirectory(dir) {
6281
7088
  if (count >= FOREIGN_RESIDUE_SCAN_LIMIT)
6282
7089
  return;
6283
7090
  count++;
6284
- const full = join16(p, e.name);
7091
+ const full = join17(p, e.name);
6285
7092
  try {
6286
- const st = statSync2(full);
7093
+ const st = statSync3(full);
6287
7094
  bytes += st.size;
6288
7095
  minMs = Math.min(minMs, st.mtimeMs);
6289
7096
  maxMs = Math.max(maxMs, st.mtimeMs);
@@ -6313,7 +7120,7 @@ function reportForeignResidue(worktreePath, forensics, cleaned) {
6313
7120
  console.warn(`[daemon] ${detail}`);
6314
7121
  }
6315
7122
  function prepareWorktree(input) {
6316
- const cache = openRepoCache(input.workdir, input.tenantId, input.repoFullName, input.env, input.remoteUrl);
7123
+ const cache = openRepoCache(input.workdir, input.tenantId, input.repoFullName, input.env, input.remoteUrl, input.daemonRemoteUrl);
6317
7124
  cache.ensure();
6318
7125
  cache.fetch();
6319
7126
  const requestedBase = input.baseBranch?.trim();
@@ -6369,7 +7176,7 @@ function prepareWorktree(input) {
6369
7176
  if (!isGitRepo) {
6370
7177
  const forensics = inspectForeignDirectory(worktreePath);
6371
7178
  try {
6372
- rmSync7(worktreePath, { recursive: true, force: true });
7179
+ rmSync9(worktreePath, { recursive: true, force: true });
6373
7180
  } catch {
6374
7181
  reportForeignResidue(worktreePath, forensics, false);
6375
7182
  throw new Error(`WORKTREE_RESIDUE: \u76EE\u5F55 ${worktreePath} \u6B8B\u7559\u4E14\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u4EBA\u5DE5\u5904\u7406`);
@@ -6425,7 +7232,7 @@ function prepareWorktree(input) {
6425
7232
  }
6426
7233
  }
6427
7234
  if (!existsSync13(worktreePath)) {
6428
- mkdirSync9(join16(worktreePath, ".."), { recursive: true });
7235
+ mkdirSync10(join17(worktreePath, ".."), { recursive: true });
6429
7236
  addWorktreeWithRetry(cache.repoCachePath, worktreePath, `origin/${input.baseBranch}`);
6430
7237
  writeWorktreeMarker(worktreePath, {
6431
7238
  schemaVersion: 1,
@@ -6442,7 +7249,7 @@ function prepareWorktree(input) {
6442
7249
  {
6443
7250
  const dotMorulaRescue = `${worktreePath}.morula-rescue`;
6444
7251
  if (existsSync13(dotMorulaRescue)) {
6445
- const dotMorulaDir = join16(worktreePath, ".morula");
7252
+ const dotMorulaDir = join17(worktreePath, ".morula");
6446
7253
  if (!existsSync13(dotMorulaDir)) {
6447
7254
  try {
6448
7255
  renameSync2(dotMorulaRescue, dotMorulaDir);
@@ -6451,7 +7258,7 @@ function prepareWorktree(input) {
6451
7258
  console.warn(`[daemon] worktree \u6F02\u79FB\u81EA\u52A8\u6062\u590D: .morula/ \u5F52\u8FD8\u5931\u8D25\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${dotMorulaRescue}`);
6452
7259
  }
6453
7260
  } else {
6454
- rmSync7(dotMorulaRescue, { recursive: true, force: true });
7261
+ rmSync9(dotMorulaRescue, { recursive: true, force: true });
6455
7262
  }
6456
7263
  }
6457
7264
  }
@@ -6471,10 +7278,10 @@ function prepareWorktree(input) {
6471
7278
  };
6472
7279
  }
6473
7280
  function resolveWorkspaceLayout(root, expected) {
6474
- const primaryDir = join16(root, safeDirName(expected.repoFullName));
6475
- if (existsSync13(join16(primaryDir, WORKTREE_MARKER_FILE)))
7281
+ const primaryDir = join17(root, safeDirName(expected.repoFullName));
7282
+ if (existsSync13(join17(primaryDir, WORKTREE_MARKER_FILE)))
6476
7283
  return { primaryDir, legacy: false };
6477
- if (existsSync13(join16(root, WORKTREE_MARKER_FILE)))
7284
+ if (existsSync13(join17(root, WORKTREE_MARKER_FILE)))
6478
7285
  return { primaryDir: root, legacy: true };
6479
7286
  return null;
6480
7287
  }
@@ -6498,7 +7305,7 @@ function validateRemovable(taskRoot, expected, repoCachePath) {
6498
7305
  ).join("\n");
6499
7306
  if (statusRelevant.length > 0)
6500
7307
  return "WORKTREE_DIRTY";
6501
- 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] });
6502
7309
  if (additionalActivity)
6503
7310
  return "WORKTREE_DIRTY";
6504
7311
  try {
@@ -6541,13 +7348,13 @@ function findActivityInReposDir(reposDir) {
6541
7348
  return null;
6542
7349
  let names = [];
6543
7350
  try {
6544
- names = readdirSync3(reposDir);
7351
+ names = readdirSync4(reposDir);
6545
7352
  } catch {
6546
7353
  return null;
6547
7354
  }
6548
7355
  for (const name of names) {
6549
- const repoPath = join16(reposDir, name);
6550
- if (!existsSync13(join16(repoPath, ".git")))
7356
+ const repoPath = join17(reposDir, name);
7357
+ if (!existsSync13(join17(repoPath, ".git")))
6551
7358
  continue;
6552
7359
  if (repoHasActivity(repoPath))
6553
7360
  return repoPath;
@@ -6558,20 +7365,20 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6558
7365
  const excluded = new Set(options.excludeDirs ?? []);
6559
7366
  let entries = [];
6560
7367
  try {
6561
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7368
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6562
7369
  } catch {
6563
7370
  return null;
6564
7371
  }
6565
- const candidates = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join16(taskRoot, e.name)).filter((dir) => !excluded.has(dir));
6566
- 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");
6567
7374
  if (existsSync13(legacyReposDir)) {
6568
7375
  try {
6569
- candidates.push(...readdirSync3(legacyReposDir).map((n) => join16(legacyReposDir, n)));
7376
+ candidates.push(...readdirSync4(legacyReposDir).map((n) => join17(legacyReposDir, n)));
6570
7377
  } catch {
6571
7378
  }
6572
7379
  }
6573
7380
  for (const repoPath of candidates) {
6574
- if (!existsSync13(join16(repoPath, ".git")))
7381
+ if (!existsSync13(join17(repoPath, ".git")))
6575
7382
  continue;
6576
7383
  if (repoHasActivity(repoPath))
6577
7384
  return repoPath;
@@ -6579,18 +7386,18 @@ function findAdditionalRepoActivity(taskRoot, options = {}) {
6579
7386
  return null;
6580
7387
  }
6581
7388
  function cleanupInjectedSkills(dir) {
6582
- const injectedSkillsRoot = join16(dir, ".agents", "skills");
7389
+ const injectedSkillsRoot = join17(dir, ".agents", "skills");
6583
7390
  if (!existsSync13(injectedSkillsRoot))
6584
7391
  return;
6585
7392
  try {
6586
- for (const name of readdirSync3(injectedSkillsRoot)) {
7393
+ for (const name of readdirSync4(injectedSkillsRoot)) {
6587
7394
  if (name === COLLABORATION_SKILL_NAME || /^[\w-]{1,64}$/.test(name)) {
6588
- rmSync7(join16(injectedSkillsRoot, name), { recursive: true, force: true });
7395
+ rmSync9(join17(injectedSkillsRoot, name), { recursive: true, force: true });
6589
7396
  }
6590
7397
  }
6591
- if (readdirSync3(injectedSkillsRoot).length === 0) {
6592
- rmSync7(join16(dir, ".agents", "skills"), { recursive: true, force: true });
6593
- 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 });
6594
7401
  }
6595
7402
  } catch {
6596
7403
  }
@@ -6599,15 +7406,15 @@ function collectSiblingGitCommonDirs(taskRoot, excludeDir) {
6599
7406
  const dirs = /* @__PURE__ */ new Set();
6600
7407
  let entries = [];
6601
7408
  try {
6602
- entries = readdirSync3(taskRoot, { withFileTypes: true });
7409
+ entries = readdirSync4(taskRoot, { withFileTypes: true });
6603
7410
  } catch {
6604
7411
  return [];
6605
7412
  }
6606
7413
  for (const e of entries) {
6607
7414
  if (!e.isDirectory() || e.name.startsWith("."))
6608
7415
  continue;
6609
- const repoPath = join16(taskRoot, e.name);
6610
- if (repoPath === excludeDir || !existsSync13(join16(repoPath, ".git")))
7416
+ const repoPath = join17(taskRoot, e.name);
7417
+ if (repoPath === excludeDir || !existsSync13(join17(repoPath, ".git")))
6611
7418
  continue;
6612
7419
  try {
6613
7420
  const commonDir = execFileSync8("git", ["-C", repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: "pipe" }).trim();
@@ -6624,13 +7431,13 @@ function removeTaskWorkspace(repoCachePath, taskRoot, expected) {
6624
7431
  throw new Error("MARKER_MISMATCH");
6625
7432
  const { primaryDir, legacy } = layout;
6626
7433
  if (!legacy) {
6627
- if (primaryDir !== join16(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
7434
+ if (primaryDir !== join17(taskRoot, safeDirName(expected.repoFullName)) || !isManagedWorkspaceRoot(taskRoot))
6628
7435
  throw new Error("GIT_REMOVE_FAILED");
6629
7436
  }
6630
7437
  cleanupInjectedSkills(legacy ? primaryDir : taskRoot);
6631
7438
  const siblingCommonDirs = legacy ? [] : collectSiblingGitCommonDirs(taskRoot, primaryDir);
6632
7439
  try {
6633
- rmSync7(taskRoot, { recursive: true, force: true });
7440
+ rmSync9(taskRoot, { recursive: true, force: true });
6634
7441
  } catch {
6635
7442
  throw new Error("GIT_REMOVE_FAILED");
6636
7443
  }
@@ -6654,7 +7461,7 @@ function removeWorktree(repoCachePath, worktreePath) {
6654
7461
  }
6655
7462
 
6656
7463
  // src/daemon/repo-checkout-service.ts
6657
- var CHECKOUT_JSON_FILE = join17(".morula", "checkout.json");
7464
+ var CHECKOUT_JSON_FILE = join18(".morula", "checkout.json");
6658
7465
  var LocalRepoCheckoutService = class {
6659
7466
  constructor(opts) {
6660
7467
  this.opts = opts;
@@ -6742,9 +7549,9 @@ var LocalRepoCheckoutService = class {
6742
7549
  return;
6743
7550
  this.tasks.set(taskId, { workdir, checkToken, primaryRepoDir });
6744
7551
  try {
6745
- const dir = join17(workdir, ".morula");
6746
- mkdirSync10(dir, { recursive: true, mode: 448 });
6747
- 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({
6748
7555
  taskId,
6749
7556
  port: this.boundPort,
6750
7557
  checkToken
@@ -6798,7 +7605,8 @@ var LocalRepoCheckoutService = class {
6798
7605
  this.opts.tenantId,
6799
7606
  repoFullName,
6800
7607
  env,
6801
- baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0
7608
+ baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0,
7609
+ baseUrl ? buildDaemonRepoUrl(repoFullName, baseUrl) : void 0
6802
7610
  );
6803
7611
  cache.ensure();
6804
7612
  cache.fetch();
@@ -6806,6 +7614,8 @@ var LocalRepoCheckoutService = class {
6806
7614
  repoCachePath: cache.repoCachePath,
6807
7615
  taskRoot: task.workdir,
6808
7616
  repoFullName,
7617
+ baseBranch: serverRes.baseBranch,
7618
+ ref: req.ref,
6809
7619
  env
6810
7620
  });
6811
7621
  const headSha = execFileSync9("git", ["-C", worktreePath, "rev-parse", "HEAD"], { encoding: "utf8", env, stdio: "pipe" }).trim();
@@ -6859,7 +7669,8 @@ var LocalRepoCheckoutService = class {
6859
7669
  this.opts.tenantId,
6860
7670
  repoFullName,
6861
7671
  env,
6862
- baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0
7672
+ baseUrl ? buildGitlabSshRemote(repoFullName, baseUrl) : void 0,
7673
+ baseUrl ? buildDaemonRepoUrl(repoFullName, baseUrl) : void 0
6863
7674
  );
6864
7675
  cache.ensure();
6865
7676
  cache.fetch();
@@ -6904,11 +7715,11 @@ function isReadOnlyRepoDir(dirName) {
6904
7715
  return READ_ONLY_DIR_RE.test(dirName) || dirName.startsWith(LEGACY_READ_ONLY_DIR_PREFIX);
6905
7716
  }
6906
7717
  function resolveRepoTargetDir(taskRoot, base) {
6907
- const direct = join17(taskRoot, base);
6908
- if (!existsSync14(direct) || existsSync14(join17(direct, ".git")))
7718
+ const direct = join18(taskRoot, base);
7719
+ if (!existsSync14(direct) || existsSync14(join18(direct, ".git")))
6909
7720
  return direct;
6910
7721
  for (let i = 2; i < 100; i++) {
6911
- const candidate = join17(taskRoot, `${base}-${i}`);
7722
+ const candidate = join18(taskRoot, `${base}-${i}`);
6912
7723
  if (!existsSync14(candidate))
6913
7724
  return candidate;
6914
7725
  }
@@ -6916,36 +7727,88 @@ function resolveRepoTargetDir(taskRoot, base) {
6916
7727
  }
6917
7728
  function materializeReadWorktree(input) {
6918
7729
  const worktreePath = resolveRepoTargetDir(input.taskRoot, `${safeRepoDirName(input.repoFullName)}${READ_ONLY_REPO_DIR_SUFFIX}`);
6919
- if (existsSync14(join17(worktreePath, ".git"))) {
6920
- execFileSync9("git", ["-C", worktreePath, "fetch", "origin", "--prune"], { env: input.env, stdio: "pipe" });
7730
+ const targetRef = resolveReadRef(input.repoCachePath, input.env, input.ref, input.baseBranch);
7731
+ if (existsSync14(join18(worktreePath, ".git"))) {
7732
+ const current = currentBranchName(worktreePath);
7733
+ if (current)
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`);
7735
+ if (hasLocalChanges2(worktreePath, input.env))
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");
7737
+ execFileSync9("git", ["-C", worktreePath, "checkout", "--detach", targetRef], { env: input.env, stdio: "pipe" });
6921
7738
  } else {
6922
- mkdirSync10(join17(worktreePath, ".."), { recursive: true });
6923
- execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", worktreePath, resolveBaseRef(input.repoCachePath, input.env)], {
7739
+ mkdirSync11(join18(worktreePath, ".."), { recursive: true });
7740
+ execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", worktreePath, targetRef], {
6924
7741
  env: input.env,
6925
7742
  stdio: "pipe"
6926
7743
  });
6927
7744
  }
6928
7745
  return worktreePath;
6929
7746
  }
6930
- function resolveBaseRef(repoCachePath, env) {
6931
- for (const ref of ["origin/main", "origin/master", "HEAD"]) {
6932
- try {
6933
- execFileSync9("git", ["-C", repoCachePath, "rev-parse", "--verify", ref], { env, stdio: "pipe" });
7747
+ function resolveReadRef(repoCachePath, env, requestedRef, baseBranch) {
7748
+ const requested = requestedRef?.trim();
7749
+ if (requested) {
7750
+ const candidates = requested.startsWith("refs/") ? [requested] : [`origin/${requested}`, requested];
7751
+ for (const ref of candidates) {
7752
+ if (refExists(repoCachePath, ref, env))
7753
+ return ref;
7754
+ }
7755
+ throw new Error(`REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u5F15\u7528 ${requested}`);
7756
+ }
7757
+ const explicitBase = baseBranch?.trim();
7758
+ if (explicitBase) {
7759
+ const ref = `origin/${explicitBase}`;
7760
+ if (refExists(repoCachePath, ref, env))
6934
7761
  return ref;
7762
+ throw new Error(`BASE_REF_NOT_FOUND: repo cache \u65E0\u9ED8\u8BA4\u5206\u652F ${explicitBase}`);
7763
+ }
7764
+ const remoteHead = (() => {
7765
+ try {
7766
+ const out = execFileSync9("git", ["-C", repoCachePath, "symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf8", env, stdio: "pipe" }).trim();
7767
+ return out.replace(/^refs\/remotes\//, "");
6935
7768
  } catch {
7769
+ return null;
6936
7770
  }
7771
+ })();
7772
+ for (const ref of [remoteHead, "origin/main", "origin/master"]) {
7773
+ if (!ref || !refExists(repoCachePath, ref, env))
7774
+ continue;
7775
+ return ref;
7776
+ }
7777
+ throw new Error("BASE_REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u8FDC\u7AEF\u5F15\u7528\uFF08origin/HEAD / origin/main / origin/master\uFF09");
7778
+ }
7779
+ function refExists(repoPath, ref, env) {
7780
+ try {
7781
+ execFileSync9("git", ["-C", repoPath, "rev-parse", "--verify", ref], { env, stdio: "pipe" });
7782
+ return true;
7783
+ } catch {
7784
+ return false;
7785
+ }
7786
+ }
7787
+ function currentBranchName(repoPath) {
7788
+ try {
7789
+ const out = execFileSync9("git", ["-C", repoPath, "branch", "--show-current"], { encoding: "utf8", stdio: "pipe" }).trim();
7790
+ return out || null;
7791
+ } catch {
7792
+ return null;
7793
+ }
7794
+ }
7795
+ function hasLocalChanges2(repoPath, env) {
7796
+ try {
7797
+ const out = execFileSync9("git", ["-C", repoPath, "status", "--porcelain"], { encoding: "utf8", env, stdio: "pipe" }).trim();
7798
+ return out.length > 0;
7799
+ } catch {
7800
+ return true;
6937
7801
  }
6938
- throw new Error("BASE_REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u5F15\u7528\uFF08origin/main / origin/master / HEAD\uFF09");
6939
7802
  }
6940
7803
  function listMaterializedRepoDirs(taskRoot, options = {}) {
6941
7804
  const excluded = new Set(options.excludeDirs ?? []);
6942
7805
  let entries;
6943
7806
  try {
6944
- entries = readdirSync4(taskRoot, { withFileTypes: true });
7807
+ entries = readdirSync5(taskRoot, { withFileTypes: true });
6945
7808
  } catch {
6946
7809
  return [];
6947
7810
  }
6948
- 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();
6949
7812
  }
6950
7813
  function parseRepoFullNameFromRemoteUrl(url) {
6951
7814
  const raw = url?.trim();
@@ -6967,7 +7830,7 @@ function parseRepoFullNameFromRemoteUrl(url) {
6967
7830
  }
6968
7831
  function collectMaterializedRepoDeliveryTargets(input) {
6969
7832
  const excludedNames = new Set(input.excludeRepoFullNames?.filter(Boolean) ?? []);
6970
- const excludedDirs = [...excludedNames].map((name) => join17(input.taskRoot, safeRepoDirName(name)));
7833
+ const excludedDirs = [...excludedNames].map((name) => join18(input.taskRoot, safeRepoDirName(name)));
6971
7834
  const recordsByDir = /* @__PURE__ */ new Map();
6972
7835
  const recordsByDirName = /* @__PURE__ */ new Map();
6973
7836
  for (const [repoFullName, record] of input.prepared ?? []) {
@@ -7030,11 +7893,15 @@ function syncGitIdentity(primaryDir, targetDir, env) {
7030
7893
  }
7031
7894
  }
7032
7895
  function materializeAdditionalRepo(input) {
7033
- if (existsSync14(join17(input.targetPath, ".git"))) {
7034
- execFileSync9("git", ["-C", input.targetPath, "fetch", "origin", "--prune"], { env: input.env, stdio: "pipe" });
7896
+ if (existsSync14(join18(input.targetPath, ".git"))) {
7897
+ const current = currentBranchName(input.targetPath);
7898
+ if (!current && !hasLocalChanges2(input.targetPath, input.env)) {
7899
+ const targetRef = resolveReadRef(input.repoCachePath, input.env, void 0, input.baseBranch);
7900
+ execFileSync9("git", ["-C", input.targetPath, "checkout", "--detach", targetRef], { env: input.env, stdio: "pipe" });
7901
+ }
7035
7902
  return input.targetPath;
7036
7903
  }
7037
- const candidates = input.baseBranch ? [`origin/${input.baseBranch}`, "origin/main", "origin/master", "HEAD"] : ["origin/main", "origin/master", "HEAD"];
7904
+ const candidates = input.baseBranch ? [`origin/${input.baseBranch}`, "origin/main", "origin/master"] : ["origin/main", "origin/master"];
7038
7905
  let baseRef = null;
7039
7906
  for (const ref of candidates) {
7040
7907
  try {
@@ -7045,8 +7912,8 @@ function materializeAdditionalRepo(input) {
7045
7912
  }
7046
7913
  }
7047
7914
  if (!baseRef)
7048
- throw new Error("BASE_REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u5F15\u7528\uFF08origin/<base> / origin/main / origin/master / HEAD\uFF09");
7049
- mkdirSync10(join17(input.targetPath, ".."), { recursive: true });
7915
+ throw new Error("BASE_REF_NOT_FOUND: repo cache \u65E0\u53EF\u7528\u8FDC\u7AEF\u5F15\u7528\uFF08origin/<base> / origin/main / origin/master\uFF09");
7916
+ mkdirSync11(join18(input.targetPath, ".."), { recursive: true });
7050
7917
  execFileSync9("git", ["-C", input.repoCachePath, "worktree", "add", "--detach", input.targetPath, baseRef], {
7051
7918
  env: input.env,
7052
7919
  stdio: "pipe"
@@ -7192,52 +8059,857 @@ function buildStageContract(stageCode, source) {
7192
8059
  return STAGE_CONTRACTS[stageCode] ?? "";
7193
8060
  }
7194
8061
 
7195
- // src/daemon/ws-client.ts
7196
- import { createHash as createHash2 } from "node:crypto";
7197
- import { io } from "socket.io-client";
7198
- var WS_AUTH_RELOAD_MAX = 3;
7199
- function tokenFingerprint(token) {
7200
- const digest = createHash2("sha256").update(token).digest("hex").slice(0, 8);
7201
- return `len=${token.length} seg=${token.split(".").length} sha256=${digest}`;
7202
- }
7203
- function isWellFormedToken(token) {
7204
- return token.split(".").length === 3;
7205
- }
7206
- function isAuthConnectError(message) {
7207
- return /malformed|expired|invalid signature|invalid token|jwt|unauthorized|401/i.test(message);
7208
- }
7209
- var DaemonWsClient = class {
7210
- constructor(baseUrl, token, resolveToken2) {
7211
- this.baseUrl = baseUrl;
7212
- this.resolveToken = resolveToken2;
7213
- this.currentToken = token;
7214
- this.currentSource = "startup";
7215
- }
7216
- baseUrl;
7217
- resolveToken;
7218
- socket = null;
7219
- handlers = null;
7220
- currentToken;
7221
- currentSource;
7222
- authReloads = 0;
7223
- reconnectAttempts = 0;
7224
- pollOnly = false;
7225
- /**
7226
- * P1:已订阅的任务房间(taskId → afterSeq)。**订阅意图独立于连接状态**——
7227
- * 连接未就绪(初始连接中 / 重连中 / 凭证重载重建 socket)时先登记,连上后由 `joinTaskRooms()` 重放,
7228
- * 否则「连上之前 claim 到的任务」永远收不到消息(服务端不发 tenant 广播)。
7229
- */
7230
- taskRooms = /* @__PURE__ */ new Map();
7231
- /** 是否已降级为 poll-only(认证长期失败;runner 的 poll 兜底继续工作) */
7232
- get isPollOnly() {
7233
- return this.pollOnly;
7234
- }
7235
- /** 当前已订阅的任务房间(诊断/测试用只读快照) */
7236
- get subscribedTaskIds() {
7237
- return [...this.taskRooms.keys()];
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");
7238
8101
  }
7239
- /**
7240
- * P1:订阅 `task:<taskId>` 房间(幂等)——本 run 的对话消息(`agent-task:message`)只发进该房间。
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
+
8867
+ // src/daemon/ws-client.ts
8868
+ import { createHash as createHash2 } from "node:crypto";
8869
+ import { io } from "socket.io-client";
8870
+ var WS_AUTH_RELOAD_MAX = 3;
8871
+ function tokenFingerprint(token) {
8872
+ const digest = createHash2("sha256").update(token).digest("hex").slice(0, 8);
8873
+ return `len=${token.length} seg=${token.split(".").length} sha256=${digest}`;
8874
+ }
8875
+ function isWellFormedToken(token) {
8876
+ return token.split(".").length === 3;
8877
+ }
8878
+ function isAuthConnectError(message) {
8879
+ return /malformed|expired|invalid signature|invalid token|jwt|unauthorized|401/i.test(message);
8880
+ }
8881
+ var DaemonWsClient = class {
8882
+ constructor(baseUrl, token, resolveToken2) {
8883
+ this.baseUrl = baseUrl;
8884
+ this.resolveToken = resolveToken2;
8885
+ this.currentToken = token;
8886
+ this.currentSource = "startup";
8887
+ }
8888
+ baseUrl;
8889
+ resolveToken;
8890
+ socket = null;
8891
+ handlers = null;
8892
+ currentToken;
8893
+ currentSource;
8894
+ authReloads = 0;
8895
+ reconnectAttempts = 0;
8896
+ pollOnly = false;
8897
+ /**
8898
+ * P1:已订阅的任务房间(taskId → afterSeq)。**订阅意图独立于连接状态**——
8899
+ * 连接未就绪(初始连接中 / 重连中 / 凭证重载重建 socket)时先登记,连上后由 `joinTaskRooms()` 重放,
8900
+ * 否则「连上之前 claim 到的任务」永远收不到消息(服务端不发 tenant 广播)。
8901
+ */
8902
+ taskRooms = /* @__PURE__ */ new Map();
8903
+ /** 是否已降级为 poll-only(认证长期失败;runner 的 poll 兜底继续工作) */
8904
+ get isPollOnly() {
8905
+ return this.pollOnly;
8906
+ }
8907
+ /** 当前已订阅的任务房间(诊断/测试用只读快照) */
8908
+ get subscribedTaskIds() {
8909
+ return [...this.taskRooms.keys()];
8910
+ }
8911
+ /**
8912
+ * P1:订阅 `task:<taskId>` 房间(幂等)——本 run 的对话消息(`agent-task:message`)只发进该房间。
7241
8913
  *
7242
8914
  * 连接未就绪时只登记(连上后 `joinTaskRooms()` 补发),保证「先 claim 后连上」也不漏帧。
7243
8915
  * `afterSeq`:与前端同口径的订阅水位(补齐由 HTTP 拉取负责,见 runner 的 catch-up)。
@@ -7328,6 +9000,12 @@ var DaemonWsClient = class {
7328
9000
  if (event)
7329
9001
  handlers.onTaskMessage(event);
7330
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
+ }
7331
9009
  socket.on("connect_error", (error) => {
7332
9010
  this.onConnectError(socket, error);
7333
9011
  });
@@ -7510,6 +9188,12 @@ var DaemonRunner = class {
7510
9188
  isolatedDimEnv = {};
7511
9189
  /** ACP 连接池(ACP-only:daemon 启动即建,懒 spawn,首个任务才起 dim acp 进程) */
7512
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;
7513
9197
  /** LOCAL-AGENT-REPO-READ-001:本地 repo checkout 端点(agent 本轮内读仓库) */
7514
9198
  repoCheckout = null;
7515
9199
  async start() {
@@ -7545,6 +9229,15 @@ var DaemonRunner = class {
7545
9229
  env: this.isolatedDimEnv
7546
9230
  });
7547
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`);
7548
9241
  this.repoCheckout = new LocalRepoCheckoutService({
7549
9242
  api: this.api,
7550
9243
  cfg: cfg2 ?? loadDaemonConfig(this.store),
@@ -7563,7 +9256,7 @@ var DaemonRunner = class {
7563
9256
  this.log(`[daemon] dim CLI: ${this.dimPath}`);
7564
9257
  let binaryMtime = "unknown";
7565
9258
  try {
7566
- binaryMtime = statSync3(process.execPath).mtime.toISOString();
9259
+ binaryMtime = statSync4(process.execPath).mtime.toISOString();
7567
9260
  } catch (_) {
7568
9261
  }
7569
9262
  this.log(`[daemon] binary=${process.execPath} mtime=${binaryMtime} v=${BUILD_VERSION2}`);
@@ -7629,6 +9322,11 @@ var DaemonRunner = class {
7629
9322
  });
7630
9323
  this.acpPool = null;
7631
9324
  }
9325
+ if (this.sessionHost) {
9326
+ void this.sessionHost.closeAll().catch(() => {
9327
+ });
9328
+ this.sessionHost = null;
9329
+ }
7632
9330
  this.repoCheckout?.close();
7633
9331
  this.repoCheckout = null;
7634
9332
  }
@@ -7965,9 +9663,68 @@ var DaemonRunner = class {
7965
9663
  // P1:重连后房间已重放 → 立即按 afterSeq 重新补齐(断连期间的帧不会补发,只能拉)
7966
9664
  onTaskResubscribed: (taskId) => {
7967
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);
7968
9672
  }
7969
9673
  };
7970
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
+ }
7971
9728
  /**
7972
9729
  * 派发事件入口(P0-b,issues/agent-task-false-failure-on-daemon-fetch-error.md):
7973
9730
  * 服务端把派发事件发到 **tenant 房间**(daemon-executor.service.ts `sendToTenant`,WS 无 per-runtime
@@ -8194,7 +9951,13 @@ var DaemonRunner = class {
8194
9951
  // 本任务的 AgentTask ID:`morula repo mr create`(服务端建 MR 通道)等机器协议据此定位任务;
8195
9952
  // agent 不需要也不应自己拼(issues/mr-created-outside-bound-project.md)
8196
9953
  ...taskId ? { MORULA_TASK_ID: taskId } : {},
8197
- ...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()
8198
9961
  };
8199
9962
  let repoDir = cfg.workdir;
8200
9963
  let taskRoot = cfg.workdir;
@@ -8205,6 +9968,7 @@ var DaemonRunner = class {
8205
9968
  const wc = detail.workContext;
8206
9969
  repoBaseUrl = tokenRes.baseUrl;
8207
9970
  const remoteUrl = repoBaseUrl ? buildGitlabSshRemote(wc?.repoFullName ?? repo, repoBaseUrl) : void 0;
9971
+ const daemonRemoteUrl = buildDaemonRepoUrl(wc?.repoFullName ?? repo, repoBaseUrl);
8208
9972
  env = { ...env, ...buildRepoEnv(tokenRes.token, repoBaseUrl) };
8209
9973
  if (wc?.baseBranch?.trim())
8210
9974
  env = { ...env, MORULA_BASE_BRANCH: wc.baseBranch.trim() };
@@ -8220,7 +9984,8 @@ var DaemonRunner = class {
8220
9984
  baseBranch: wc.baseBranch || "main",
8221
9985
  code: detail.code ?? null,
8222
9986
  env,
8223
- remoteUrl
9987
+ remoteUrl,
9988
+ daemonRemoteUrl
8224
9989
  });
8225
9990
  repoDir = managed.worktreePath;
8226
9991
  worktreePath = managed.worktreePath;
@@ -8229,7 +9994,7 @@ var DaemonRunner = class {
8229
9994
  });
8230
9995
  this.log(`[daemon] \u4EFB\u52A1 ${taskId} \u5DE5\u4F5C\u533A\u5C31\u7EEA: ${taskRoot}\uFF08\u4ED3\u5E93 ${repoDir}\uFF0Cbranch=${wc.branchName}\uFF09`);
8231
9996
  } else {
8232
- repoDir = ensureRepo(cfg.workdir, this.tenantId, repo, env, remoteUrl);
9997
+ repoDir = ensureRepo(cfg.workdir, this.tenantId, repo, env, remoteUrl, daemonRemoteUrl);
8233
9998
  taskRoot = repoDir;
8234
9999
  }
8235
10000
  ensureGitIdentity(repoDir, detail.executor.executorName);
@@ -8246,12 +10011,12 @@ var DaemonRunner = class {
8246
10011
  });
8247
10012
  if (spaceToken?.token)
8248
10013
  env = { ...env, ...buildRepoEnv(spaceToken.token, spaceToken.baseUrl) };
8249
- repoDir = join18(cfg.workdir, "scratch", taskId);
10014
+ repoDir = join20(cfg.workdir, "scratch", taskId);
8250
10015
  taskRoot = repoDir;
8251
- mkdirSync11(repoDir, { recursive: true });
8252
- const readme = join18(repoDir, "README.md");
10016
+ mkdirSync13(repoDir, { recursive: true });
10017
+ const readme = join20(repoDir, "README.md");
8253
10018
  if (!existsSync15(readme)) {
8254
- writeFileSync9(
10019
+ writeFileSync11(
8255
10020
  readme,
8256
10021
  `# \u9694\u79BB\u5DE5\u4F5C\u533A\uFF08\u4EFB\u52A1 ${taskId}\uFF09
8257
10022
 
@@ -8259,7 +10024,8 @@ var DaemonRunner = class {
8259
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
8260
10025
 
8261
10026
  ## \u8BFB\u53D6\u4ED3\u5E93\u5185\u5BB9\uFF08\u91CD\u8981\uFF09
8262
- \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
8263
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
8264
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
8265
10031
  `,
@@ -8435,8 +10201,6 @@ var DaemonRunner = class {
8435
10201
  */
8436
10202
  async runOnce(taskId, detail, taskRoot, provider, providerKey, requestedModelId, env, repoBaseUrl, signal) {
8437
10203
  const streamChunks = [];
8438
- let textBuf = "";
8439
- let textTimer = null;
8440
10204
  let thoughtBuf = "";
8441
10205
  let thoughtTimer = null;
8442
10206
  let streamFlushing = false;
@@ -8535,6 +10299,16 @@ var DaemonRunner = class {
8535
10299
  void flushStreamChunks(false);
8536
10300
  }
8537
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}`) });
8538
10312
  const resumeSessionId = providerKey === "dim" ? detail.dimSessionId ?? detail.lastRun?.dimSessionId ?? null : null;
8539
10313
  const resumeWorkDir = detail.dimWorkDir ?? detail.lastRun?.dimWorkDir ?? null;
8540
10314
  const canResume = !!resumeSessionId && !!resumeWorkDir && existsSync15(resumeWorkDir);
@@ -8581,16 +10355,19 @@ var DaemonRunner = class {
8581
10355
  }
8582
10356
  }
8583
10357
  if (event.type === "tool_call_started") {
8584
- streamChunks.push({
8585
- chunkType: "tool",
8586
- seqHint: nextChunkSeq(),
8587
- data: {
8588
- toolName: event.toolName,
8589
- callId: event.callId,
8590
- input: event.input !== void 0 ? redactInputSummary(event.input) : void 0
8591
- }
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(() => {
8592
10370
  });
8593
- void flushStreamChunks(true);
8594
10371
  }
8595
10372
  if (event.type === "tool_call_completed") {
8596
10373
  streamChunks.push({
@@ -8606,17 +10383,7 @@ var DaemonRunner = class {
8606
10383
  void flushStreamChunks(true);
8607
10384
  }
8608
10385
  if (event.type === "assistant_text_delta") {
8609
- textBuf += event.text;
8610
- if (!textTimer) {
8611
- textTimer = setTimeout(() => {
8612
- textTimer = null;
8613
- if (textBuf) {
8614
- streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text: textBuf } });
8615
- textBuf = "";
8616
- void flushStreamChunks(false);
8617
- }
8618
- }, 200);
8619
- }
10386
+ textStream.push(event.text);
8620
10387
  }
8621
10388
  if (event.type === "usage") {
8622
10389
  streamChunks.push({ chunkType: "usage", seqHint: nextChunkSeq(), data: event.usage });
@@ -8634,14 +10401,8 @@ var DaemonRunner = class {
8634
10401
  }
8635
10402
  };
8636
10403
  const result = await provider.run(input);
8637
- if (textTimer) {
8638
- clearTimeout(textTimer);
8639
- textTimer = null;
8640
- }
8641
- if (textBuf) {
8642
- streamChunks.push({ chunkType: "text", seqHint: nextChunkSeq(), data: { text: textBuf } });
8643
- textBuf = "";
8644
- }
10404
+ await textStream.flushParagraph();
10405
+ textStream.dispose();
8645
10406
  if (thoughtTimer) {
8646
10407
  clearTimeout(thoughtTimer);
8647
10408
  thoughtTimer = null;
@@ -8811,7 +10572,7 @@ var DaemonRunner = class {
8811
10572
  await this.api.submitCleanupResult(item.id, this.runtimeId, { ok: true });
8812
10573
  return;
8813
10574
  }
8814
- const managedRoot = join18(cfg.workdir, "worktrees", this.tenantId);
10575
+ const managedRoot = join20(cfg.workdir, "worktrees", this.tenantId);
8815
10576
  if (!wc.worktreePath.startsWith(`${managedRoot}/`)) {
8816
10577
  await this.api.submitCleanupResult(item.id, this.runtimeId, {
8817
10578
  ok: false,
@@ -8859,38 +10620,38 @@ function isDebugLogEnabled() {
8859
10620
  var spaceTokenFailureLogged = false;
8860
10621
  function writeCustomSkills(targetDir, skills, log) {
8861
10622
  const NAME_RE = /^[\w-]{1,64}$/;
8862
- const base = join18(targetDir, ".agents", "skills");
10623
+ const base = join20(targetDir, ".agents", "skills");
8863
10624
  for (const skill of skills) {
8864
10625
  const name = skill.name.trim();
8865
10626
  if (!NAME_RE.test(name)) {
8866
10627
  log?.(`[daemon] \u8DF3\u8FC7\u975E\u6CD5 skill \u540D\u79F0: ${JSON.stringify(name)}`);
8867
10628
  continue;
8868
10629
  }
8869
- const dir = join18(base, name);
8870
- if (dirname7(dir) !== base) {
10630
+ const dir = join20(base, name);
10631
+ if (dirname8(dir) !== base) {
8871
10632
  log?.(`[daemon] \u8DF3\u8FC7\u8D8A\u754C skill \u8DEF\u5F84: ${JSON.stringify(name)}`);
8872
10633
  continue;
8873
10634
  }
8874
- mkdirSync11(dir, { recursive: true });
10635
+ mkdirSync13(dir, { recursive: true });
8875
10636
  const meta = ["---", `name: ${name}`];
8876
10637
  if (skill.description?.trim()) {
8877
10638
  meta.push(`description: ${skill.description.trim().replace(/\n/g, " ")}`);
8878
10639
  }
8879
10640
  meta.push("---", "");
8880
- writeFileSync9(join18(dir, "SKILL.md"), `${meta.join("\n")}
10641
+ writeFileSync11(join20(dir, "SKILL.md"), `${meta.join("\n")}
8881
10642
  ${skill.content ?? ""}`, "utf8");
8882
10643
  }
8883
10644
  }
8884
10645
  function writeCollaborationSkill(targetDir, repo, repoBaseUrl, log, baseBranch, provider = repoBaseUrl ? "gitlab" : "github") {
8885
- const dir = join18(targetDir, ".agents", "skills", COLLABORATION_SKILL_NAME);
8886
- const refsDir = join18(dir, "references");
8887
- 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 });
8888
10649
  const isGitlab = provider === "gitlab" && !!repoBaseUrl;
8889
10650
  const front = isGitlab ? buildGitlabSkillFrontmatter() : COLLABORATION_SKILL_FRONTMATTER;
8890
10651
  const body = isGitlab ? buildGitlabSkillBody(repo ?? "", repoBaseUrl, baseBranch) : buildGithubSkillBody(baseBranch);
8891
- writeFileSync9(join18(dir, "SKILL.md"), `${front}
10652
+ writeFileSync11(join20(dir, "SKILL.md"), `${front}
8892
10653
  ${body}`, "utf8");
8893
- 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");
8894
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/`);
8895
10656
  }
8896
10657
  function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundBaseBranch) {
@@ -8921,8 +10682,8 @@ function resolveContractPlatform(availableRepos, boundRepo, boundBaseUrl, boundB
8921
10682
  };
8922
10683
  }
8923
10684
  function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
8924
- const briefDir = join18(targetDir, ".agent_context");
8925
- mkdirSync11(briefDir, { recursive: true });
10685
+ const briefDir = join20(targetDir, ".agent_context");
10686
+ mkdirSync13(briefDir, { recursive: true });
8926
10687
  const brief = `# \u4EFB\u52A1\u4E0A\u4E0B\u6587\uFF08Task Context\uFF09
8927
10688
 
8928
10689
  - \u672C AgentTask \u4EFB\u52A1 ID\uFF1A\`${taskId}\`
@@ -8931,7 +10692,7 @@ function writeTaskContextBrief(targetDir, taskId, detail, repo, log) {
8931
10692
  ---
8932
10693
 
8933
10694
  ${buildTaskPromptWithContract(detail, void 0)}`;
8934
- writeFileSync9(join18(briefDir, "task-context.md"), brief, "utf8");
10695
+ writeFileSync11(join20(briefDir, "task-context.md"), brief, "utf8");
8935
10696
  log?.(`[daemon] \u5DF2\u6CE8\u5165\u4EFB\u52A1\u4E0A\u4E0B\u6587 brief \u5230 .agent_context/task-context.md`);
8936
10697
  }
8937
10698
  var PROJECT_AGENTS_MD_MAX_CHARS = 1e5;
@@ -8945,8 +10706,8 @@ function writeProjectAgentsMd(targetDir, agentsMd, log) {
8945
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`);
8946
10707
  content = content.slice(0, PROJECT_AGENTS_MD_MAX_CHARS);
8947
10708
  }
8948
- mkdirSync11(targetDir, { recursive: true });
8949
- writeFileSync9(join18(targetDir, "AGENTS.md"), `${content}
10709
+ mkdirSync13(targetDir, { recursive: true });
10710
+ writeFileSync11(join20(targetDir, "AGENTS.md"), `${content}
8950
10711
  `, "utf8");
8951
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`);
8952
10713
  return true;
@@ -8966,52 +10727,20 @@ function resolveRepo(detail) {
8966
10727
  }
8967
10728
  function buildTaskPromptWithContract(detail, repoBaseUrl) {
8968
10729
  const prompt = detail.taskPrompt ?? "";
8969
- const repo = resolveRepo(detail);
8970
- const snapRepo = detail.contextSnapshot;
8971
- const repoResolution = typeof snapRepo?.repoResolution === "string" ? snapRepo.repoResolution : void 0;
8972
- const availableRepos = Array.isArray(detail.availableRepos) ? detail.availableRepos.filter((r) => r.repoFullName) : [];
8973
- const multiRepoList = availableRepos.length > 1 ? availableRepos.map((r) => `- ${r.repoFullName}\uFF08\u9ED8\u8BA4\u5206\u652F\uFF1A${r.defaultBranch || "main"}\uFF09`).join("\n") : "";
8974
- 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
8975
- \u53EF\u7528\u4ED3\u5E93\u6E05\u5355\uFF1A
8976
- ${multiRepoList}
8977
-
8978
- ` : "";
8979
- let repoHint = "";
8980
- const repoRefs = Array.isArray(detail.contextSnapshot?.repoRefs) ? detail.contextSnapshot.repoRefs : [];
8981
- if (repoRefs.length > 0) {
8982
- const list = repoRefs.map((r) => r.repoFullName).filter(Boolean).join("\u3001");
8983
- 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
8984
-
8985
- `;
8986
- } else if (repo) {
8987
- 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
8988
-
8989
- `;
8990
- } else if (repoResolution === "MULTI_REPO_AGENT_DECIDES") {
8991
- 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
8992
-
8993
- `;
8994
- } else if (repoResolution === "REPO_SELECTION_REQUIRED") {
8995
- const snapCandidates = Array.isArray(detail.contextSnapshot?.repoCandidates) ? detail.contextSnapshot.repoCandidates : [];
8996
- const candidates = snapCandidates.length > 0 ? snapCandidates : availableRepos.map((r) => r.repoFullName).filter((n) => Boolean(n));
8997
- 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
8998
-
8999
- ` : `\u7A7A\u95F4\u5173\u8054\u591A\u4E2A\u4ED3\u5E93\uFF0C\u9700\u8981\u5411\u7528\u6237\u786E\u8BA4\u8981\u64CD\u4F5C\u7684\u4ED3\u5E93\u3002
9000
-
9001
- `;
9002
- } else if (!repo && (repoResolution === "NO_CONNECTION" || !repoResolution)) {
9003
- if (availableRepos.length > 0) {
9004
- const list = availableRepos.map((r) => r.repoFullName).filter(Boolean).join("\u3001");
9005
- 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
9006
10734
 
9007
- `;
9008
- }
9009
- }
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
+ ` : ""}`;
9010
10738
  const toolsHint = `## \u5DE5\u4F5C\u533A\u4E0E\u5DE5\u5177
9011
10739
 
9012
- - \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
9013
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
9014
- - \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
9015
10744
 
9016
10745
  `;
9017
10746
  const taskSource = detail.contextSnapshot?.source;
@@ -9019,19 +10748,12 @@ ${multiRepoList}
9019
10748
  const stageContractBlock = stageContract ? `${stageContract}
9020
10749
 
9021
10750
  ` : "";
9022
- const snapCtx = detail.contextSnapshot;
9023
- let extraHint = "";
9024
- if (snapCtx?.triggerSource === "comment_mention" && snapCtx.commentId) {
9025
- 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
9026
-
9027
- `;
9028
- }
9029
- if (snapCtx?.handoffNote && typeof snapCtx.handoffNote === "string" && snapCtx.handoffNote.trim()) {
9030
- 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}**
9031
10753
 
9032
- `;
9033
- }
9034
- const head = `${stageContractBlock}${toolsHint}${repoHint}${extraHint}`;
10754
+ ` : "";
10755
+ const head = `${stageContractBlock}${keysHint}
10756
+ ${toolsHint}${extraHint}`;
9035
10757
  return head ? `${head}========== \u4F60\u7684\u76EE\u6807 ==========
9036
10758
  ${prompt}` : `${prompt}`;
9037
10759
  }
@@ -9044,7 +10766,7 @@ async function reportRepoTargets(taskId, targets, env, ops) {
9044
10766
  }
9045
10767
  async function reportOneRepoTarget(taskId, target, env, ops) {
9046
10768
  const execEnv = mergeExecEnv(env);
9047
- if (!existsSync15(target.dir) || !existsSync15(join18(target.dir, ".git"))) {
10769
+ if (!existsSync15(target.dir) || !existsSync15(join20(target.dir, ".git"))) {
9048
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`);
9049
10771
  return;
9050
10772
  }
@@ -9213,8 +10935,8 @@ Task ref: ${taskCode}\uFF08\u7531 morula \u81EA\u52A8\u8865\u5145\uFF0C\u7528\u4
9213
10935
  // src/commands/doctor.ts
9214
10936
  init_dist2();
9215
10937
  import { existsSync as existsSync16, readFileSync as readFileSync10, realpathSync } from "node:fs";
9216
- import { homedir as homedir11 } from "node:os";
9217
- 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";
9218
10940
  init_daemon_config();
9219
10941
  init_daemon_lock();
9220
10942
  init_dim_locate();
@@ -9234,10 +10956,10 @@ async function doctorCommand(context) {
9234
10956
  async function collectDoctor(context, probe) {
9235
10957
  const checks = [];
9236
10958
  const env = context.env;
9237
- const configDir = env.MORULA_CONFIG_DIR ?? join19(homedir11(), ".morula-cli");
9238
- const configPath = env.MORULA_CONFIG ?? join19(configDir, "config.yaml");
9239
- const daemonPath = join19(dirname8(configPath), "daemon");
9240
- 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");
9241
10963
  const contextName = context.runtime.context ?? null;
9242
10964
  const baseUrl = context.runtime.baseUrl ?? null;
9243
10965
  const tokenPresent = Boolean(context.runtime.token);
@@ -9251,12 +10973,11 @@ async function collectDoctor(context, probe) {
9251
10973
  configReadable = false;
9252
10974
  }
9253
10975
  }
9254
- const lockPath = join19(daemonPath, DAEMON_LOCK_FILE);
10976
+ const lockPath = join21(daemonPath, DAEMON_LOCK_FILE);
9255
10977
  const lock = probeDaemonLock(lockPath);
9256
- const pidFile = join19(daemonPath, "daemon.pid");
9257
- const filePid = existsSync16(pidFile) ? Number(readFileSync10(pidFile, "utf8").trim() || 0) : 0;
9258
- const lockPid = lock.state === "active" ? lock.record.pid : 0;
9259
- 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;
9260
10981
  const cfg = loadDaemonConfig(context.store);
9261
10982
  const dim = safeResolveDim();
9262
10983
  const dimPath = dim.path;
@@ -9424,12 +11145,12 @@ function detectInstallMode() {
9424
11145
 
9425
11146
  // src/commands/daemon.ts
9426
11147
  function configFilePath(env) {
9427
- 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");
9428
11149
  }
9429
11150
  function daemonDir(configPath) {
9430
- return join20(dirname9(configPath), "daemon");
11151
+ return join22(dirname10(configPath), "daemon");
9431
11152
  }
9432
- var PID_FILE = "daemon.pid";
11153
+ var PID_FILE = DAEMON_PID_FILE;
9433
11154
  var LOG_FILE = "daemon.log";
9434
11155
  async function daemonCommand(context) {
9435
11156
  const { action } = context.parsed;
@@ -9621,8 +11342,8 @@ async function run2(context) {
9621
11342
  saveDaemonConfig(context.store, { name, workdir, maxConcurrent });
9622
11343
  }
9623
11344
  const resolvedWorkdir = workdir ?? loadDaemonConfig(context.store)?.workdir ?? DEFAULT_WORKDIR;
9624
- mkdirSync12(resolvedWorkdir, { recursive: true });
9625
- 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);
9626
11347
  const acquired = acquireDaemonLock(lockPath, { workdir: resolvedWorkdir });
9627
11348
  if (!acquired.ok) {
9628
11349
  const pid = acquired.existing?.pid;
@@ -9696,10 +11417,10 @@ async function start(context) {
9696
11417
  }
9697
11418
  const dir = daemonDir(configFilePath(context.env));
9698
11419
  if (!dryRun)
9699
- mkdirSync12(dir, { recursive: true, mode: 448 });
9700
- const pidFile = join20(dir, PID_FILE);
9701
- const logFile = join20(dir, LOG_FILE);
9702
- 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);
9703
11424
  const lock = probeDaemonLock(lockPath);
9704
11425
  if (lock.state === "active") {
9705
11426
  throw new CliError(
@@ -9718,7 +11439,7 @@ async function start(context) {
9718
11439
  });
9719
11440
  }
9720
11441
  if (!dryRun)
9721
- rmSync8(pidFile, { force: true });
11442
+ rmSync10(pidFile, { force: true });
9722
11443
  }
9723
11444
  const env = {
9724
11445
  ...process.env,
@@ -9756,12 +11477,12 @@ async function start(context) {
9756
11477
  const { spawnDaemonProcess: spawnDaemonProcess2 } = await Promise.resolve().then(() => (init_spawn(), spawn_exports));
9757
11478
  const result = spawnDaemonProcess2({ logFile, env });
9758
11479
  if (result.error || result.pid <= 0) {
9759
- rmSync8(pidFile, { force: true });
11480
+ rmSync10(pidFile, { force: true });
9760
11481
  throw new CliError("DAEMON_START_FAILED", EXIT.OTHER, `[daemon] \u542F\u52A8\u5931\u8D25: ${result.error ?? `pid=${result.pid}`}`, false, {
9761
11482
  hint: `tail -n 50 ${logFile}`
9762
11483
  });
9763
11484
  }
9764
- writeFileSync10(pidFile, String(result.pid), { mode: 384 });
11485
+ writeFileSync12(pidFile, String(result.pid), { mode: 384 });
9765
11486
  context.err(`[daemon] \u5DF2\u540E\u53F0\u542F\u52A8\uFF08pid=${result.pid}\uFF09\uFF0C\u65E5\u5FD7: ${logFile}`);
9766
11487
  return { running: true, pid: result.pid, logFile, lockFile: lockPath };
9767
11488
  }
@@ -9769,8 +11490,8 @@ async function stop(context) {
9769
11490
  rejectUnknownFlags(context.parsed.flags, ["dry-run"]);
9770
11491
  const dryRun = isDryRun(context);
9771
11492
  const dir = daemonDir(configFilePath(context.env));
9772
- const pidFile = join20(dir, PID_FILE);
9773
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
11493
+ const pidFile = join22(dir, PID_FILE);
11494
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
9774
11495
  const lock = probeDaemonLock(lockPath);
9775
11496
  const lockPid = lock.state === "absent" ? null : lock.record?.pid ?? null;
9776
11497
  const filePid = existsSync17(pidFile) ? Number(readFileSync11(pidFile, "utf8").trim()) : null;
@@ -9783,7 +11504,7 @@ async function stop(context) {
9783
11504
  };
9784
11505
  }
9785
11506
  if (!pid) {
9786
- rmSync8(pidFile, { force: true });
11507
+ rmSync10(pidFile, { force: true });
9787
11508
  releaseDaemonLock(lockPath);
9788
11509
  return { running: false, message: "daemon \u672A\u5728\u8FD0\u884C\uFF08\u65E0\u5B58\u6D3B pid\uFF09" };
9789
11510
  }
@@ -9792,7 +11513,7 @@ async function stop(context) {
9792
11513
  releaseDaemonLock(lockPath);
9793
11514
  else
9794
11515
  releaseDaemonLock(lockPath, pid);
9795
- rmSync8(pidFile, { force: true });
11516
+ rmSync10(pidFile, { force: true });
9796
11517
  if (result.outcome === "killed")
9797
11518
  context.err(`[daemon] pid=${pid} \u672A\u54CD\u5E94 SIGTERM\uFF0C\u5DF2 SIGKILL\uFF08\u9501\u5DF2\u91CA\u653E\uFF09`);
9798
11519
  return { running: false, stoppedPid: pid, outcome: result.outcome };
@@ -9801,21 +11522,17 @@ async function status2(context) {
9801
11522
  rejectUnknownFlags(context.parsed.flags, []);
9802
11523
  const cfg = loadDaemonConfig(context.store);
9803
11524
  const dir = daemonDir(configFilePath(context.env));
9804
- const pidFile = join20(dir, PID_FILE);
9805
- const lockPath = join20(dir, DAEMON_LOCK_FILE);
9806
- const lock = probeDaemonLock(lockPath);
9807
- const lockPid = lock.state === "active" ? lock.record.pid : null;
9808
- const filePid = existsSync17(pidFile) ? Number(readFileSync11(pidFile, "utf8").trim() || 0) : 0;
9809
- const pid = lockPid ?? (isProcessAlive(filePid) ? filePid : null);
11525
+ const lockPath = join22(dir, DAEMON_LOCK_FILE);
11526
+ const probe = probeDaemonInstance(dirname10(configFilePath(context.env)));
9810
11527
  return {
9811
- running: pid !== null,
9812
- pid,
11528
+ running: probe.running,
11529
+ pid: probe.pid,
9813
11530
  daemonId: cfg?.daemonId ?? null,
9814
11531
  runtimeId: cfg?.runtimeId ?? null,
9815
11532
  name: cfg?.name ?? null,
9816
11533
  workdir: cfg?.workdir ?? DEFAULT_WORKDIR,
9817
11534
  maxConcurrent: cfg?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT,
9818
- logFile: join20(dir, LOG_FILE),
11535
+ logFile: join22(dir, LOG_FILE),
9819
11536
  lockFile: lockPath
9820
11537
  };
9821
11538
  }
@@ -9843,95 +11560,6 @@ function parseMaxConcurrent(context) {
9843
11560
 
9844
11561
  // src/commands/setup.ts
9845
11562
  init_dist2();
9846
-
9847
- // src/daemon/plugin-register.ts
9848
- 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";
9849
- import { homedir as homedir13 } from "node:os";
9850
- import { dirname as dirname10, join as join21 } from "node:path";
9851
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9852
- var PLUGIN_NAME = "morula-cli";
9853
- var MANIFEST_DIR_NAME = ".codex-plugin";
9854
- function legacyAgentsPluginDir(home = homedir13()) {
9855
- return join21(home, ".agents", "plugins", PLUGIN_NAME);
9856
- }
9857
- function resolveDimcodeHome(env = process.env, home = homedir13()) {
9858
- const fromEnv = env.DIMCODE_HOME?.trim();
9859
- return fromEnv || join21(home, ".dimcode", "v2");
9860
- }
9861
- function defaultPluginsRoot(env = process.env, home = homedir13()) {
9862
- return join21(resolveDimcodeHome(env, home), "plugins");
9863
- }
9864
- function resolvePackagePaths(entryUrl) {
9865
- const distDir = dirname10(fileURLToPath2(entryUrl));
9866
- return {
9867
- packageRoot: dirname10(distDir),
9868
- distDir,
9869
- assetsDir: join21(distDir, "plugin")
9870
- };
9871
- }
9872
- function buildLauncherScript(opts) {
9873
- const entry = join21(opts.packageRoot, "dist", "morula.js");
9874
- return `#!/bin/sh
9875
- # 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
9876
- # \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
9877
- NODE=${JSON.stringify(opts.nodePath)}
9878
- [ -x "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
9879
- if [ -z "$NODE" ]; then
9880
- echo '{"code":"ERROR","message":"node not found; reinstall or re-run: morula-runtime setup"}' >&2
9881
- exit 127
9882
- fi
9883
- exec "$NODE" ${JSON.stringify(entry)} "$@"
9884
- `;
9885
- }
9886
- function assetsReady(paths) {
9887
- return existsSync18(join21(paths.assetsDir, "plugin.json")) && existsSync18(join21(paths.assetsDir, "skills"));
9888
- }
9889
- function listSkillNames(assetsDir) {
9890
- const dir = join21(assetsDir, "skills");
9891
- if (!existsSync18(dir))
9892
- return [];
9893
- return readdirSync5(dir).filter((name) => {
9894
- try {
9895
- return statSync4(join21(dir, name)).isDirectory();
9896
- } catch {
9897
- return false;
9898
- }
9899
- }).sort();
9900
- }
9901
- function registerPlugin(opts) {
9902
- const { pluginsRoot, paths, version } = opts;
9903
- const nodePath = opts.nodePath ?? process.execPath;
9904
- const pluginDir = join21(pluginsRoot, PLUGIN_NAME);
9905
- if (!assetsReady(paths))
9906
- throw new Error(`plugin assets not found at ${paths.assetsDir}; run \`pnpm run build:dist\` (dev) or reinstall the package`);
9907
- const skillsSrc = join21(paths.assetsDir, "skills");
9908
- const skills = listSkillNames(paths.assetsDir);
9909
- const manifestDir = join21(pluginDir, MANIFEST_DIR_NAME);
9910
- mkdirSync13(manifestDir, { recursive: true });
9911
- const manifest = JSON.parse(readFileSync12(join21(paths.assetsDir, "plugin.json"), "utf8"));
9912
- writeFileSync11(join21(manifestDir, "plugin.json"), `${JSON.stringify({ ...manifest, version }, null, 2)}
9913
- `);
9914
- const skillsDest = join21(pluginDir, "skills");
9915
- rmSync9(skillsDest, { recursive: true, force: true });
9916
- mkdirSync13(skillsDest, { recursive: true });
9917
- cpSync(skillsSrc, skillsDest, { recursive: true });
9918
- const binDir = join21(pluginDir, "bin");
9919
- mkdirSync13(binDir, { recursive: true });
9920
- const binPath = join21(binDir, "morula");
9921
- writeFileSync11(binPath, buildLauncherScript({ nodePath, packageRoot: paths.packageRoot }));
9922
- chmodSync3(binPath, 493);
9923
- const legacy = legacyAgentsPluginDir(opts.home);
9924
- return {
9925
- pluginDir,
9926
- binPath,
9927
- skills,
9928
- version,
9929
- node: nodePath,
9930
- legacyDir: existsSync18(join21(legacy, MANIFEST_DIR_NAME)) ? legacy : null
9931
- };
9932
- }
9933
-
9934
- // src/commands/setup.ts
9935
11563
  var KNOWN_FLAGS = ["root", "help"];
9936
11564
  async function setupCommand(context) {
9937
11565
  rejectUnknownFlags(context.parsed.flags, KNOWN_FLAGS);