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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -740,7 +740,7 @@ var BUILD_ID, BUILD_CONTEXTS, BUILD_DEFAULT_CONTEXT;
740
740
  var init_build_env = __esm({
741
741
  "../morula-cli-core/dist/build-env.js"() {
742
742
  "use strict";
743
- BUILD_ID = "20260922142838.0c791f70";
743
+ BUILD_ID = "20260923140226.31d77ab2";
744
744
  BUILD_CONTEXTS = {
745
745
  "local": "http://localhost:8080/api",
746
746
  "test": "https://morula.basecastle.com/api",
@@ -951,6 +951,22 @@ var init_ci = __esm({
951
951
  testReport(runId) {
952
952
  return this.client.get(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/test-report`);
953
953
  }
954
+ /** 触发新流水线(D2)。ref 门禁在服务端(集成分支 ∪ 生产分支)。非幂等。 */
955
+ triggerPipeline(connectionId, ref) {
956
+ return this.client.post(`/vcs/connections/${encodeURIComponent(requiredId2(connectionId, "connection id"))}/pipelines`, { ref });
957
+ }
958
+ /** 重跑整条流水线的失败 job(D2)。只允许 failure / cancelled。非幂等。 */
959
+ retryRun(runId) {
960
+ return this.client.post(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/retry`);
961
+ }
962
+ /** 重跑单个 job(D2)。只允许 failure / cancelled。非幂等。 */
963
+ retryJob(jobId) {
964
+ return this.client.post(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/retry`);
965
+ }
966
+ /** 触发手动 job(D2,仅人面)。非幂等。 */
967
+ playJob(jobId) {
968
+ return this.client.post(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/play`);
969
+ }
954
970
  };
955
971
  }
956
972
  });
@@ -4408,7 +4424,7 @@ init_dist2();
4408
4424
  init_dist2();
4409
4425
 
4410
4426
  // src/build-env.ts
4411
- var BUILD_VERSION2 = "0.1.0-alpha.12";
4427
+ var BUILD_VERSION2 = "0.1.0-alpha.14";
4412
4428
 
4413
4429
  // src/commands/daemon.ts
4414
4430
  init_dist();
@@ -8661,7 +8677,6 @@ var SessionHost = class {
8661
8677
  env,
8662
8678
  turnNo: 0,
8663
8679
  promptInFlight: null,
8664
- cancelRequested: false,
8665
8680
  // resume 成功 → dim 会话里已有上一进程注入的管家指令,不重复前置
8666
8681
  instructed: resumed,
8667
8682
  appliedConfig: { ...appliedConfig, permission },
@@ -8694,7 +8709,6 @@ var SessionHost = class {
8694
8709
  if (!cmd.prompt?.trim())
8695
8710
  return { ok: false, error: "SESSION_PROMPT_EMPTY" };
8696
8711
  managed.turnNo += 1;
8697
- managed.cancelRequested = false;
8698
8712
  this.touch(managed);
8699
8713
  const turnNo = managed.turnNo;
8700
8714
  const mapper = new AcpEventMapper();
@@ -8890,12 +8904,16 @@ ${fileRefs.join("\n")}` : text }];
8890
8904
  }
8891
8905
  return { blocks, fileRefs };
8892
8906
  }
8893
- /** 取消当前回合(ACP session/cancel;会话保持打开——长驻语义) */
8907
+ /**
8908
+ * 取消当前回合(ACP `session/cancel` 通知;会话保持打开——长驻语义)。
8909
+ *
8910
+ * **软中断**:dim 在安全点(step 之间)生效,不是硬杀进程;daemon 只负责把通知发出去,
8911
+ * 本轮的终态仍由 ACP `session/prompt` 响应驱动(`cancelled` → `turn_completed`)。
8912
+ */
8894
8913
  cancel(cmd) {
8895
8914
  const managed = this.sessions.get(cmd.sessionId);
8896
8915
  if (!managed)
8897
8916
  return { ok: false, error: "SESSION_NOT_OPEN" };
8898
- managed.cancelRequested = true;
8899
8917
  managed.conn.cancel(managed.dimSessionId);
8900
8918
  this.touch(managed);
8901
8919
  return { ok: true };
@@ -10091,16 +10109,25 @@ var DaemonRunner = class {
10091
10109
  /**
10092
10110
  * 会话宿主命令分发。
10093
10111
  *
10094
- * 两道闸:
10112
+ * 三道闸:
10095
10113
  * ① **runtimeId + daemonId 自过滤**(tenant 广播 → 只有归属设备处理;字段缺失时不过滤,兼容旧服务端)。
10096
10114
  * daemonId 是**实例**维度(issues/duplicate-daemon-same-runtime-double-execution.md):同机多实例
10097
10115
  * 注册时会复用同一行 AgentRuntime(同一 runtimeId),缺了它两个实例会各自 `open` 同一 sessionId
10098
10116
  * 并各跑一轮 prompt(实测:同一轮两份正文事件、两个 daemon 都「会话已打开」);
10099
- * ② **按会话串行**:服务端每轮 prompt 前会补发 `open` 做兜底,而 socket.io 只保证「送达有序」,
10100
- * 不保证「异步处理有序」——`void handleSessionCommand()` 会让 open/prompt 交错执行,
10101
- * prompt 可能先于 open 完成而拿到 SESSION_NOT_OPEN。故按 sessionId 串成一条 promise 链。
10117
+ * ② **按会话串行**(open / prompt / config):服务端每轮 prompt 前会补发 `open` 做兜底,而 socket.io
10118
+ * 只保证「送达有序」,不保证「异步处理有序」——`void handleSessionCommand()` 会让 open/prompt
10119
+ * 交错执行,prompt 可能先于 open 完成而拿到 SESSION_NOT_OPEN。故按 sessionId 串成一条 promise 链。
10120
+ * ③ **抢占类命令(cancel / close)不进链**:`prompt` 的 promise 直到本轮 `end_turn` 才 resolve
10121
+ * (`session-host.prompt()` await 的是 ACP `session/prompt` 响应),排在它后面 = 取消永远发生在
10122
+ * 回合结束之后,用户看到的是「点了停止但没停」(issues/steward-session-cancel-queued-behind-prompt.md)。
10123
+ * 它们只等该会话的 **open 门**——open 未完成时会话还不存在,取消只会拿到 SESSION_NOT_OPEN。
10102
10124
  */
10103
10125
  sessionCommandChains = /* @__PURE__ */ new Map();
10126
+ /**
10127
+ * 各会话**在飞 `open` 命令**的完成信号(抢占类命令的唯一前置门)。
10128
+ * 只在 open 入链时登记,open 落定即清理——它表达的是「会话在设备上就绪」,与后续 prompt 无关。
10129
+ */
10130
+ sessionOpenGates = /* @__PURE__ */ new Map();
10104
10131
  handleSessionCommand(kind, payload) {
10105
10132
  const runtimeId = typeof payload.runtimeId === "string" ? payload.runtimeId : void 0;
10106
10133
  if (runtimeId && this.runtimeId && runtimeId !== this.runtimeId)
@@ -10113,15 +10140,25 @@ var DaemonRunner = class {
10113
10140
  this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u7F3A\u5C11 sessionId\uFF08\u5FFD\u7565\uFF09`);
10114
10141
  return Promise.resolve();
10115
10142
  }
10143
+ const logFailure = (err) => {
10144
+ this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
10145
+ };
10146
+ if (kind === "cancel" || kind === "close") {
10147
+ const gate = this.sessionOpenGates.get(sessionId) ?? Promise.resolve();
10148
+ return gate.catch(() => {
10149
+ }).then(() => this.runSessionCommand(kind, payload)).catch(logFailure);
10150
+ }
10116
10151
  const prev = this.sessionCommandChains.get(sessionId) ?? Promise.resolve();
10117
10152
  const next = prev.catch(() => {
10118
- }).then(() => this.runSessionCommand(kind, payload)).catch((err) => {
10119
- this.log(`[daemon] \u4F1A\u8BDD\u547D\u4EE4 ${kind} \u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
10120
- });
10153
+ }).then(() => this.runSessionCommand(kind, payload)).catch(logFailure);
10121
10154
  this.sessionCommandChains.set(sessionId, next);
10155
+ if (kind === "open")
10156
+ this.sessionOpenGates.set(sessionId, next);
10122
10157
  void next.finally(() => {
10123
10158
  if (this.sessionCommandChains.get(sessionId) === next)
10124
10159
  this.sessionCommandChains.delete(sessionId);
10160
+ if (this.sessionOpenGates.get(sessionId) === next)
10161
+ this.sessionOpenGates.delete(sessionId);
10125
10162
  });
10126
10163
  return next;
10127
10164
  }
@@ -10137,9 +10174,13 @@ var DaemonRunner = class {
10137
10174
  await host.open(payload);
10138
10175
  else if (kind === "prompt")
10139
10176
  await host.prompt(payload);
10140
- else if (kind === "cancel")
10141
- host.cancel(payload);
10142
- else if (kind === "config")
10177
+ else if (kind === "cancel") {
10178
+ const res = host.cancel(payload);
10179
+ if (res.ok)
10180
+ this.log(`[daemon] \u4F1A\u8BDD ${String(payload.sessionId ?? "")} \u53D6\u6D88\u5DF2\u4E0B\u53D1\uFF08ACP session/cancel\uFF09`);
10181
+ else
10182
+ this.log(`[daemon] \u4F1A\u8BDD ${String(payload.sessionId ?? "")} \u53D6\u6D88\u5931\u8D25: ${res.error ?? "UNKNOWN"}`);
10183
+ } else if (kind === "config")
10143
10184
  await host.config(payload);
10144
10185
  else if (kind === "close")
10145
10186
  await host.close(payload);
package/dist/morula.js CHANGED
@@ -704,8 +704,8 @@ function page(ok, message) {
704
704
  }
705
705
 
706
706
  // ../morula-cli-core/dist/build-env.js
707
- var BUILD_VERSION = "0.1.0-alpha.12";
708
- var BUILD_ID = "20260922142838.0c791f70";
707
+ var BUILD_VERSION = "0.1.0-alpha.14";
708
+ var BUILD_ID = "20260923140226.31d77ab2";
709
709
  var BUILD_CONTEXTS = {
710
710
  "local": "http://localhost:8080/api",
711
711
  "test": "https://morula.basecastle.com/api",
@@ -880,6 +880,22 @@ var CiApi = class {
880
880
  testReport(runId) {
881
881
  return this.client.get(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/test-report`);
882
882
  }
883
+ /** 触发新流水线(D2)。ref 门禁在服务端(集成分支 ∪ 生产分支)。非幂等。 */
884
+ triggerPipeline(connectionId, ref) {
885
+ return this.client.post(`/vcs/connections/${encodeURIComponent(requiredId2(connectionId, "connection id"))}/pipelines`, { ref });
886
+ }
887
+ /** 重跑整条流水线的失败 job(D2)。只允许 failure / cancelled。非幂等。 */
888
+ retryRun(runId) {
889
+ return this.client.post(`/vcs/runs/${encodeURIComponent(requiredId2(runId, "run id"))}/retry`);
890
+ }
891
+ /** 重跑单个 job(D2)。只允许 failure / cancelled。非幂等。 */
892
+ retryJob(jobId) {
893
+ return this.client.post(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/retry`);
894
+ }
895
+ /** 触发手动 job(D2,仅人面)。非幂等。 */
896
+ playJob(jobId) {
897
+ return this.client.post(`/vcs/jobs/${encodeURIComponent(requiredId2(jobId, "job id"))}/play`);
898
+ }
883
899
  };
884
900
  function requiredId2(id, label) {
885
901
  const normalized = id.trim();
@@ -2209,7 +2225,13 @@ async function ciCommand(context) {
2209
2225
  return ciDeployments(context);
2210
2226
  if (action === "test-report")
2211
2227
  return ciTestReport(context);
2212
- throw usage("usage: morula ci <status|log|deployments|test-report>");
2228
+ if (action === "retry")
2229
+ return ciRetry(context);
2230
+ if (action === "trigger")
2231
+ return ciTrigger(context);
2232
+ if (action === "play")
2233
+ return ciPlay(context);
2234
+ throw usage("usage: morula ci <status|log|deployments|test-report|retry|trigger|play>");
2213
2235
  }
2214
2236
  async function ciStatus(context) {
2215
2237
  const { positionals, flags } = context.parsed;
@@ -2289,6 +2311,76 @@ async function ciTestReport(context) {
2289
2311
  throw ciFailure(result);
2290
2312
  return result;
2291
2313
  }
2314
+ async function ciTrigger(context) {
2315
+ const { flags } = context.parsed;
2316
+ rejectUnknownFlags(flags, ["space", "ref"]);
2317
+ const spaceId = flagString(flags, "space");
2318
+ const ref = flagString(flags, "ref");
2319
+ if (!spaceId)
2320
+ throw usage("usage: morula ci trigger --space <spaceId> --ref <ref>");
2321
+ if (!ref)
2322
+ throw usage("--ref \u5FC5\u586B\uFF1A\u8981\u89E6\u53D1\u6D41\u6C34\u7EBF\u7684\u5206\u652F\u540D");
2323
+ const view = await context.client.ci.spaceView(spaceId, { limit: 1 });
2324
+ if (view.ok === false)
2325
+ throw ciFailure(view);
2326
+ const connectionId = view.repos?.[0]?.connectionId;
2327
+ if (!connectionId) {
2328
+ throw new CliError("NO_CONNECTION", EXIT.OTHER, `\u7A7A\u95F4 ${spaceId} \u6CA1\u6709\u7ED1\u5B9A\u7684\u4ED3\u5E93\u8FDE\u63A5\uFF0C\u65E0\u6CD5\u89E6\u53D1\u6D41\u6C34\u7EBF`);
2329
+ }
2330
+ const result = await context.client.ci.triggerPipeline(connectionId, ref);
2331
+ if (result.ok === false)
2332
+ throw ciFailure(result, "\u64CD\u4F5C");
2333
+ return result;
2334
+ }
2335
+ async function ciPlay(context) {
2336
+ const { flags } = context.parsed;
2337
+ rejectUnknownFlags(flags, ["job"]);
2338
+ const jobId = flagString(flags, "job");
2339
+ if (!jobId)
2340
+ throw usage("usage: morula ci play --job <jobId>");
2341
+ const result = await context.client.ci.playJob(jobId);
2342
+ if (result.ok === false)
2343
+ throw ciFailure(result, "\u64CD\u4F5C");
2344
+ return result;
2345
+ }
2346
+ async function ciRetry(context) {
2347
+ const { positionals, flags } = context.parsed;
2348
+ rejectUnknownFlags(flags, ["space", "run", "job"]);
2349
+ const usageHint = "usage: morula ci retry --space <spaceId> [--run <runId>] [--job <jobId>]";
2350
+ if (positionals.length)
2351
+ throw usage(`\u91CD\u8DD1\u53EA\u5728\u7A7A\u95F4\u9762\u5F00\u653E\uFF1A\u8BF7\u7528 --space <spaceId>\u3002${usageHint}`);
2352
+ const runId = flagString(flags, "run");
2353
+ const jobId = flagString(flags, "job");
2354
+ if (runId && jobId)
2355
+ throw usage(`--run \u4E0E --job \u4E92\u65A5\uFF0C\u53EA\u80FD\u7ED9\u4E00\u4E2A\uFF08${usageHint}\uFF09`);
2356
+ const spaceId = flagString(flags, "space");
2357
+ if (!spaceId)
2358
+ throw usage(usageHint);
2359
+ if (jobId) {
2360
+ const result2 = await context.client.ci.retryJob(jobId);
2361
+ if (result2.ok === false)
2362
+ throw ciFailure(result2, "\u64CD\u4F5C");
2363
+ return result2;
2364
+ }
2365
+ let targetRunId = runId;
2366
+ if (!targetRunId) {
2367
+ const view = await context.client.ci.spaceView(spaceId, { failed: true });
2368
+ if (view.ok === false)
2369
+ throw ciFailure(view);
2370
+ targetRunId = view.runs[0]?.id;
2371
+ if (!targetRunId) {
2372
+ throw new CliError(
2373
+ "CI_RUN_NOT_FOUND",
2374
+ EXIT.NOT_FOUND,
2375
+ `\u7A7A\u95F4 ${spaceId} \u6CA1\u6709\u5931\u8D25\u7684\u6D41\u6C34\u7EBF\u8FD0\u884C\u53EF\u91CD\u8DD1\uFF08\u53EF\u5148\u7528 morula ci status --space ${spaceId} --failed \u786E\u8BA4\uFF09`
2376
+ );
2377
+ }
2378
+ }
2379
+ const result = await context.client.ci.retryRun(targetRunId);
2380
+ if (result.ok === false)
2381
+ throw ciFailure(result, "\u64CD\u4F5C");
2382
+ return result;
2383
+ }
2292
2384
  function resolveFace(context, usageHint) {
2293
2385
  const taskId = context.parsed.positionals[0]?.trim();
2294
2386
  const spaceId = flagString(context.parsed.flags, "space");
@@ -2298,11 +2390,11 @@ function resolveFace(context, usageHint) {
2298
2390
  throw usage(usageHint);
2299
2391
  return taskId ? { kind: "task", taskId } : { kind: "space", spaceId };
2300
2392
  }
2301
- function ciFailure(failure) {
2393
+ function ciFailure(failure, label = "\u67E5\u8BE2") {
2302
2394
  return new CliError(
2303
2395
  failure.code,
2304
2396
  exitForCiCode(failure.code),
2305
- `CI \u67E5\u8BE2\u5931\u8D25: ${failure.message || "\u672A\u77E5\u539F\u56E0"}\uFF08${failure.code}\uFF09`
2397
+ `CI ${label}\u5931\u8D25: ${failure.message || "\u672A\u77E5\u539F\u56E0"}\uFF08${failure.code}\uFF09`
2306
2398
  );
2307
2399
  }
2308
2400
  function exitForCiCode(code) {
@@ -3787,7 +3879,7 @@ var SPEC = {
3787
3879
  },
3788
3880
  {
3789
3881
  resource: "ci",
3790
- summary: "CI/CD pipeline facts (read-only): pipeline runs / job logs / deployments / failed test cases. Read faces share one command surface \u2014 task face `<taskId>` (AgentTask credential, self-check your MR pipeline) vs space face `--space <spaceId>` (steward/session credential, space-wide inspection); the two are mutually exclusive. Write operations (retry / play / trigger) were removed on 2026-09-22 (CI panel is read-only): operate pipelines directly in the VCS platform instead",
3882
+ summary: "CI/CD pipeline facts + write operations: pipeline runs / job logs / deployments / failed test cases / trigger pipeline / retry / play manual job. Read faces share one command surface \u2014 task face `<taskId>` (AgentTask credential, self-check your MR pipeline) vs space face `--space <spaceId>` (steward/session credential, space-wide inspection); the two are mutually exclusive. Write ops (trigger / retry / play) are ONLY open on the space face (not task face); agent_session tokens are read-only for CI (AgentSessionScopeGuard does not whitelist these endpoints). D2: retry of the newest FAILED run is the default when --run is omitted",
3791
3883
  actions: [
3792
3884
  {
3793
3885
  action: "status",
@@ -3843,11 +3935,47 @@ var SPEC = {
3843
3935
  writes: "none",
3844
3936
  idempotent: true,
3845
3937
  emits: ["ok", "runId", "report"]
3938
+ },
3939
+ {
3940
+ action: "trigger",
3941
+ summary: "trigger a new pipeline on the space's first active connection for the given ref (space face ONLY; ref must be in the connection's integration branch or production branches, or the server rejects with CI_REF_NOT_ALLOWED; NOT idempotent)",
3942
+ flags: [
3943
+ { name: "space", type: "string", valueHint: "<id>", description: "\u7A7A\u95F4 ID\uFF08\u5FC5\u586B\uFF09", required: true },
3944
+ { name: "ref", type: "string", valueHint: "<ref>", description: "\u8981\u89E6\u53D1\u6D41\u6C34\u7EBF\u7684\u5206\u652F\u540D\uFF08\u5FC5\u586B\uFF09", required: true }
3945
+ ],
3946
+ examples: ["morula ci trigger --space <id> --ref dev"],
3947
+ writes: "remote",
3948
+ idempotent: false,
3949
+ emits: ["ok", "action", "runId", "jobId", "externalId", "ref", "webUrl", "acceptedAt"]
3950
+ },
3951
+ {
3952
+ action: "play",
3953
+ summary: "trigger a manual job (space face ONLY \u2014 job id lookup is stateless; NOT idempotent; only manual jobs \u2014 status=manual \u2014 can be played, others get CI_JOB_NOT_MANUAL)",
3954
+ flags: [
3955
+ { name: "job", type: "string", valueHint: "<jobId>", description: "\u5E73\u53F0 job ID\uFF08\u5FC5\u586B\uFF1B\u6765\u81EA ci status \u7684 jobs[].id\uFF0C\u987B\u4E3A\u624B\u52A8 job\uFF09", required: true }
3956
+ ],
3957
+ examples: ["morula ci play --job <jobId>"],
3958
+ writes: "remote",
3959
+ idempotent: false,
3960
+ emits: ["ok", "action", "runId", "jobId", "externalId", "ref", "acceptedAt"]
3961
+ },
3962
+ {
3963
+ action: "retry",
3964
+ summary: "re-run a failed pipeline (space face ONLY \u2014 a positional <taskId> is a usage error, retry is not open to the task face; person + steward, and a steward must go through an elicitation confirm card). Omit both --run and --job to retry the newest FAILED run of the space (no failed run at all \u2192 CI_RUN_NOT_FOUND exit 3). Only failure/cancelled runs or jobs can be retried (CI_RETRY_NOT_ALLOWED exit 6). NOT idempotent: every call starts another platform re-run",
3965
+ flags: [
3966
+ { name: "space", type: "string", valueHint: "<id>", description: "\u7A7A\u95F4 ID\uFF08\u5FC5\u586B\uFF09", required: true },
3967
+ { name: "run", type: "string", valueHint: "<runId>", description: "\u8981\u91CD\u8DD1\u7684\u8FD0\u884C ID\uFF08\u4E0E --job \u4E92\u65A5\uFF1B\u7F3A\u7701 = \u8BE5\u7A7A\u95F4\u6700\u65B0\u4E00\u6761\u5931\u8D25\u8FD0\u884C\uFF09" },
3968
+ { name: "job", type: "string", valueHint: "<jobId>", description: "\u8981\u91CD\u8DD1\u7684\u5355\u4E2A job ID\uFF08\u4E0E --run \u4E92\u65A5\uFF1B\u6765\u81EA ci status \u7684 jobs[].id\uFF09" }
3969
+ ],
3970
+ examples: ["morula ci retry --space <id>", "morula ci retry --space <id> --job <jobId>"],
3971
+ writes: "remote",
3972
+ idempotent: false,
3973
+ emits: ["ok", "action", "runId", "jobId", "externalId", "ref", "acceptedAt"]
3846
3974
  }
3847
3975
  ],
3848
3976
  notes: [
3849
3977
  "\u4E24\u4E2A\u9762\uFF08\u4EFB\u52A1\u9762 <taskId> / \u7A7A\u95F4\u9762 --space <spaceId>\uFF09\u4E92\u65A5\uFF1A\u540C\u65F6\u7ED9 \u2192 usage error(2)\uFF0C\u90FD\u4E0D\u7ED9\u540C\u6837 usage error\u3002\u51ED\u8BC1\u4E0E\u4F5C\u7528\u57DF\u4E0D\u540C\uFF0C\u731C\u9519\u4F1A\u53D8\u6210\u9759\u9ED8\u8D8A\u6743\u6216\u9759\u9ED8\u5931\u8D25",
3850
- "CI \u547D\u4EE4\u9762\u7EAF\u53EA\u8BFB\uFF08\u5199\u64CD\u4F5C\u5DF2\u79FB\u9664\uFF0C2026-09-22 \u5B9A\u6848\uFF09\uFF1Astatus / log / deployments / test-report \u5168\u90E8 writes: none\uFF1B\u91CD\u8DD1 / \u89E6\u53D1\u6D41\u6C34\u7EBF\u8BF7\u5728 VCS \u5E73\u53F0\uFF08GitLab/GitHub\uFF09\u4E0A\u76F4\u63A5\u64CD\u4F5C",
3978
+ "D2 \u5199\u64CD\u4F5C\uFF08trigger / play / retry\uFF09\u53EA\u5728\u7A7A\u95F4\u9762\u5F00\u653E\uFF1Aagent_session \u51ED\u8BC1\uFF08\u7BA1\u7406\u5458\uFF09\u6743\u9650\u53EA\u8BFB\uFF0C\u4E0D\u767B\u8BB0 AgentSessionScopeGuard \u5199\u9762\u767D\u540D\u5355\u2014\u2014retry \u8D70\u7BA1\u5BB6 elicitation \u786E\u8BA4\u5361\uFF0Cplay \u4EC5\u5728 web \u4EBA\u9762\u4F7F\u7528",
3851
3979
  "\u670D\u52A1\u7AEF\u4E1A\u52A1\u5931\u8D25\u662F HTTP 200 + `{ ok:false, code, message }`\uFF1ACI_JOB_NOT_FOUND / CI_RUN_NOT_FOUND / TASK_NOT_FOUND / OBJECT_NOT_FOUND \u2192 exit 3\uFF1BCAPABILITY_UNSUPPORTED / NO_CONNECTION / \u5176\u4F59 \u2192 exit 1"
3852
3980
  ]
3853
3981
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morula-cli",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.14",
4
4
  "description": "Use Morula CLI task management from DimCode.",
5
5
  "interface": {
6
6
  "displayName": "Morula CLI",
@@ -1401,10 +1401,10 @@ morula repo mr diff <taskId> --mr <iid>
1401
1401
 
1402
1402
  ## `morula ci`
1403
1403
 
1404
- CI/CD pipeline facts (read-only): pipeline runs / job logs / deployments / failed test cases. Read faces share one command surface — task face `<taskId>` (AgentTask credential, self-check your MR pipeline) vs space face `--space <spaceId>` (steward/session credential, space-wide inspection); the two are mutually exclusive. Write operations (retry / play / trigger) were removed on 2026-09-22 (CI panel is read-only): operate pipelines directly in the VCS platform instead
1404
+ CI/CD pipeline facts + write operations: pipeline runs / job logs / deployments / failed test cases / trigger pipeline / retry / play manual job. Read faces share one command surface — task face `<taskId>` (AgentTask credential, self-check your MR pipeline) vs space face `--space <spaceId>` (steward/session credential, space-wide inspection); the two are mutually exclusive. Write ops (trigger / retry / play) are ONLY open on the space face (not task face); agent_session tokens are read-only for CI (AgentSessionScopeGuard does not whitelist these endpoints). D2: retry of the newest FAILED run is the default when --run is omitted
1405
1405
 
1406
1406
  > 两个面(任务面 <taskId> / 空间面 --space <spaceId>)互斥:同时给 → usage error(2),都不给同样 usage error。凭证与作用域不同,猜错会变成静默越权或静默失败
1407
- > CI 命令面纯只读(写操作已移除,2026-09-22 定案):status / log / deployments / test-report 全部 writes: none;重跑 / 触发流水线请在 VCS 平台(GitLab/GitHub)上直接操作
1407
+ > D2 写操作(trigger / play / retry)只在空间面开放:agent_session 凭证(管理员)权限只读,不登记 AgentSessionScopeGuard 写面白名单——retry 走管家 elicitation 确认卡,play 仅在 web 人面使用
1408
1408
  > 服务端业务失败是 HTTP 200 + `{ ok:false, code, message }`:CI_JOB_NOT_FOUND / CI_RUN_NOT_FOUND / TASK_NOT_FOUND / OBJECT_NOT_FOUND → exit 3;CAPABILITY_UNSUPPORTED / NO_CONNECTION / 其余 → exit 1
1409
1409
 
1410
1410
  ### `morula ci status`
@@ -1503,6 +1503,64 @@ morula ci test-report <taskId>
1503
1503
 
1504
1504
  > `writes: none` · `interactive: no` · `idempotent: yes` · `emits: ok, runId, report`
1505
1505
 
1506
+ ### `morula ci trigger`
1507
+
1508
+ **用法**:`morula ci trigger [--space <id>] [--ref <ref>]`
1509
+
1510
+ trigger a new pipeline on the space's first active connection for the given ref (space face ONLY; ref must be in the connection's integration branch or production branches, or the server rejects with CI_REF_NOT_ALLOWED; NOT idempotent)
1511
+
1512
+ | Flag | 类型 | 必填 | 取值 | 默认 | 说明 |
1513
+ |---|---|---|---|---|---|
1514
+ | `--space <id>` | string | 是 | - | - | 空间 ID(必填) |
1515
+ | `--ref <ref>` | string | 是 | - | - | 要触发流水线的分支名(必填) |
1516
+
1517
+ **示例**
1518
+
1519
+ ```bash
1520
+ morula ci trigger --space <id> --ref dev
1521
+ ```
1522
+
1523
+ > `writes: remote` · `interactive: no` · `idempotent: no` · `emits: ok, action, runId, jobId, externalId, ref, webUrl, acceptedAt`
1524
+
1525
+ ### `morula ci play`
1526
+
1527
+ **用法**:`morula ci play [--job <jobId>]`
1528
+
1529
+ trigger a manual job (space face ONLY — job id lookup is stateless; NOT idempotent; only manual jobs — status=manual — can be played, others get CI_JOB_NOT_MANUAL)
1530
+
1531
+ | Flag | 类型 | 必填 | 取值 | 默认 | 说明 |
1532
+ |---|---|---|---|---|---|
1533
+ | `--job <jobId>` | string | 是 | - | - | 平台 job ID(必填;来自 ci status 的 jobs[].id,须为手动 job) |
1534
+
1535
+ **示例**
1536
+
1537
+ ```bash
1538
+ morula ci play --job <jobId>
1539
+ ```
1540
+
1541
+ > `writes: remote` · `interactive: no` · `idempotent: no` · `emits: ok, action, runId, jobId, externalId, ref, acceptedAt`
1542
+
1543
+ ### `morula ci retry`
1544
+
1545
+ **用法**:`morula ci retry [--space <id>] [--run <runId>] [--job <jobId>]`
1546
+
1547
+ re-run a failed pipeline (space face ONLY — a positional <taskId> is a usage error, retry is not open to the task face; person + steward, and a steward must go through an elicitation confirm card). Omit both --run and --job to retry the newest FAILED run of the space (no failed run at all → CI_RUN_NOT_FOUND exit 3). Only failure/cancelled runs or jobs can be retried (CI_RETRY_NOT_ALLOWED exit 6). NOT idempotent: every call starts another platform re-run
1548
+
1549
+ | Flag | 类型 | 必填 | 取值 | 默认 | 说明 |
1550
+ |---|---|---|---|---|---|
1551
+ | `--space <id>` | string | 是 | - | - | 空间 ID(必填) |
1552
+ | `--run <runId>` | string | - | - | - | 要重跑的运行 ID(与 --job 互斥;缺省 = 该空间最新一条失败运行) |
1553
+ | `--job <jobId>` | string | - | - | - | 要重跑的单个 job ID(与 --run 互斥;来自 ci status 的 jobs[].id) |
1554
+
1555
+ **示例**
1556
+
1557
+ ```bash
1558
+ morula ci retry --space <id>
1559
+ morula ci retry --space <id> --job <jobId>
1560
+ ```
1561
+
1562
+ > `writes: remote` · `interactive: no` · `idempotent: no` · `emits: ok, action, runId, jobId, externalId, ref, acceptedAt`
1563
+
1506
1564
  <!-- hand:start ci -->
1507
1565
  <!-- hand:end ci -->
1508
1566
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcships/morula-runtime",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.12",
4
+ "version": "0.1.0-alpha.14",
5
5
  "description": "Device-level CLI for the Morula local agent runtime (daemon/portal/models/auth) and the Morula business CLI (morula), distributed via npm",
6
6
  "bin": {
7
7
  "morula-runtime": "./bin/morula-runtime.js",