baa-conductor

git clone 

commit
4796db4
parent
d7a83fd
author
im_wower
date
2026-03-26 01:24:38 +0800 CST
merge: land T-S013 to T-S016
32 files changed,  +954, -109
M DESIGN.md
+9, -9
 1@@ -56,9 +56,9 @@
 2 
 3 ### 本地只读观察面
 4 
 5-- `http://100.71.210.78:4318`
 6-- 由 `apps/status-api` 提供
 7-- 只用于本地调试和过渡观察,不再作为默认对外业务接口
 8+- 推荐入口已经收口到 `conductor` 的 `http://100.71.210.78:4317/v1/status` 与 `/v1/status/ui`
 9+- `http://100.71.210.78:4318` 仍由 `apps/status-api` 提供兼容包装层
10+- 只用于本地调试、过渡观察和 legacy 路径兼容,不再作为默认对外业务接口
11 
12 ### 浏览器控制面
13 
14@@ -72,7 +72,7 @@
15 
16 - `mini` 本地常驻进程
17 - 负责 heartbeat、租约续约、最小调度循环和 runtime 探针
18-- 是后续统一 discovery / control / task / run 接口的目标承载面
19+- 是后续统一 discovery / control / status-view / task / run 接口的目标承载面
20 
21 ### legacy `control-api` surface(已移出当前仓库)
22 
23@@ -82,10 +82,10 @@
24 
25 ### `apps/status-api`
26 
27-- `mini` 本地状态读取面
28+- `mini` 本地状态兼容包装层
29 - 默认读取 `BAA_CONDUCTOR_LOCAL_API` 对应的 conductor `/v1/system/state`
30 - `BAA_CONTROL_API_BASE` 只剩手工兼容回退语义
31-- 定位是迁移期本地只读观察服务,不是主控制面
32+- 继续保留 `/describe`、`/v1/status`、`/v1/status/ui`、`/ui` 等 legacy 本地观察合同,不是主控制面
33 
34 ### `codexd`(已实现独立 daemon,仍在继续收口)
35 
36@@ -219,9 +219,9 @@
37 ## 11. 当前已知残留
38 
39 - `conductor-daemon` 和 runtime 脚本已经有 canonical 名字 `BAA_CONDUCTOR_PUBLIC_API_BASE` / `--public-api-base`,但仍保留 legacy 别名 `BAA_CONTROL_API_BASE` / `--control-api-base`
40-- `status-api` 仍是独立的本地只读观察面,是否并入 `conductor-daemon` 尚未定案
41-- 根验证入口已经落到 `pnpm lint` / `pnpm test`,但 `worker-runner` 和 runtime / e2e 检查还没全部接进去
42-- runtime 默认服务集合仍包含 `status-api`;是否继续维持默认安装/启动,尚未定案
43+- `conductor-daemon` 已承接 `/v1/status` 和 `/v1/status/ui`;`status-api` 退到显式 opt-in 的本地兼容包装层
44+- 根验证入口已经落到 `pnpm lint` / `pnpm test`,工作区还补了 `pnpm smoke`;但 on-node `mini` 检查还没收成单独 wrapper
45+- runtime 默认服务集合已经收口到 `conductor` + `codexd`;`status-api` 改成显式 opt-in 观察服务
46 - `conductor.makefile.so` 目前只回源 `conductor-daemon` 已有路由,尚未承接完整业务 API
47 - 线上和历史文档里仍可能残留 `control-api.makefile.so`、Cloudflare / D1 相关资产或表述
48 - 如果后续再次出现 app-server 未发出合法 `turn/completed` 就提前断流,应视为新的 child / transport 故障,而不是 reopen 已修复的 BUG-008 / BUG-010
M README.md
+11, -9
 1@@ -88,7 +88,9 @@ docs/
 2 
 3 - 所有新接口设计默认先落 `mini` 本地 `4317`
 4 - 所有新公网说明统一写 `conductor.makefile.so`
 5-- `status-api` 只作为本地只读观察面,不再作为默认对外业务接口
 6+- 只读状态视图的推荐入口已收口到 `conductor` 的 `/v1/status` 和 `/v1/status/ui`
 7+- `status-api` 只作为本地只读观察兼容层,不再作为默认对外业务接口
 8+- `mini` on-node 静态+运行态检查统一入口是 `./scripts/runtime/verify-mini.sh`(仓库根可用 `pnpm verify:mini`)
 9 - 运行中的浏览器插件代码以 [`plugins/baa-firefox`](./plugins/baa-firefox) 为准
10 - 当前正式浏览器 HTTP 面是 `/v1/browser/*`,只支持 Claude,且通过本地 `/ws/firefox` 转发到 Firefox 插件页面内 HTTP 代理
11 - `codexd` 目前还是半成品,不是已上线组件
12@@ -99,9 +101,9 @@ docs/
13 
14 | 面 | 地址 | 定位 | 说明 |
15 | --- | --- | --- | --- |
16-| local API | `http://100.71.210.78:4317` | 唯一主接口、内网真相源 | 当前已承接 `/describe`、`/health`、`/version`、`/v1/capabilities`、`/v1/browser/*`、`/v1/system/state`、`/v1/controllers`、`/v1/tasks`、`/v1/runs`、`pause/resume/drain`、`/v1/exec`、`/v1/files/read` 和 `/v1/files/write`;其中 `/v1/browser/*` 当前只支持 Claude,host-ops 统一要求 `Authorization: Bearer <BAA_SHARED_TOKEN>` |
17+| local API | `http://100.71.210.78:4317` | 唯一主接口、内网真相源 | 当前已承接 `/describe`、`/health`、`/version`、`/v1/capabilities`、`/v1/status`、`/v1/status/ui`、`/v1/browser/*`、`/v1/system/state`、`/v1/controllers`、`/v1/tasks`、`/v1/runs`、`pause/resume/drain`、`/v1/exec`、`/v1/files/read` 和 `/v1/files/write`;其中 `/v1/browser/*` 当前只支持 Claude,host-ops 统一要求 `Authorization: Bearer <BAA_SHARED_TOKEN>` |
18 | public host | `https://conductor.makefile.so` | 唯一公网域名 | 由 VPS Nginx 回源到 `100.71.210.78:4317`;`/v1/exec` 和 `/v1/files/*` 不再允许匿名调用 |
19-| local status view | `http://100.71.210.78:4318` | 本地只读观察面 | 迁移期保留,不是主控制面 |
20+| local status view | `http://100.71.210.78:4318` | 本地只读观察兼容层 | 显式 opt-in 保留,继续提供 `/describe`、`/v1/status`、`/v1/status/ui` 和 `/ui` 等 legacy 合同,不是主控制面 |
21 
22 legacy 兼容说明:
23 
24@@ -113,21 +115,21 @@ legacy 兼容说明:
25 
26 1. 把 `/describe`、能力发现、状态/任务/运行查询与控制动作并到 `mini` 本地 API,并保持 `conductor.makefile.so` 同步暴露。
27 2. 让浏览器、CLI、AI、运维文档全部默认走 `conductor.makefile.so` / `100.71.210.78:4317`。
28-3. 让 `status-api` 退回本地辅助视图,或并入 `conductor` 主接口。
29+3. 让 `status-api` 退回本地兼容包装层,并把 `/v1/status`、`/v1/status/ui` 并到 `conductor` 主接口。
30 4. 删除 `control-api.makefile.so`、Cloudflare Worker、D1 和 hand/shell 的主系统角色。
31 
32 ## 当前最重要的事
33 
34 - 保持 `mini` launchd、自启动和本地探针稳定
35 - 保持 `conductor.makefile.so -> 100.71.210.78:4317` 的链路稳定
36-- 把剩余测试入口继续收口到仓库根
37-- 决定 `status-api` 是否还应该留在默认 runtime 服务集合里
38+- 把剩余 on-node 检查继续收口成更统一的入口
39+- 继续清理仍依赖 `4318` wrapper 的旧脚本、书签和文档
40 
41 ## 当前已知 gap
42 
43-- `worker-runner` 还没有稳定包级 `test`,所以根 `pnpm test` 仍未覆盖它
44-- `status-api` 仍是独立的本地只读观察服务,而且仍在默认 runtime 安装/启动/检查集合里;是否继续保留这个默认位次,仍待后续任务决定
45-- `status-api` 已经默认读取 `conductor-daemon` 的 `/v1/system/state`,但仍是单独的本地只读观察服务;是否继续保留还是并入主接口,仍待后续任务决定
46+- `verify-mini.sh` 只收口静态检查和运行态探针;会话级链路回归仍要单独跑 `pnpm smoke` 或 `./scripts/runtime/codexd-e2e-smoke.sh`
47+- `status-api` 已降为显式 opt-in 的本地只读兼容包装层;旧调用方仍可能继续依赖 `4318`
48+- `status-api` 和 `conductor /v1/status` 现在共享同一套状态拼装/渲染语义;后续如果要删 `status-api`,还需要先盘点残留调用方
49 
50 ## 本机能力层
51 
M apps/conductor-daemon/package.json
+2, -2
 1@@ -8,10 +8,10 @@
 2     "@baa-conductor/host-ops": "workspace:*"
 3   },
 4   "scripts": {
 5-    "build": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm exec tsc -p tsconfig.json",
 6+    "build": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm -C ../.. -F @baa-conductor/status-api build && pnpm exec tsc -p tsconfig.json",
 7     "dev": "pnpm run build && node dist/index.js",
 8     "start": "node dist/index.js",
 9     "test": "pnpm run build && node --test src/index.test.js",
10-    "typecheck": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm exec tsc --noEmit -p tsconfig.json"
11+    "typecheck": "pnpm -C ../.. -F @baa-conductor/db build && pnpm -C ../.. -F @baa-conductor/host-ops build && pnpm -C ../.. -F @baa-conductor/status-api build && pnpm exec tsc --noEmit -p tsconfig.json"
12   }
13 }
M apps/conductor-daemon/src/index.test.js
+42, -0
 1@@ -1757,6 +1757,32 @@ test("handleConductorHttpRequest serves the migrated local business endpoints fr
 2       localApiContext
 3     );
 4     assert.equal(parseJsonBody(systemStateResponse).data.mode, "paused");
 5+
 6+    const statusViewResponse = await handleConductorHttpRequest(
 7+      {
 8+        method: "GET",
 9+        path: "/v1/status"
10+      },
11+      localApiContext
12+    );
13+    assert.equal(statusViewResponse.status, 200);
14+    const statusViewPayload = parseJsonBody(statusViewResponse);
15+    assert.deepEqual(Object.keys(statusViewPayload).sort(), ["data", "ok"]);
16+    assert.equal(statusViewPayload.data.mode, "paused");
17+    assert.equal(statusViewPayload.data.queueDepth, 0);
18+    assert.equal(statusViewPayload.data.activeRuns, 1);
19+
20+    const statusViewUiResponse = await handleConductorHttpRequest(
21+      {
22+        method: "GET",
23+        path: "/v1/status/ui"
24+      },
25+      localApiContext
26+    );
27+    assert.equal(statusViewUiResponse.status, 200);
28+    assert.equal(statusViewUiResponse.headers["content-type"], "text/html; charset=utf-8");
29+    assert.match(statusViewUiResponse.body, /JSON endpoint: <strong>\/v1\/status<\/strong>/u);
30+    assert.match(statusViewUiResponse.body, /HTML endpoint: <strong>\/v1\/status\/ui<\/strong>/u);
31   } finally {
32     await codexd.stop();
33     rmSync(hostOpsDir, {
34@@ -1961,6 +1987,21 @@ test("ConductorRuntime serves health and migrated local API endpoints over HTTP"
35     assert.equal(systemStatePayload.data.holder_id, "mini-main");
36     assert.equal(systemStatePayload.data.mode, "running");
37 
38+    const statusViewResponse = await fetch(`${baseUrl}/v1/status`);
39+    assert.equal(statusViewResponse.status, 200);
40+    const statusViewPayload = await statusViewResponse.json();
41+    assert.deepEqual(Object.keys(statusViewPayload).sort(), ["data", "ok"]);
42+    assert.equal(statusViewPayload.data.mode, "running");
43+    assert.equal(statusViewPayload.data.queueDepth, 0);
44+    assert.equal(statusViewPayload.data.activeRuns, 0);
45+
46+    const statusViewUiResponse = await fetch(`${baseUrl}/v1/status/ui`);
47+    assert.equal(statusViewUiResponse.status, 200);
48+    assert.equal(statusViewUiResponse.headers.get("content-type"), "text/html; charset=utf-8");
49+    const statusViewUiHtml = await statusViewUiResponse.text();
50+    assert.match(statusViewUiHtml, /Readable automation state for people and browser controls\./u);
51+    assert.match(statusViewUiHtml, /HTML endpoint: <strong>\/v1\/status\/ui<\/strong>/u);
52+
53     const codexStatusResponse = await fetch(`${baseUrl}/v1/codex`);
54     assert.equal(codexStatusResponse.status, 200);
55     const codexStatusPayload = await codexStatusResponse.json();
56@@ -2031,6 +2072,7 @@ test("ConductorRuntime serves health and migrated local API endpoints over HTTP"
57     const businessDescribePayload = await businessDescribeResponse.json();
58     assert.equal(businessDescribePayload.data.surface, "business");
59     assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/codex/u);
60+    assert.match(JSON.stringify(businessDescribePayload.data.endpoints), /\/v1\/status/u);
61 
62     const controlDescribeResponse = await fetch(`${baseUrl}/describe/control`);
63     assert.equal(controlDescribeResponse.status, 200);
M apps/conductor-daemon/src/local-api.ts
+70, -0
  1@@ -25,6 +25,10 @@ import {
  2   type FileReadOperationRequest,
  3   type FileWriteOperationRequest
  4 } from "../../../packages/host-ops/dist/index.js";
  5+// @ts-ignore conductor reuses the built status-api snapshot normalizer directly.
  6+import { createStatusSnapshotFromControlApiPayload } from "../../status-api/dist/apps/status-api/src/data-source.js";
  7+// @ts-ignore conductor reuses the built status-api HTML renderer directly.
  8+import { renderStatusPage } from "../../status-api/dist/apps/status-api/src/render.js";
  9 
 10 import {
 11   jsonResponse,
 12@@ -58,6 +62,10 @@ const CODEX_ROUTE_IDS = new Set([
 13 const HOST_OPERATIONS_ROUTE_IDS = new Set(["host.exec", "host.files.read", "host.files.write"]);
 14 const HOST_OPERATIONS_AUTH_HEADER = "Authorization: Bearer <BAA_SHARED_TOKEN>";
 15 const HOST_OPERATIONS_WWW_AUTHENTICATE = 'Bearer realm="baa-conductor-host-ops"';
 16+const STATUS_VIEW_HTML_HEADERS = {
 17+  "cache-control": "no-store",
 18+  "content-type": "text/html; charset=utf-8"
 19+} as const;
 20 const BROWSER_CLAUDE_PLATFORM = "claude";
 21 const BROWSER_CLAUDE_ROOT_URL = "https://claude.ai/";
 22 const BROWSER_CLAUDE_ORGANIZATIONS_PATH = "/api/organizations";
 23@@ -322,6 +330,20 @@ const LOCAL_API_ROUTES: LocalApiRouteDefinition[] = [
 24     pathPattern: "/v1/system/state",
 25     summary: "读取本地系统状态"
 26   },
 27+  {
 28+    id: "status.view.json",
 29+    kind: "read",
 30+    method: "GET",
 31+    pathPattern: "/v1/status",
 32+    summary: "读取兼容 status-api 的只读 JSON 状态视图"
 33+  },
 34+  {
 35+    id: "status.view.ui",
 36+    kind: "read",
 37+    method: "GET",
 38+    pathPattern: "/v1/status/ui",
 39+    summary: "读取兼容 status-api 的只读 HTML 状态面板"
 40+  },
 41   {
 42     id: "system.pause",
 43     kind: "write",
 44@@ -2408,6 +2430,8 @@ function routeBelongsToSurface(
 45       "service.health",
 46       "service.version",
 47       "system.capabilities",
 48+      "status.view.json",
 49+      "status.view.ui",
 50       "browser.status",
 51       "browser.claude.open",
 52       "browser.claude.send",
 53@@ -2462,6 +2486,7 @@ function buildCapabilitiesData(
 54     workflow: [
 55       "GET /describe",
 56       "GET /v1/capabilities",
 57+      "GET /v1/status for the narrower read-only status view",
 58       "GET /v1/browser if browser mediation is needed",
 59       "GET /v1/system/state",
 60       "GET /v1/browser/claude/current or /v1/tasks or /v1/codex",
 61@@ -2601,6 +2626,12 @@ async function handleDescribeRead(context: LocalApiRequestContext, version: stri
 62         path: "/v1/system/state",
 63         curl: buildCurlExample(origin, requireRouteDefinition("system.state"))
 64       },
 65+      {
 66+        title: "Read the read-only compatibility status view",
 67+        method: "GET",
 68+        path: "/v1/status",
 69+        curl: buildCurlExample(origin, requireRouteDefinition("status.view.json"))
 70+      },
 71       {
 72         title: "Pause local automation explicitly",
 73         method: "POST",
 74@@ -2631,6 +2662,7 @@ async function handleDescribeRead(context: LocalApiRequestContext, version: stri
 75     ],
 76     notes: [
 77       "AI callers should prefer /describe/business for business queries and /describe/control for control actions.",
 78+      "GET /v1/status and GET /v1/status/ui expose the narrow read-only compatibility status view; /v1/system/state remains the fuller control-oriented truth surface.",
 79       "The formal /v1/browser/* surface currently supports Claude only and forwards browser work through the local Firefox bridge.",
 80       "All /v1/codex routes proxy the independent codexd daemon; this process does not host Codex sessions itself.",
 81       "POST /v1/exec and POST /v1/files/* require Authorization: Bearer <BAA_SHARED_TOKEN>; missing or wrong tokens return 401 JSON.",
 82@@ -2690,6 +2722,12 @@ async function handleScopedDescribeRead(
 83           path: "/v1/browser/claude/current",
 84           curl: buildCurlExample(origin, requireRouteDefinition("browser.claude.current"))
 85         },
 86+        {
 87+          title: "Read the compatibility status snapshot",
 88+          method: "GET",
 89+          path: "/v1/status",
 90+          curl: buildCurlExample(origin, requireRouteDefinition("status.view.json"))
 91+        },
 92         {
 93           title: "List recent tasks",
 94           method: "GET",
 95@@ -2714,6 +2752,7 @@ async function handleScopedDescribeRead(
 96       ],
 97       notes: [
 98         "This surface is intended to be enough for business-query discovery without reading external docs.",
 99+        "Use GET /v1/status for the narrow read-only compatibility snapshot and GET /v1/status/ui for the matching HTML panel.",
100         "The formal /v1/browser/* surface currently supports Claude only and rides on the local Firefox bridge.",
101         "All /v1/codex routes proxy the independent codexd daemon instead of an in-process bridge.",
102         "If you pivot to /describe/control for /v1/exec or /v1/files/*, those host-ops routes require Authorization: Bearer <BAA_SHARED_TOKEN>.",
103@@ -2865,6 +2904,33 @@ async function handleSystemStateRead(context: LocalApiRequestContext): Promise<C
104   );
105 }
106 
107+async function loadStatusViewSnapshot(context: LocalApiRequestContext) {
108+  return createStatusSnapshotFromControlApiPayload(
109+    await buildSystemStateData(requireRepository(context.repository)),
110+    new Date(context.now())
111+  );
112+}
113+
114+async function handleStatusViewJsonRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
115+  return jsonResponse(200, {
116+    ok: true,
117+    data: await loadStatusViewSnapshot(context)
118+  });
119+}
120+
121+async function handleStatusViewUiRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
122+  return {
123+    status: 200,
124+    headers: {
125+      ...STATUS_VIEW_HTML_HEADERS
126+    },
127+    body: renderStatusPage(await loadStatusViewSnapshot(context), {
128+      htmlPaths: ["/v1/status/ui"],
129+      jsonPath: "/v1/status"
130+    })
131+  };
132+}
133+
134 async function handleBrowserStatusRead(context: LocalApiRequestContext): Promise<ConductorHttpResponse> {
135   return buildSuccessEnvelope(context.requestId, 200, buildBrowserStatusData(context));
136 }
137@@ -3436,6 +3502,10 @@ async function dispatchBusinessRoute(
138       return handleCodexTurnCreate(context);
139     case "system.state":
140       return handleSystemStateRead(context);
141+    case "status.view.json":
142+      return handleStatusViewJsonRead(context);
143+    case "status.view.ui":
144+      return handleStatusViewUiRead(context);
145     case "system.pause":
146       return handleSystemMutation(context, "paused");
147     case "system.resume":
M apps/status-api/src/index.test.js
+1, -0
1@@ -64,6 +64,7 @@ test("status-api describe reports conductor local truth with legacy compatibilit
2   assert.equal(payload.data.truth_source.base_url, "http://100.71.210.78:4317");
3   assert.deepEqual(payload.data.notes, [
4     "Status API is read-only.",
5+    "Preferred entry lives on conductor: /v1/status and /v1/status/ui.",
6     "Default truth source comes from BAA_CONDUCTOR_LOCAL_API.",
7     "Use BAA_CONTROL_API_BASE only for legacy ad-hoc compatibility overrides."
8   ]);
M apps/status-api/src/render.ts
+37, -3
 1@@ -1,11 +1,24 @@
 2 import type { StatusSnapshot } from "./contracts.js";
 3 
 4-export function renderStatusPage(snapshot: StatusSnapshot): string {
 5+const DEFAULT_STATUS_JSON_PATH = "/v1/status";
 6+const DEFAULT_STATUS_HTML_PATHS = ["/", "/v1/status/ui"] as const;
 7+
 8+export interface StatusPageRenderOptions {
 9+  htmlPaths?: readonly string[];
10+  jsonPath?: string;
11+}
12+
13+export function renderStatusPage(
14+  snapshot: StatusSnapshot,
15+  options: StatusPageRenderOptions = {}
16+): string {
17   const modeLabel = formatMode(snapshot.mode);
18   const leaderLabel = snapshot.leaderHost ?? snapshot.leaderId ?? "No active leader lease";
19   const leaderDetail = snapshot.leaderId == null ? "Truth source did not report an active holder." : `holder_id=${snapshot.leaderId}`;
20   const leaseLabel = snapshot.leaseExpiresAt == null ? "No lease expiry" : formatTimestamp(snapshot.leaseExpiresAt);
21   const leaseDetail = snapshot.leaseActive ? "Lease is currently valid." : "Lease is missing or stale.";
22+  const jsonPath = normalizeStatusPath(options.jsonPath, DEFAULT_STATUS_JSON_PATH);
23+  const htmlPaths = normalizeStatusPathList(options.htmlPaths, DEFAULT_STATUS_HTML_PATHS);
24 
25   return `<!doctype html>
26 <html lang="en">
27@@ -240,8 +253,8 @@ export function renderStatusPage(snapshot: StatusSnapshot): string {
28       </section>
29 
30       <section class="footer">
31-        <p class="meta">JSON endpoint: <strong>/v1/status</strong></p>
32-        <p class="meta">HTML endpoint: <strong>/</strong> or <strong>/v1/status/ui</strong></p>
33+        <p class="meta">JSON endpoint: ${renderEndpointList([jsonPath])}</p>
34+        <p class="meta">HTML endpoint: ${renderEndpointList(htmlPaths)}</p>
35         <p class="meta">Observed at: ${escapeHtml(formatTimestamp(snapshot.observedAt))}</p>
36       </section>
37     </main>
38@@ -249,6 +262,27 @@ export function renderStatusPage(snapshot: StatusSnapshot): string {
39 </html>`;
40 }
41 
42+function normalizeStatusPath(value: string | undefined, fallback: string): string {
43+  const normalized = value?.trim();
44+
45+  return normalized == null || normalized === "" ? fallback : normalized;
46+}
47+
48+function normalizeStatusPathList(
49+  value: readonly string[] | undefined,
50+  fallback: readonly string[]
51+): string[] {
52+  const normalized = value
53+    ?.map((entry) => entry.trim())
54+    .filter((entry) => entry !== "");
55+
56+  return normalized == null || normalized.length === 0 ? [...fallback] : normalized;
57+}
58+
59+function renderEndpointList(paths: readonly string[]): string {
60+  return paths.map((path) => `<strong>${escapeHtml(path)}</strong>`).join(" or ");
61+}
62+
63 function renderMetricCard(label: string, value: string, detail: string, accent = false): string {
64   return `<article class="card${accent ? " accent-panel" : ""}">
65     <p class="label">${escapeHtml(label)}</p>
M apps/status-api/src/service.ts
+2, -1
 1@@ -162,7 +162,7 @@ function buildStatusApiDescribeData(options: StatusApiHandlerOptions): Record<st
 2     name: "baa-conductor-status-api",
 3     version: resolveStatusApiVersion(options.version),
 4     description:
 5-      "Read-only status view service. It does not own conductor truth; it renders a narrow status snapshot for humans, browsers, and AI clients.",
 6+      "Read-only compatibility status view service. It does not own conductor truth; new callers should prefer conductor /v1/status while this service preserves the legacy local status-api contract.",
 7     pid: processInfo.pid,
 8     uptime_sec: processInfo.uptimeSec,
 9     cwd: processInfo.cwd,
10@@ -191,6 +191,7 @@ function buildStatusApiDescribeData(options: StatusApiHandlerOptions): Record<st
11     ],
12     notes: [
13       "Status API is read-only.",
14+      "Preferred entry lives on conductor: /v1/status and /v1/status/ui.",
15       "Default truth source comes from BAA_CONDUCTOR_LOCAL_API.",
16       "Use BAA_CONTROL_API_BASE only for legacy ad-hoc compatibility overrides."
17     ]
M apps/worker-runner/package.json
+1, -0
1@@ -5,6 +5,7 @@
2   "main": "dist/index.js",
3   "scripts": {
4     "build": "pnpm exec tsc -p tsconfig.json && BAA_DIST_DIR=apps/worker-runner/dist BAA_DIST_ENTRY=apps/worker-runner/src/index.js BAA_IMPORT_ALIASES='@baa-conductor/logging=../../../packages/logging/src/index.js;@baa-conductor/checkpointing=../../../packages/checkpointing/src/index.js' BAA_FIX_RELATIVE_EXTENSIONS=true pnpm -C ../.. run build:runtime-postprocess",
5+    "test": "pnpm run build && node --test src/index.test.js",
6     "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json"
7   }
8 }
A apps/worker-runner/src/index.test.js
+171, -0
  1@@ -0,0 +1,171 @@
  2+import assert from "node:assert/strict";
  3+import { mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
  4+import { tmpdir } from "node:os";
  5+import { join } from "node:path";
  6+import test from "node:test";
  7+
  8+import {
  9+  prepareStepRun,
 10+  runStep
 11+} from "../dist/index.js";
 12+
 13+async function createFixture(t) {
 14+  const rootDir = await mkdtemp(join(tmpdir(), "baa-worker-runner-"));
 15+  const repoRoot = join(rootDir, "repo");
 16+  const worktreePath = join(rootDir, "worktrees", "task");
 17+  const runsRootDir = join(rootDir, "runs");
 18+
 19+  await mkdir(worktreePath, { recursive: true });
 20+  await mkdir(repoRoot, { recursive: true });
 21+
 22+  t.after(async () => {
 23+    await rm(rootDir, { recursive: true, force: true });
 24+  });
 25+
 26+  return {
 27+    repoRoot,
 28+    worktreePath,
 29+    runsRootDir
 30+  };
 31+}
 32+
 33+function createRequest(runtime, overrides = {}) {
 34+  return {
 35+    taskId: "task-worker-runner",
 36+    stepId: "step-001",
 37+    runId: "run-001",
 38+    attempt: 1,
 39+    stepName: "Prepare local run",
 40+    stepKind: "review",
 41+    workerKind: "shell",
 42+    timeoutSec: 45,
 43+    runtime,
 44+    createdAt: "2026-03-26T00:00:00.000Z",
 45+    checkpoint: {
 46+      mode: "capture",
 47+      logTailLines: 10,
 48+      summaryHint: "Prepared by worker-runner test."
 49+    },
 50+    ...overrides
 51+  };
 52+}
 53+
 54+test("prepareStepRun creates local run layout without checkpoints when disabled", async (t) => {
 55+  const runtime = await createFixture(t);
 56+  const run = await prepareStepRun(
 57+    createRequest(runtime, {
 58+      checkpoint: {
 59+        mode: "disabled"
 60+      }
 61+    })
 62+  );
 63+
 64+  assert.equal(run.metadata.checkpointMode, "disabled");
 65+  assert.equal(run.state.status, "prepared");
 66+  assert.equal(run.state.lastEventSeq, 1);
 67+  assert.equal(run.checkpoint.mode, "disabled");
 68+  assert.deepEqual(run.checkpoint.supportedTypes, []);
 69+  assert.equal(run.checkpoint.records.length, 0);
 70+  assert.deepEqual(
 71+    run.logSession.worker.entries.map((entry) => entry.type),
 72+    ["run_prepared"]
 73+  );
 74+
 75+  const checkpointFiles = await readdir(run.logPaths.checkpointsDir);
 76+  const workerLog = await readFile(run.logPaths.workerLogPath, "utf8");
 77+
 78+  assert.deepEqual(checkpointFiles, []);
 79+  assert.match(workerLog, /"type":"run_prepared"/);
 80+});
 81+
 82+test("runStep default placeholder executor persists capture checkpoints and lifecycle logs", async (t) => {
 83+  const runtime = await createFixture(t);
 84+  const result = await runStep(createRequest(runtime));
 85+
 86+  assert.equal(result.ok, true);
 87+  assert.equal(result.outcome, "prepared");
 88+  assert.equal(result.state.status, "prepared");
 89+  assert.equal(result.metrics.checkpointCount, 2);
 90+  assert.equal(result.checkpoint.records.length, 2);
 91+  assert.equal(result.logSummary.lifecycleEventCount, 6);
 92+  assert.equal(result.logSummary.stdoutChunkCount, 0);
 93+  assert.equal(result.logSummary.stderrChunkCount, 0);
 94+  assert.equal(result.state.checkpointSeq, 2);
 95+  assert.deepEqual(
 96+    result.lifecycleEvents.map((entry) => entry.type),
 97+    [
 98+      "run_prepared",
 99+      "checkpoint_slot_reserved",
100+      "worker_started",
101+      "worker_execution_deferred",
102+      "worker_exited",
103+      "step_prepared"
104+    ]
105+  );
106+  assert.equal(result.checkpoint.records[0]?.type, "summary");
107+  assert.equal(result.checkpoint.records[1]?.type, "log_tail");
108+  assert.match(
109+    result.checkpoint.records[1]?.contentText ?? "",
110+    /\[worker\]/
111+  );
112+  assert.ok(result.artifacts.some((artifact) => artifact.name === "meta.json"));
113+  assert.ok(result.artifacts.some((artifact) => artifact.name === "checkpoints"));
114+
115+  const checkpointFiles = await readdir(result.logPaths.checkpointsDir);
116+  const workerLog = await readFile(result.logPaths.workerLogPath, "utf8");
117+
118+  assert.equal(checkpointFiles.length, 2);
119+  assert.match(workerLog, /"type":"worker_execution_deferred"/);
120+});
121+
122+test("runStep records blocked outcomes, stream logs, and follow-up metadata from a custom executor", async (t) => {
123+  const runtime = await createFixture(t);
124+  const result = await runStep(createRequest(runtime), {
125+    async execute(run) {
126+      return {
127+        ok: false,
128+        outcome: "blocked",
129+        summary: `Manual approval required for ${run.request.stepId}.`,
130+        blocked: true,
131+        needsHuman: true,
132+        stdout: ["stdout line"],
133+        stderr: ["stderr line"],
134+        suggestedFollowup: [
135+          {
136+            stepName: "Ask operator",
137+            stepKind: "planner",
138+            reason: "Approval required before continuing."
139+          }
140+        ],
141+        artifacts: [
142+          {
143+            name: "handoff.txt",
144+            kind: "artifact",
145+            path: join(run.logPaths.artifactsDir, "handoff.txt"),
146+            description: "Operator handoff note."
147+          }
148+        ],
149+        exitCode: 17
150+      };
151+    }
152+  });
153+
154+  assert.equal(result.ok, false);
155+  assert.equal(result.outcome, "blocked");
156+  assert.equal(result.blocked, true);
157+  assert.equal(result.needsHuman, true);
158+  assert.equal(result.state.status, "blocked");
159+  assert.equal(result.state.exitCode, 17);
160+  assert.equal(result.logSummary.stdoutChunkCount, 1);
161+  assert.equal(result.logSummary.stderrChunkCount, 1);
162+  assert.equal(result.lifecycleEvents.at(-1)?.type, "step_blocked");
163+  assert.equal(result.suggestedFollowup.length, 1);
164+  assert.equal(result.suggestedFollowup[0]?.stepKind, "planner");
165+  assert.ok(result.artifacts.some((artifact) => artifact.name === "handoff.txt"));
166+
167+  const stdoutLog = await readFile(result.logPaths.stdoutLogPath, "utf8");
168+  const stderrLog = await readFile(result.logPaths.stderrLogPath, "utf8");
169+
170+  assert.equal(stdoutLog, "stdout line\n");
171+  assert.equal(stderrLog, "stderr line\n");
172+});
M docs/api/README.md
+36, -10
 1@@ -20,7 +20,8 @@
 2 原则:
 3 
 4 - `conductor-daemon` 本地 API 是这些业务接口的真相源
 5-- `status-api` 仍是只读状态视图
 6+- 推荐只读状态视图入口是 `conductor` 上的 `GET /v1/status` 和 `GET /v1/status/ui`
 7+- `status-api` 保留为本地只读状态视图兼容包装层
 8 - Cloudflare Worker / D1 / `control-api.makefile.so` 都只剩 legacy 兼容或残留依赖盘点背景,不再是这批业务接口的主路径
 9 
10 ## 入口
11@@ -28,10 +29,10 @@
12 | 服务 | 地址 | 说明 |
13 | --- | --- | --- |
14 | conductor public host | `https://conductor.makefile.so` | 唯一公网入口;VPS Nginx 回源到同一个 `conductor-daemon` local-api |
15-| conductor-daemon local-api | `BAA_CONDUCTOR_LOCAL_API`,默认可用值如 `http://127.0.0.1:4317` | 本地真相源;承接 describe/health/version/capabilities/browser/system/controllers/tasks/codex/host-ops |
16+| conductor-daemon local-api | `BAA_CONDUCTOR_LOCAL_API`,默认可用值如 `http://127.0.0.1:4317` | 本地真相源;承接 describe/health/version/capabilities/status/browser/system/controllers/tasks/codex/host-ops |
17 | codexd local-api | `BAA_CODEXD_LOCAL_API_BASE`,默认可用值如 `http://127.0.0.1:4319` | 独立 `codexd` 本地服务;支持 `GET /describe` 自描述;`conductor-daemon` 的 `/v1/codex/*` 只代理到这里 |
18 | conductor-daemon local-firefox-ws | 由 `BAA_CONDUCTOR_LOCAL_API` 派生,例如 `ws://127.0.0.1:4317/ws/firefox` | 本地 Firefox 插件双向 bridge;复用同一个 listener,不单独开公网端口 |
19-| status-api local view | `http://127.0.0.1:4318` | 本地只读状态 JSON 和 HTML 视图;默认从 `BAA_CONDUCTOR_LOCAL_API` 的 `/v1/system/state` 取数,不承担公网入口角色 |
20+| status-api local view | `http://127.0.0.1:4318` | 本地只读状态兼容包装层;继续提供 `/describe`、`/v1/status`、`/v1/status/ui` 和 `/ui`,默认从 `BAA_CONDUCTOR_LOCAL_API` 的 `/v1/system/state` 取数,不承担公网入口角色 |
21 
22 ## Describe First
23 
24@@ -39,11 +40,12 @@
25 
26 1. `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/business` 或 `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/control`
27 2. 如有需要,再看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/capabilities`
28-3. 如果是控制动作,再看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/system/state`
29-4. 按需查看 `browser`、`controllers`、`tasks`、`codex`
30-5. 如果要做本机 shell / 文件操作,先读 `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/control` 返回里的 `host_operations`,并准备 `Authorization: Bearer <BAA_SHARED_TOKEN>`
31-6. 只有在明确需要写操作时,再调用 `pause` / `resume` / `drain` 或 `host-ops`
32-7. 只有在明确需要浏览器双向通讯时,再手动连接 `/ws/firefox`
33+3. 如果只需要窄的只读状态视图,先看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/status`
34+4. 如果是控制动作或需要完整 truth shape,再看 `GET ${BAA_CONDUCTOR_LOCAL_API}/v1/system/state`
35+5. 按需查看 `browser`、`controllers`、`tasks`、`codex`
36+6. 如果要做本机 shell / 文件操作,先读 `GET ${BAA_CONDUCTOR_LOCAL_API}/describe/control` 返回里的 `host_operations`,并准备 `Authorization: Bearer <BAA_SHARED_TOKEN>`
37+7. 只有在明确需要写操作时,再调用 `pause` / `resume` / `drain` 或 `host-ops`
38+8. 只有在明确需要浏览器双向通讯时,再手动连接 `/ws/firefox`
39 
40 如果是直接调用 `codexd`:
41 
42@@ -90,6 +92,19 @@
43 - 成功时直接返回最新 system state,而不是只回一个 ack
44 - 这样 HTTP 客户端和 WS `action_request` 都能复用同一份状态合同
45 
46+### 只读状态视图接口
47+
48+| 方法 | 路径 | 说明 |
49+| --- | --- | --- |
50+| `GET` | `/v1/status` | 与 `status-api` 对齐的窄 JSON 状态快照;适合人和 AI 做只读观察 |
51+| `GET` | `/v1/status/ui` | 与 `status-api` 对齐的 HTML 状态面板 |
52+
53+说明:
54+
55+- `/v1/system/state` 仍是控制型 truth shape,不因为兼容状态视图而改合同
56+- 新调用方默认优先走 `conductor` 上的这两个入口
57+- `status-api` 仅在需要保留 `4318` / `/ui` / `/describe` 兼容位次时启用
58+
59 ### Codex 代理接口
60 
61 这些路由固定代理到独立 `codexd`:
62@@ -271,7 +286,12 @@ host-ops 约定:
63 
64 ## Status API
65 
66-`status-api` 仍是本地只读视图服务,不拥有真相,也不承担公网入口角色。
67+`status-api` 现在是本地只读状态兼容包装层,不拥有真相,也不承担公网入口角色。
68+
69+推荐入口:
70+
71+- 新调用方默认使用 `conductor` 上的 `GET /v1/status` 和 `GET /v1/status/ui`
72+- 只有还需要 `4318`、`/describe`、`/ui` 这组 legacy 路径时,才显式启动 `status-api`
73 
74 truth source:
75 
76@@ -279,8 +299,9 @@ truth source:
77 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口;只有本地 `4317` 不可达时才需要显式改到公网
78 - `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 `conductor-daemon` upstream/public API base 的 canonical 变量名;legacy `BAA_CONTROL_API_BASE` 只保留两个兼容点:`conductor-daemon` 把它当别名,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
79 - conductor launchd 安装副本会同时写入 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 legacy `BAA_CONTROL_API_BASE`;默认 launchd 不再给 `status-api` 写入这些变量
80+- runtime 默认安装/启动/检查集合现在只包含 `conductor` 和 `codexd`;`status-api` 只在显式 `--service status-api` 或 `install-mini --with-status-api` 时启用
81 - 如果旧文档或配置里还出现 legacy `control-api.makefile.so`,只能按迁移兼容 / 残留依赖盘点目标理解,绝不再是默认或 canonical truth source
82-- `status-api` 负责把该状态整理成 JSON 或 HTML
83+- `status-api` 和 `conductor /v1/status*` 共享同一套状态拼装和 HTML 渲染语义
84 
85 当前端点:
86 
87@@ -309,6 +330,11 @@ LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
88 curl "${LOCAL_API_BASE}/v1/system/state"
89 ```
90 
91+```bash
92+LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
93+curl "${LOCAL_API_BASE}/v1/status"
94+```
95+
96 ```bash
97 LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://127.0.0.1:4317}"
98 curl "${LOCAL_API_BASE}/v1/tasks?limit=5"
M docs/ops/repo-verification.md
+32, -6
 1@@ -4,8 +4,9 @@
 2 
 3 - `pnpm lint`
 4 - `pnpm test`
 5+- `pnpm smoke`
 6 
 7-它们的目标是给协作者一个在仓库根可重复执行的最小验收口径,而不是替代所有 smoke / runtime / on-node 检查。
 8+它们的目标是给协作者一个在仓库根可重复执行的主线验收口径,而不是替代所有 on-node 检查。
 9 
10 ## `pnpm lint`
11 
12@@ -34,23 +35,48 @@
13 - `@baa-conductor/host-ops`
14 - `@baa-conductor/codex-app-server`
15 - `@baa-conductor/codex-exec`
16+- `@baa-conductor/worker-runner`
17 - `@baa-conductor/status-api`
18 - `@baa-conductor/conductor-daemon`
19 - `@baa-conductor/codexd`
20 
21-脚本会按顺序执行每个包自己的 `pnpm --filter <pkg> test`,这样失败点能直接定位到具体包。`@baa-conductor/status-api` 的包级 `test` 会先执行自身 `build`,再跑 `node --test src/index.test.js`,不依赖人工交互或线上环境。
22+脚本会按顺序执行每个包自己的 `pnpm --filter <pkg> test`,这样失败点能直接定位到具体包。`@baa-conductor/status-api` 和 `@baa-conductor/worker-runner` 的包级 `test` 都会先执行自身 `build`,再跑 `node --test src/index.test.js`,不依赖人工交互或线上环境。
23 
24 刻意不覆盖:
25 
26-- `@baa-conductor/worker-runner` 等当前没有稳定 `test` 脚本的工作区
27+- 仍未提供稳定 `test` 脚本的工作区
28 - 包内 `smoke` 命令
29-- `scripts/runtime/*.sh`、`tests/**/*.mjs` 这类更慢或需要额外运行时条件的端到端检查
30+- `scripts/runtime/*.sh`、`tests/**/*.mjs` 这类更慢或更接近 runtime 的检查
31+
32+## `pnpm smoke`
33+
34+`pnpm smoke` 会顺序执行:
35+
36+1. `pnpm build`
37+2. `node --test scripts/runtime/public-api-base.test.mjs`
38+3. `node --test tests/control-api/control-api-smoke.test.mjs`
39+4. `node --test tests/codexd/codexd-e2e-smoke.test.mjs`
40+5. `node --test tests/browser/browser-control-e2e-smoke.test.mjs`
41+
42+覆盖范围:
43+
44+- runtime 脚本的 conductor public API compatibility 行为
45+- legacy `control-api-worker` / Cloudflare / importer absence 约束
46+- codexd 本地 app-server 会话链路 e2e smoke
47+- browser-control 本地 Firefox bridge e2e smoke
48+
49+说明:
50+
51+- 这条入口比 `pnpm test` 慢,因为会先执行整仓 `build`
52+- 它只覆盖仓库内可自举的 smoke,不替代真实 `mini` 节点上的 launchd / HTTP / port 检查
53 
54 ## 额外检查
55 
56 以下验证仍建议按场景单独执行:
57 
58 - `pnpm typecheck`
59+- `pnpm verify:mini`
60 - `./scripts/runtime/check-node.sh --node mini`
61-- `./scripts/runtime/codexd-e2e-smoke.sh`
62-- `./scripts/runtime/browser-control-e2e-smoke.sh`
63+- `./scripts/runtime/check-launchd.sh --node mini`
64+
65+其中 `pnpm verify:mini` 是 `mini` 节点 on-node 的统一入口:默认检查 `conductor`、`codexd`,并顺序执行 `check-launchd.sh` 与 `check-node.sh --skip-static-check`。需要把可选观察面纳入时,再显式加 `pnpm verify:mini --service status-api`。
M docs/runtime/README.md
+24, -8
 1@@ -18,7 +18,8 @@
 2 - codexd local API: `http://127.0.0.1:4319`
 3 - codexd event stream: `ws://127.0.0.1:4319/v1/codexd/events`
 4 - canonical public host: `https://conductor.makefile.so`
 5-- `status-api` `http://100.71.210.78:4318` 只作为本地只读观察面,默认回源 `BAA_CONDUCTOR_LOCAL_API`,当前 canonical 值是 `http://100.71.210.78:4317`
 6+- `conductor` `http://100.71.210.78:4317` 现在直接提供 `/v1/status` 和 `/v1/status/ui` 只读状态视图
 7+- `status-api` `http://100.71.210.78:4318` 只作为本地只读兼容包装层,默认回源 `BAA_CONDUCTOR_LOCAL_API`
 8 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口
 9 - `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 `conductor` upstream/public API base 的 canonical 变量名;launchd 会连同 legacy `BAA_CONTROL_API_BASE` 一起写给 `conductor`
10 - `status-api` 只保留对 legacy `BAA_CONTROL_API_BASE` 的代码层兼容回退,不再把它当 canonical truth source
11@@ -27,9 +28,9 @@
12 
13 ## 当前正式服务
14 
15-- `conductor`: `launchd` 托管的主控制面,承载 `4317` 本地 API
16+- `conductor`: `launchd` 托管的主控制面,承载 `4317` 本地 API,也承接 `/v1/status` 和 `/v1/status/ui`
17 - `codexd`: `launchd` 托管的独立 Codex 运行面,只走 `codex app-server` 路线,监听 `127.0.0.1:4319`
18-- `status-api`: `launchd` 托管的本地只读观察面,监听 `4318`,默认读取 `4317` 上的 conductor `/v1/system/state`
19+- `status-api`: 可选 `launchd` 本地只读兼容包装层,监听 `4318`,默认读取 `4317` 上的 conductor `/v1/system/state`
20 
21 `codexd` 正式能力面只保留:
22 
23@@ -49,25 +50,40 @@
24 1. `./scripts/runtime/install-mini.sh`
25 2. `./scripts/runtime/status-launchd.sh`
26 3. `./scripts/runtime/restart-launchd.sh`
27-4. `./scripts/runtime/check-node.sh --node mini`
28+4. `./scripts/runtime/verify-mini.sh`
29 5. `./scripts/runtime/codexd-e2e-smoke.sh`
30 6. `./scripts/runtime/browser-control-e2e-smoke.sh`
31 
32+如果还需要保留 `4318` 的 legacy 状态包装层,再显式加:
33+
34+7. `./scripts/runtime/install-mini.sh --with-status-api`
35+8. `./scripts/runtime/status-launchd.sh --service status-api`
36+
37 ## 当前推荐入口
38 
39 - 安装并切到当前正式仓库路径:
40   - `./scripts/runtime/install-mini.sh`
41+- 读取默认只读状态视图:
42+  - `curl "${BAA_CONDUCTOR_LOCAL_API:-http://100.71.210.78:4317}/v1/status"`
43+- 如果旧脚本仍要求 `4318` wrapper,再安装:
44+  - `./scripts/runtime/install-mini.sh --with-status-api`
45 - 查看 `launchd` / HTTP 状态:
46   - `./scripts/runtime/status-launchd.sh`
47-  - 默认同时检查 `conductor`、`codexd`、`status-api`
48+  - 默认同时检查 `conductor`、`codexd`
49+  - 需要确认 `4318` 兼容包装层时,用 `--service status-api`
50 - 停止 / 启动 / 重启:
51   - `./scripts/runtime/stop-launchd.sh`
52   - `./scripts/runtime/start-launchd.sh`
53   - `./scripts/runtime/restart-launchd.sh`
54-  - 默认同时操作 `conductor`、`codexd`、`status-api`
55-  - 需要单独管理时,用 `--service codexd`
56+  - 默认同时操作 `conductor`、`codexd`
57+  - 需要单独管理时,用 `--service codexd`、`--service conductor` 或 `--service status-api`
58 - 节点检查:
59-  - `./scripts/runtime/check-node.sh --node mini`
60+  - `./scripts/runtime/verify-mini.sh`
61+  - 或在仓库根跑 `pnpm verify:mini`
62+  - 默认顺序执行 `check-launchd.sh` + `check-node.sh --skip-static-check`
63+  - 默认只检查 `conductor`、`codexd`
64+  - 需要把 `4318` 兼容包装层纳入检查时,显式加 `--service status-api`
65+  - 需要单独定位静态或运行态问题时,再直接调用底层 `check-launchd.sh` / `check-node.sh`
66 - 会话链路 smoke:
67   - `./scripts/runtime/codexd-e2e-smoke.sh`
68   - 会起临时 `codexd` + `conductor`,覆盖 `codexd status`、`GET /v1/codex`、session create/read、turn create/read,以及 `logs/codexd/**`、`state/codexd/**` 落盘
M docs/runtime/environment.md
+11, -3
 1@@ -121,12 +121,20 @@ Firefox WS 派生规则:
 2   --node mini \
 3   --service conductor \
 4   --service codexd \
 5-  --service status-api \
 6   --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
 7   --local-api-base http://100.71.210.78:4317 \
 8   --local-api-allowed-hosts 100.71.210.78 \
 9-  --codexd-local-api-base http://127.0.0.1:4319 \
10-  --status-api-host 100.71.210.78
11+  --codexd-local-api-base http://127.0.0.1:4319
12 ```
13 
14 默认 mini 安装不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。`--control-api-base` 仍可作为 legacy 兼容别名使用;如果同一轮 CLI 同时给出新旧参数名,`--public-api-base` 优先。脚本现在也只会把这些变量写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
15+
16+如果要安装可选状态观察面,再单独执行:
17+
18+```bash
19+./scripts/runtime/install-launchd.sh \
20+  --repo-dir /Users/george/code/baa-conductor \
21+  --node mini \
22+  --service status-api \
23+  --status-api-host 100.71.210.78
24+```
M docs/runtime/launchd.md
+23, -8
 1@@ -6,7 +6,7 @@
 2 
 3 - `conductor` 由 `launchd` 托管,并承载 canonical local API `http://100.71.210.78:4317`
 4 - `codexd` 由 `launchd` 托管,并作为正式独立 Codex 运行面监听 `http://127.0.0.1:4319`
 5-- `status-api` 随默认安装一起部署,但只作为本地只读观察面,默认从 `BAA_CONDUCTOR_LOCAL_API` 读取 `/v1/system/state`
 6+- `status-api` 是显式 opt-in 的本地只读观察面,默认从 `BAA_CONDUCTOR_LOCAL_API` 读取 `/v1/system/state`
 7 - 工作目录固定到 `/Users/george/code/baa-conductor`
 8 - 通过仓库内脚本统一安装、启动、停止、重启与验证
 9 
10@@ -20,10 +20,16 @@
11 
12 1. 初始化 runtime 目录
13 2. 构建仓库
14-3. 渲染并安装 `conductor` / `codexd` / `status-api` 的 LaunchAgents
15+3. 渲染并安装默认 `conductor` / `codexd` 的 LaunchAgents
16 4. 重启 launchd 服务
17 5. 跑静态检查和节点检查
18 
19+如果还要把本地状态观察面一起装上,用:
20+
21+```bash
22+./scripts/runtime/install-mini.sh --with-status-api
23+```
24+
25 默认会把共享 token 收口到:
26 
27 - `~/.config/baa-conductor/shared-token.txt`
28@@ -78,7 +84,7 @@
29 ./scripts/runtime/restart-launchd.sh
30 ```
31 
32-这些命令默认同时操作 `conductor`、`codexd`、`status-api`。需要单独管理时,用 `--service codexd`、`--service conductor` 或 `--service status-api`。
33+这些命令默认同时操作 `conductor`、`codexd`。需要单独管理可选观察面时,再显式加 `--service status-api`。
34 
35 例如只重启 `codexd`:
36 
37@@ -96,7 +102,7 @@
38 
39 ## 渲染安装副本
40 
41-完整 mini 安装副本示例:
42+默认 mini 安装副本示例:
43 
44 ```bash
45 ./scripts/runtime/install-launchd.sh \
46@@ -104,17 +110,26 @@
47   --node mini \
48   --service conductor \
49   --service codexd \
50-  --service status-api \
51   --install-dir /Users/george/Library/LaunchAgents \
52   --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
53   --local-api-base http://100.71.210.78:4317 \
54   --local-api-allowed-hosts 100.71.210.78 \
55-  --codexd-local-api-base http://127.0.0.1:4319 \
56-  --status-api-host 100.71.210.78
57+  --codexd-local-api-base http://127.0.0.1:4319
58 ```
59 
60 默认 mini 安装不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。`--control-api-base` 仍可作为 legacy 兼容别名使用;如果新旧参数同时出现,`--public-api-base` 优先。脚本现在会把 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 legacy `BAA_CONTROL_API_BASE` 一起写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
61 
62+如果需要本地状态观察面,再显式安装:
63+
64+```bash
65+./scripts/runtime/install-launchd.sh \
66+  --repo-dir /Users/george/code/baa-conductor \
67+  --node mini \
68+  --service status-api \
69+  --install-dir /Users/george/Library/LaunchAgents \
70+  --status-api-host 100.71.210.78
71+```
72+
73 单独安装 `codexd`:
74 
75 ```bash
76@@ -149,7 +164,7 @@
77 
78 - `conductor`: 读取 `BAA_CONDUCTOR_LOCAL_API`
79 - `codexd`: 读取 `BAA_CODEXD_LOCAL_API_BASE`
80-- `status-api`: 读取 `BAA_STATUS_API_HOST` 并按默认端口 `4318` 探活
81+- `status-api`: 只有显式选择时才读取 `BAA_STATUS_API_HOST` 并按默认端口 `4318` 探活
82 
83 mini 正确安装后,建议追加一轮代理验证:
84 
M docs/runtime/node-verification.md
+37, -14
  1@@ -4,11 +4,38 @@
  2 
  3 - `conductor` `http://100.71.210.78:4317`
  4 - `codexd` `http://127.0.0.1:4319`
  5-- `status-api` `http://100.71.210.78:4318`,默认读取 `http://100.71.210.78:4317/v1/system/state`
  6+- `status-api` `http://100.71.210.78:4318`,是显式 opt-in 的本地只读观察面,默认读取 `http://100.71.210.78:4317/v1/system/state`
  7 
  8 其中 `codexd` 的正式产品面仍是 `status / sessions / turn / events`,但 on-node 运维探针只要求 `/healthz` 和 `/v1/codexd/status`。会话级端到端链路单独由仓库 smoke 覆盖。
  9 
 10-## 1. 构建与静态检查
 11+## 1. 推荐入口:统一 mini verify wrapper
 12+
 13+```bash
 14+./scripts/runtime/verify-mini.sh \
 15+  --repo-dir /Users/george/code/baa-conductor \
 16+  --install-dir /Users/george/Library/LaunchAgents \
 17+  --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
 18+  --local-api-base http://100.71.210.78:4317 \
 19+  --local-api-allowed-hosts 100.71.210.78 \
 20+  --codexd-api-base http://127.0.0.1:4319 \
 21+  --expected-rolez leader \
 22+  --check-loaded
 23+```
 24+
 25+这条入口会顺序执行:
 26+
 27+- `check-launchd.sh`
 28+- `check-node.sh --skip-static-check`
 29+
 30+说明:
 31+
 32+- 默认服务集合是 `conductor`、`codexd`
 33+- `status-api` 仍是显式 opt-in;如果已经安装,再额外加 `--service status-api`
 34+- 需要覆盖 `status-api` 地址时,再补 `--status-api-base http://100.71.210.78:4318 --status-api-host 100.71.210.78`
 35+- 如需从仓库根调用,可以用 `pnpm verify:mini --check-loaded`
 36+- 会话级链路 smoke 仍然单独执行,不并入这条 wrapper
 37+
 38+## 2. 底层静态检查
 39 
 40 ```bash
 41 npx --yes pnpm -r build
 42@@ -17,13 +44,11 @@ npx --yes pnpm -r build
 43   --node mini \
 44   --service conductor \
 45   --service codexd \
 46-  --service status-api \
 47   --install-dir /Users/george/Library/LaunchAgents \
 48   --shared-token-file /Users/george/.config/baa-conductor/shared-token.txt \
 49   --local-api-base http://100.71.210.78:4317 \
 50   --local-api-allowed-hosts 100.71.210.78 \
 51-  --codexd-local-api-base http://127.0.0.1:4319 \
 52-  --status-api-host 100.71.210.78
 53+  --codexd-local-api-base http://127.0.0.1:4319
 54 ```
 55 
 56 说明:
 57@@ -33,9 +58,10 @@ npx --yes pnpm -r build
 58 - `check-launchd.sh` 现在会优先校验 `conductor` 安装副本里的 `BAA_CONDUCTOR_PUBLIC_API_BASE`,同时接受只带 legacy `BAA_CONTROL_API_BASE` 的旧安装副本;其他服务要求这两个变量都不存在
 59 - `check-launchd.sh` 现在也会校验 `conductor` 安装副本里的 `BAA_CODEXD_LOCAL_API_BASE`
 60 - `check-launchd.sh` 现在会校验 `codexd` 的监听地址、事件流路径、日志目录、状态目录和 `app-server` child 配置
 61+- 如果 `status-api` 已显式安装,再额外加 `--service status-api --status-api-host 100.71.210.78`
 62 - 这些静态检查不包含 run/exec 路线
 63 
 64-## 2. 运行态检查
 65+## 3. 底层运行态检查
 66 
 67 ```bash
 68 ./scripts/runtime/check-node.sh \
 69@@ -43,13 +69,10 @@ npx --yes pnpm -r build
 70   --node mini \
 71   --service conductor \
 72   --service codexd \
 73-  --service status-api \
 74   --install-dir /Users/george/Library/LaunchAgents \
 75   --local-api-base http://100.71.210.78:4317 \
 76   --local-api-allowed-hosts 100.71.210.78 \
 77   --codexd-api-base http://127.0.0.1:4319 \
 78-  --status-api-base http://100.71.210.78:4318 \
 79-  --status-api-host 100.71.210.78 \
 80   --expected-rolez leader \
 81   --check-loaded
 82 ```
 83@@ -61,11 +84,11 @@ npx --yes pnpm -r build
 84 - `logs/launchd/*.log` 是否存在
 85 - `conductor` 是否监听 `4317` 并返回 `/healthz`、`/readyz`、`/rolez`、`/v1/codex`
 86 - `codexd` 是否监听 `4319` 并返回 `/healthz`、`/v1/codexd/status`
 87-- `status-api` 是否监听 `4318` 并返回 `/healthz`、`/v1/status`
 88-- `status-api /v1/status` 是否能跟随同机 `conductor` 的 `/v1/system/state`
 89+- 如果显式纳入 `--service status-api`,再验证 `status-api` 是否监听 `4318` 并返回 `/healthz`、`/v1/status`
 90+- 如果显式纳入 `--service status-api`,再验证 `status-api /v1/status` 是否能跟随同机 `conductor` 的 `/v1/system/state`
 91 - 不要求探测 run/exec 路线
 92 
 93-## 3. 会话链路 smoke
 94+## 4. 会话链路 smoke
 95 
 96 ```bash
 97 ./scripts/runtime/codexd-e2e-smoke.sh
 98@@ -79,7 +102,7 @@ npx --yes pnpm -r build
 99 - turn create/read
100 - `logs/codexd/**` 和 `state/codexd/**` 落盘
101 
102-## 4. 手工探针
103+## 5. 手工探针
104 
105 主路径:
106 
107@@ -103,7 +126,7 @@ curl -fsSL http://100.71.210.78:4318/v1/status
108 
109 会话链路回归不要直接手工拼 `run/exec` 探针,统一跑 `./scripts/runtime/codexd-e2e-smoke.sh`。
110 
111-## 5. 常见失败点
112+## 6. 常见失败点
113 
114 - `conductor /rolez` 不是 `leader`
115 - `codexd` 没有监听 `127.0.0.1:4319`
M package.json
+2, -0
1@@ -8,6 +8,8 @@
2     "typecheck": "pnpm -r typecheck",
3     "lint": "node ./scripts/verify-workspace.mjs lint",
4     "test": "node ./scripts/verify-workspace.mjs test",
5+    "smoke": "node ./scripts/verify-workspace.mjs smoke",
6+    "verify:mini": "bash ./scripts/runtime/verify-mini.sh",
7     "tasks": "ls tasks"
8   },
9   "devDependencies": {
M plans/STATUS_SUMMARY.md
+9, -5
 1@@ -6,10 +6,11 @@
 2 
 3 ## 当前代码基线
 4 
 5-- 主线基线:`main@7b27e5a`
 6+- 主线基线:`main@d7a83fd`
 7 - 任务文档已统一收口到 `tasks/`
 8 - 当前活动任务见 `tasks/TASK_OVERVIEW.md`
 9 - `T-S001` 到 `T-S012` 已经合入主线
10+- 当前工作区已经落下 `T-S013`、`T-S014`,并继续补根级 `pnpm smoke`
11 
12 ## 当前状态
13 
14@@ -56,11 +57,14 @@
15 - `T-S010`:仓库根 `pnpm lint` / `pnpm test` 已变成真实入口,并新增 repo verification 文档
16 - `T-S011`:`status-api` 已补包级 `test` 入口,并接入根 `pnpm test`
17 - `T-S012`:repo 模板、`install-mini.sh` 帮助文本、`pnpm-lock.yaml` 与 legacy smoke 已进一步收口到当前主线口径
18+- `T-S013`:`worker-runner` 已补包级 `test`,并接入根 `pnpm test`
19+- `T-S014`:runtime 默认服务集合已收口到 `conductor` + `codexd`,`status-api` 改为显式 opt-in
20+- 根级 `pnpm smoke` 已补进工作区,覆盖 runtime public-api compatibility、legacy absence、codexd e2e 和 browser-control e2e smoke
21 
22 ## 下一步任务
23 
24-- `T-S013`:给 `worker-runner` 补包级测试并接入根验证入口
25-- `T-S014`:把 `status-api` 从默认 runtime 服务集合里降为显式 opt-in
26+- `T-S015`:给 `mini` 单节点补统一 on-node verify wrapper
27+- `T-S016`:收口 `status-api` 终局并给 `conductor` 提供兼容状态视图
28 
29 ## 当前仍需关注
30 
31@@ -69,5 +73,5 @@
32 - 仍保留的 `control-api` 命名已经限定在历史任务卡、legacy 测试路径、兼容变量名和外部残留资产说明里;如果未来要继续删旧,需要单独评估文件名和兼容面
33 - 如果未来新增 runtime 测试绕开 `withRuntimeFixture(...)`,同类 listener 泄漏仍可能重新出现
34 - 这次没有改 `ConductorRuntime.stop()` 内部逻辑;如果未来关闭路径本身阻塞,还需要单独补运行时层测试
35-- 根 `pnpm test` 目前还没有覆盖 `worker-runner` 和 runtime / e2e 检查
36-- `status-api` 虽然已经退回本地只读观察面,但 runtime 默认服务集合仍把它当默认安装/启动对象;是否继续保留这个默认位次,尚未收口
37+- 根 `pnpm test` 现在已经覆盖 `worker-runner`;runtime / e2e 检查则由工作区里的 `pnpm smoke` 继续收口
38+- `status-api` 虽然已经退回 opt-in 本地只读观察面,但是否继续保留独立服务、还是并入 `conductor`,尚未收口
M scripts/runtime/check-launchd.sh
+2, -1
 1@@ -40,7 +40,8 @@ Options:
 2   --help                    Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are checked.
 6+  If no service is specified, conductor + codexd are checked.
 7+  Use --service status-api to validate the optional local read-only observer.
 8   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
 9   longer writes conductor public-api base env vars for it.
10   --public-api-base only applies when conductor is part of the checked set.
M scripts/runtime/check-node.sh
+3, -2
 1@@ -40,8 +40,9 @@ Options:
 2   --help                       Show this help text.
 3 
 4 Notes:
 5-  The default runtime check set is conductor + codexd + status-api. Use
 6-  --service to narrow the scope or --all-services to include worker-runner.
 7+  The default runtime check set is conductor + codexd. Use --service status-api
 8+  to include the optional local read-only observer, or --all-services to also
 9+  include worker-runner.
10   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
11   longer writes conductor public-api base env vars for it.
12   conductor HTTP probes include /v1/codex to ensure proxy wiring to codexd.
M scripts/runtime/common.sh
+2, -2
 1@@ -81,11 +81,11 @@ validate_node() {
 2 }
 3 
 4 default_services() {
 5-  printf '%s\n' conductor codexd status-api
 6+  printf '%s\n' conductor codexd
 7 }
 8 
 9 default_node_verification_services() {
10-  printf '%s\n' conductor codexd status-api
11+  printf '%s\n' conductor codexd
12 }
13 
14 all_services() {
M scripts/runtime/install-launchd.sh
+2, -1
 1@@ -37,9 +37,10 @@ Options:
 2   --help                    Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are installed.
 6+  If no service is specified, conductor + codexd are installed.
 7   Use --service codexd to render codexd independently; it does not require a
 8   shared token.
 9+  Use --service status-api to install the optional local read-only observer.
10   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
11   longer writes conductor public-api base env vars for it.
12   --public-api-base only affects conductor install copies, and the default is
M scripts/runtime/install-mini.sh
+25, -14
  1@@ -20,6 +20,7 @@ Options:
  2                             Falls back to legacy
  3                             ~/.config/baa-conductor/control-api-worker.secrets.env
  4                             only if the default file is missing.
  5+  --with-status-api         Also install/restart/check the optional local status-api service.
  6   --skip-build              Skip pnpm build.
  7   --skip-restart            Skip launchd restart.
  8   --skip-check              Skip check-launchd/check-node verification.
  9@@ -29,9 +30,10 @@ Notes:
 10   This is the single-node mini convenience wrapper. It:
 11   1. bootstraps runtime directories
 12   2. builds the repo
 13-  3. installs conductor + codexd + status-api LaunchAgents
 14+  3. installs conductor + codexd LaunchAgents by default
 15   4. restarts them
 16   5. verifies the node
 17+  Add --with-status-api when you also want the optional local read-only observer.
 18   codexd verification only treats /healthz and /v1/codexd/status as install acceptance;
 19   /v1/codexd/runs* and codex exec are not part of the formal runtime contract.
 20 EOF
 21@@ -51,6 +53,7 @@ secrets_env=""
 22 skip_build="0"
 23 skip_restart="0"
 24 skip_check="0"
 25+with_status_api="0"
 26 codexd_api_base="${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}"
 27 status_api_base="http://100.71.210.78:4318"
 28 
 29@@ -80,6 +83,10 @@ while [[ $# -gt 0 ]]; do
 30       skip_build="1"
 31       shift
 32       ;;
 33+    --with-status-api)
 34+      with_status_api="1"
 35+      shift
 36+      ;;
 37     --skip-restart)
 38       skip_restart="1"
 39       shift
 40@@ -178,12 +185,19 @@ if [[ "$skip_build" != "1" ]]; then
 41   )
 42 fi
 43 
 44+install_services=(
 45+  --service conductor
 46+  --service codexd
 47+)
 48+
 49+if [[ "$with_status_api" == "1" ]]; then
 50+  install_services+=(--service status-api)
 51+fi
 52+
 53 run_or_print 0 "${SCRIPT_DIR}/install-launchd.sh" \
 54   --repo-dir "$repo_dir" \
 55   --node mini \
 56-  --service conductor \
 57-  --service codexd \
 58-  --service status-api \
 59+  "${install_services[@]}" \
 60   --install-dir "$install_dir" \
 61   --shared-token-file "$shared_token_file" \
 62   --local-api-base "http://100.71.210.78:4317" \
 63@@ -194,22 +208,21 @@ run_or_print 0 "${SCRIPT_DIR}/install-launchd.sh" \
 64 if [[ "$skip_restart" != "1" ]]; then
 65   run_or_print 0 "${SCRIPT_DIR}/restart-launchd.sh" \
 66     --install-dir "$install_dir" \
 67-    --service conductor \
 68-    --service codexd \
 69-    --service status-api
 70+    "${install_services[@]}"
 71 fi
 72 
 73 if [[ "$skip_check" != "1" ]]; then
 74   wait_for_http "conductor" "http://100.71.210.78:4317/healthz"
 75   wait_for_http "codexd" "${codexd_api_base}/healthz"
 76-  wait_for_http "status-api" "${status_api_base}/healthz"
 77+
 78+  if [[ "$with_status_api" == "1" ]]; then
 79+    wait_for_http "status-api" "${status_api_base}/healthz"
 80+  fi
 81 
 82   run_or_print 0 "${SCRIPT_DIR}/check-launchd.sh" \
 83     --repo-dir "$repo_dir" \
 84     --node mini \
 85-    --service conductor \
 86-    --service codexd \
 87-    --service status-api \
 88+    "${install_services[@]}" \
 89     --install-dir "$install_dir" \
 90     --shared-token-file "$shared_token_file" \
 91     --local-api-base "http://100.71.210.78:4317" \
 92@@ -221,9 +234,7 @@ if [[ "$skip_check" != "1" ]]; then
 93   run_or_print 0 "${SCRIPT_DIR}/check-node.sh" \
 94     --repo-dir "$repo_dir" \
 95     --node mini \
 96-    --service conductor \
 97-    --service codexd \
 98-    --service status-api \
 99+    "${install_services[@]}" \
100     --install-dir "$install_dir" \
101     --shared-token-file "$shared_token_file" \
102     --local-api-base "http://100.71.210.78:4317" \
M scripts/runtime/reload-launchd.sh
+2, -1
 1@@ -22,7 +22,8 @@ Options:
 2   --help                 Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are reloaded.
 6+  If no service is specified, conductor + codexd are reloaded.
 7+  Use --service status-api to reload the optional local read-only observer.
 8   codexd reload recovery waits on /healthz only; /v1/codexd/runs* and
 9   codex exec are not part of reload acceptance.
10 EOF
M scripts/runtime/start-launchd.sh
+2, -1
 1@@ -21,7 +21,8 @@ Options:
 2   --help                 Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are started.
 6+  If no service is specified, conductor + codexd are started.
 7+  Use --service status-api to start the optional local read-only observer.
 8 EOF
 9 }
10 
M scripts/runtime/status-launchd.sh
+2, -1
 1@@ -24,7 +24,8 @@ Options:
 2   --help                 Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are shown.
 6+  If no service is specified, conductor + codexd are shown.
 7+  Use --service status-api to inspect the optional local read-only observer.
 8   conductor HTTP status also reports /v1/codex to surface codexd proxy wiring.
 9   codexd HTTP status only reports /healthz and /v1/codexd/status.
10   /v1/codexd/runs* is not treated as a formal runtime probe.
M scripts/runtime/stop-launchd.sh
+2, -1
 1@@ -21,7 +21,8 @@ Options:
 2   --help                 Show this help text.
 3 
 4 Notes:
 5-  If no service is specified, conductor + codexd + status-api are stopped.
 6+  If no service is specified, conductor + codexd are stopped.
 7+  Use --service status-api to stop the optional local read-only observer.
 8 EOF
 9 }
10 
A scripts/runtime/verify-mini.sh
+119, -0
  1@@ -0,0 +1,119 @@
  2+#!/usr/bin/env bash
  3+set -euo pipefail
  4+
  5+SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
  6+# shellcheck source=./common.sh
  7+source "${SCRIPT_DIR}/common.sh"
  8+
  9+usage() {
 10+  cat <<'EOF'
 11+Usage:
 12+  scripts/runtime/verify-mini.sh [options]
 13+
 14+Options:
 15+  --node mini                  Optional compatibility flag. Only mini is supported.
 16+  --scope agent|daemon         Expected launchd scope. Defaults to agent.
 17+  --service NAME               Add one service to the verify set. Repeatable.
 18+  --all-services               Verify conductor, codexd, worker-runner, and status-api.
 19+  --repo-dir PATH              Repo root used to derive runtime paths.
 20+  --home-dir PATH              HOME value expected in installed plist files.
 21+  --install-dir PATH           Validate installed copies under this directory.
 22+  --shared-token TOKEN         Expect this exact token in installed copies.
 23+  --shared-token-file PATH     Read the expected token from a file.
 24+  --public-api-base URL        Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
 25+  --control-api-base URL       Legacy alias for --public-api-base.
 26+  --local-api-base URL         Conductor local API base URL.
 27+  --local-api-allowed-hosts CSV
 28+                               Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
 29+  --codexd-api-base URL        codexd local API base URL for both static and runtime checks.
 30+  --codexd-local-api-base URL  Alias for --codexd-api-base.
 31+  --codexd-event-stream-path PATH
 32+                               Expected BAA_CODEXD_EVENT_STREAM_PATH.
 33+  --codexd-server-command COMMAND
 34+                               Expected BAA_CODEXD_SERVER_COMMAND.
 35+  --codexd-server-cwd PATH     Expected BAA_CODEXD_SERVER_CWD.
 36+  --status-api-base URL        Status API base URL.
 37+  --status-api-host HOST       Expected BAA_STATUS_API_HOST in installed copies.
 38+  --username NAME              Expected UserName for LaunchDaemons.
 39+  --domain TARGET              launchctl domain target for --check-loaded.
 40+  --check-loaded               Also require launchctl print to succeed for each service.
 41+  --expected-rolez VALUE       Expected conductor /rolez body: leader or any.
 42+  --skip-dist-check            Skip dist/index.js existence checks during static verification.
 43+  --skip-port-check            Skip local TCP LISTEN checks during runtime verification.
 44+  --skip-process-check         Skip host process command-line checks during runtime verification.
 45+  --skip-http-check            Skip conductor/codexd/status-api HTTP probes.
 46+  --skip-log-check             Skip launchd stdout/stderr file checks during runtime verification.
 47+  --help                       Show this help text.
 48+
 49+Notes:
 50+  This wrapper runs check-launchd.sh first, then check-node.sh with
 51+  --skip-static-check so the static pass only runs once.
 52+  If no service is specified, mini defaults to conductor + codexd.
 53+  Use --service status-api to include the optional local read-only observer.
 54+  Use the lower-level scripts directly only when you need to isolate static
 55+  versus runtime failures.
 56+EOF
 57+}
 58+
 59+launchd_args=()
 60+node_args=()
 61+
 62+while [[ $# -gt 0 ]]; do
 63+  case "$1" in
 64+    --)
 65+      shift
 66+      ;;
 67+    --node)
 68+      validate_node "$2"
 69+      shift 2
 70+      ;;
 71+    --scope | --service | --repo-dir | --home-dir | --install-dir | --shared-token | \
 72+    --shared-token-file | --public-api-base | --control-api-base | --local-api-base | \
 73+    --local-api-allowed-hosts | --status-api-host | --username | --domain)
 74+      launchd_args+=("$1" "$2")
 75+      node_args+=("$1" "$2")
 76+      shift 2
 77+      ;;
 78+    --all-services | --check-loaded)
 79+      launchd_args+=("$1")
 80+      node_args+=("$1")
 81+      shift
 82+      ;;
 83+    --codexd-api-base | --codexd-local-api-base)
 84+      launchd_args+=(--codexd-local-api-base "$2")
 85+      node_args+=(--codexd-api-base "$2")
 86+      shift 2
 87+      ;;
 88+    --codexd-event-stream-path | --codexd-server-command | --codexd-server-cwd)
 89+      launchd_args+=("$1" "$2")
 90+      shift 2
 91+      ;;
 92+    --skip-dist-check)
 93+      launchd_args+=("$1")
 94+      shift
 95+      ;;
 96+    --status-api-base | --expected-rolez)
 97+      node_args+=("$1" "$2")
 98+      shift 2
 99+      ;;
100+    --skip-port-check | --skip-process-check | --skip-http-check | --skip-log-check)
101+      node_args+=("$1")
102+      shift
103+      ;;
104+    --help)
105+      usage
106+      exit 0
107+      ;;
108+    *)
109+      die "Unknown option: $1"
110+      ;;
111+  esac
112+done
113+
114+runtime_log "mini verify: static launchd checks"
115+"${SCRIPT_DIR}/check-launchd.sh" --node mini "${launchd_args[@]}"
116+
117+runtime_log "mini verify: runtime checks"
118+"${SCRIPT_DIR}/check-node.sh" --node mini --skip-static-check "${node_args[@]}"
119+
120+runtime_log "mini verify passed"
M scripts/verify-workspace.mjs
+25, -2
 1@@ -11,6 +11,7 @@ const testPackages = [
 2   "@baa-conductor/host-ops",
 3   "@baa-conductor/codex-app-server",
 4   "@baa-conductor/codex-exec",
 5+  "@baa-conductor/worker-runner",
 6   "@baa-conductor/status-api",
 7   "@baa-conductor/conductor-daemon",
 8   "@baa-conductor/codexd"
 9@@ -30,14 +31,36 @@ const entrypoints = {
10   test: testPackages.map((pkg) => ({
11     label: `${pkg} test`,
12     command: ["pnpm", "--filter", pkg, "test"]
13-  }))
14+  })),
15+  smoke: [
16+    {
17+      label: "workspace build",
18+      command: ["pnpm", "build"]
19+    },
20+    {
21+      label: "runtime public API compatibility smoke",
22+      command: ["node", "--test", "scripts/runtime/public-api-base.test.mjs"]
23+    },
24+    {
25+      label: "legacy control-api absence smoke",
26+      command: ["node", "--test", "tests/control-api/control-api-smoke.test.mjs"]
27+    },
28+    {
29+      label: "codexd e2e smoke",
30+      command: ["node", "--test", "tests/codexd/codexd-e2e-smoke.test.mjs"]
31+    },
32+    {
33+      label: "browser-control e2e smoke",
34+      command: ["node", "--test", "tests/browser/browser-control-e2e-smoke.test.mjs"]
35+    }
36+  ]
37 };
38 
39 const mode = process.argv[2];
40 const steps = entrypoints[mode];
41 
42 if (!steps) {
43-  console.error("Usage: node scripts/verify-workspace.mjs <lint|test>");
44+  console.error("Usage: node scripts/verify-workspace.mjs <lint|test|smoke>");
45   process.exit(1);
46 }
47 
A tasks/T-S015.md
+118, -0
  1@@ -0,0 +1,118 @@
  2+# Task T-S015:给 `mini` 单节点补统一 on-node verify wrapper
  3+
  4+## 直接给对话的提示词
  5+
  6+读 `/Users/george/code/baa-conductor/tasks/T-S015.md` 任务文档,完成开发任务。
  7+
  8+如需补背景,再读:
  9+
 10+- `/Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
 11+- `/Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
 12+- `/Users/george/code/baa-conductor/docs/runtime/node-verification.md`
 13+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
 14+- `/Users/george/code/baa-conductor/docs/ops/repo-verification.md`
 15+- `/Users/george/code/baa-conductor/package.json`
 16+
 17+## 当前基线
 18+
 19+- 仓库:`/Users/george/code/baa-conductor`
 20+- 分支:`main`
 21+- 提交:`d7a83fd`
 22+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
 23+
 24+## 建议分支名
 25+
 26+- `chore/add-mini-verify-wrapper`
 27+
 28+## 目标
 29+
 30+把 `mini` 节点的 on-node 静态检查和运行态检查收成一条统一入口,降低运维和回归时的命令记忆成本。
 31+
 32+## 背景
 33+
 34+- 当前仓库根已经有 `pnpm lint`、`pnpm test`、`pnpm smoke`,但这些都属于 repo 内可自举验证。
 35+- 真正的 on-node 检查仍然需要分别记住 `check-launchd.sh` 和 `check-node.sh`,而且要自己拼同一组参数。
 36+- `status-api` 已经降成 opt-in 观察面,新的 wrapper 也需要跟这个默认集合保持一致。
 37+
 38+## 涉及仓库
 39+
 40+- `/Users/george/code/baa-conductor`
 41+
 42+## 范围
 43+
 44+- 新增一条统一的 `mini` on-node verify wrapper
 45+- 让默认检查集合跟当前 runtime 默认服务集合一致
 46+- 回写 runtime / ops 文档和根脚本入口
 47+
 48+## 路径约束
 49+
 50+- 优先复用现有 `check-launchd.sh` 和 `check-node.sh`
 51+- 不要重写已有静态检查或 HTTP 探针逻辑
 52+- 不要把任务扩展成 launchd 安装流程重构
 53+
 54+## 推荐实现边界
 55+
 56+建议优先做:
 57+
 58+- 新增 `scripts/runtime/verify-mini.sh` 或等价 wrapper
 59+- 默认同时跑静态检查和运行态检查
 60+- 提供显式 `--service status-api` 或等价开关,保持 opt-in 观察面能力
 61+
 62+## 允许修改的目录
 63+
 64+- `/Users/george/code/baa-conductor/scripts/runtime/`
 65+- `/Users/george/code/baa-conductor/docs/runtime/`
 66+- `/Users/george/code/baa-conductor/docs/ops/`
 67+- `/Users/george/code/baa-conductor/package.json`
 68+- `/Users/george/code/baa-conductor/README.md`
 69+
 70+## 尽量不要修改
 71+
 72+- `/Users/george/code/baa-conductor/apps/`
 73+- `/Users/george/code/baa-conductor/tasks/`
 74+
 75+## 必须完成
 76+
 77+### 1. 补统一 on-node wrapper
 78+
 79+- 新入口默认覆盖 `conductor` 和 `codexd`
 80+- 调用方不需要再手动拼两套重复参数
 81+
 82+### 2. 保持现有检查脚本职责不变
 83+
 84+- `check-launchd.sh` 和 `check-node.sh` 仍然保留
 85+- wrapper 只是编排,不要复制一份内部逻辑
 86+
 87+### 3. 回写文档和根入口
 88+
 89+- runtime 文档要写清新的推荐 on-node 用法
 90+- 如有必要,根 `package.json` 补一个合适的调用入口
 91+
 92+## 需要特别注意
 93+
 94+- 不要破坏当前 `status-api` opt-in 语义
 95+- 不要把 repo 内 `pnpm smoke` 和 on-node verify 混成一条命令
 96+- 与 `status-api` 终局任务并行时,尽量避免同时大改同一段 runtime 文档
 97+
 98+## 验收标准
 99+
100+- `mini` on-node 检查有统一入口
101+- 默认服务集合与当前 runtime 默认集合一致
102+- 文档和帮助文本已同步
103+- `git diff --check` 通过
104+
105+## 推荐验证命令
106+
107+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/verify-mini.sh`
108+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
109+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
110+- `git -C /Users/george/code/baa-conductor diff --check`
111+
112+## 交付要求
113+
114+完成后请说明:
115+
116+- 新 wrapper 叫什么、默认会跑什么
117+- 默认服务集合是什么
118+- 修改了哪些脚本和文档
119+- 还有哪些 on-node 检查仍需要人工判断
A tasks/T-S016.md
+119, -0
  1@@ -0,0 +1,119 @@
  2+# Task T-S016:收口 `status-api` 终局并给 `conductor` 提供兼容状态视图
  3+
  4+## 直接给对话的提示词
  5+
  6+读 `/Users/george/code/baa-conductor/tasks/T-S016.md` 任务文档,完成开发任务。
  7+
  8+如需补背景,再读:
  9+
 10+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/local-api.ts`
 11+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
 12+- `/Users/george/code/baa-conductor/apps/status-api/src/service.ts`
 13+- `/Users/george/code/baa-conductor/apps/status-api/src/index.test.js`
 14+- `/Users/george/code/baa-conductor/docs/api/README.md`
 15+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
 16+- `/Users/george/code/baa-conductor/README.md`
 17+
 18+## 当前基线
 19+
 20+- 仓库:`/Users/george/code/baa-conductor`
 21+- 分支:`main`
 22+- 提交:`d7a83fd`
 23+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
 24+
 25+## 建议分支名
 26+
 27+- `feat/conductor-status-view-compat`
 28+
 29+## 目标
 30+
 31+把当前只读状态视图进一步收口到 `conductor` 主接口上,同时保留 `status-api` 的兼容位次或明确降级路径。
 32+
 33+## 背景
 34+
 35+- `status-api` 现在已经是显式 opt-in 的本地观察服务,不再属于默认 runtime 套件。
 36+- 但 JSON / HTML 状态视图仍然是单独服务能力,导致状态面仍然分散在两个本地端口上。
 37+- 如果主线要继续收口,最自然的下一步是让 `conductor` 自己也能提供这层只读状态视图。
 38+
 39+## 涉及仓库
 40+
 41+- `/Users/george/code/baa-conductor`
 42+
 43+## 范围
 44+
 45+- 给 `conductor` 增加与当前 `status-api` 对齐的只读状态视图入口
 46+- 决定 `status-api` 保留为兼容包装层,还是进一步降级为 legacy/可删组件
 47+- 回写 API / runtime / README 文档
 48+
 49+## 路径约束
 50+
 51+- 优先复用现有 `status-api` 的取数和渲染逻辑,避免复制两套状态拼装代码
 52+- 不要把任务扩展成重新设计 `/v1/system/state`
 53+- 不要顺手改 runtime 安装脚本默认服务集合,它已经在前一轮任务里收口
 54+
 55+## 推荐实现边界
 56+
 57+建议优先做:
 58+
 59+- 在 `conductor` 上补 `status-api` 等价或明确兼容的状态视图路由
 60+- 保持现有 `status-api` 合同兼容,至少不让现有调用方直接断掉
 61+- 用测试覆盖 JSON 和 HTML 只读视图
 62+
 63+## 允许修改的目录
 64+
 65+- `/Users/george/code/baa-conductor/apps/conductor-daemon/`
 66+- `/Users/george/code/baa-conductor/apps/status-api/`
 67+- `/Users/george/code/baa-conductor/docs/api/`
 68+- `/Users/george/code/baa-conductor/docs/runtime/`
 69+- `/Users/george/code/baa-conductor/README.md`
 70+- `/Users/george/code/baa-conductor/DESIGN.md`
 71+
 72+## 尽量不要修改
 73+
 74+- `/Users/george/code/baa-conductor/scripts/runtime/`
 75+- `/Users/george/code/baa-conductor/tasks/`
 76+
 77+## 必须完成
 78+
 79+### 1. 给 `conductor` 提供兼容状态视图
 80+
 81+- 至少有一组稳定入口能返回当前 JSON 状态视图
 82+- 如保留 HTML 视图,也要说明其定位
 83+
 84+### 2. 保持兼容迁移路径
 85+
 86+- 现有 `status-api` 不应在没有说明的情况下直接消失
 87+- 文档里要明确推荐入口和兼容入口
 88+
 89+### 3. 补测试和文档
 90+
 91+- `conductor-daemon` / `status-api` 至少一侧需要新增或更新测试
 92+- API / runtime / README 口径要一致
 93+
 94+## 需要特别注意
 95+
 96+- 不要破坏现有 `/v1/system/state`
 97+- 不要把只读状态视图误写成新的控制接口
 98+- 与 on-node verify wrapper 任务并行时,尽量避免同时改同一段 README / runtime 文档
 99+
100+## 验收标准
101+
102+- `conductor` 提供可用的兼容状态视图入口
103+- `status-api` 的保留/降级路径清楚且不突兀
104+- 测试通过,文档同步
105+- `git diff --check` 通过
106+
107+## 推荐验证命令
108+
109+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon test`
110+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/status-api test`
111+- `git -C /Users/george/code/baa-conductor diff --check`
112+
113+## 交付要求
114+
115+完成后请说明:
116+
117+- `conductor` 新增了哪些状态视图入口
118+- `status-api` 现在的定位是什么
119+- 修改了哪些测试和文档
120+- 还有哪些兼容风险
M tasks/TASK_OVERVIEW.md
+11, -5
 1@@ -9,7 +9,7 @@
 2 - `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期 legacy 兼容残留和依赖盘点用途
 3 - `baa-hand` / `baa-shell` 只保留为接口语义参考,不再作为主系统维护
 4 - 当前任务卡都放在本目录
 5-- 当前任务基线:`main@7b27e5a`
 6+- 当前任务基线:`main@d7a83fd`
 7 
 8 ## 最近完成任务
 9 
10@@ -27,6 +27,12 @@
11 10. [`T-S010.md`](./T-S010.md):补仓库根验证入口
12 11. [`T-S011.md`](./T-S011.md):把 `status-api` 测试接入根验证入口
13 12. [`T-S012.md`](./T-S012.md):清理 repo 中最后一批 legacy 模板与残留 importer
14+13. [`T-S013.md`](./T-S013.md):给 `worker-runner` 补包级测试并接入根验证入口
15+14. [`T-S014.md`](./T-S014.md):把 `status-api` 从默认 runtime 服务集合里降为显式 opt-in
16+
17+当前工作区还在继续推进:
18+
19+- 根级 `pnpm smoke` 入口,覆盖 repo 内可自举的 runtime compatibility / legacy absence / codexd e2e / browser-control e2e smoke
20 
21 说明:
22 
23@@ -36,13 +42,13 @@
24 
25 围绕剩余技术债,当前建议继续推进这 2 张任务卡:
26 
27-1. [`T-S013.md`](./T-S013.md):给 `worker-runner` 补包级测试并接入根验证入口
28-2. [`T-S014.md`](./T-S014.md):把 `status-api` 从默认 runtime 服务集合里降为显式 opt-in
29+1. [`T-S015.md`](./T-S015.md):给 `mini` 单节点补统一 on-node verify wrapper
30+2. [`T-S016.md`](./T-S016.md):收口 `status-api` 终局并给 `conductor` 提供兼容状态视图
31 
32 说明:
33 
34-- `T-S013` 主要改 `apps/worker-runner/`、根验证脚本和 repo verification 文档
35-- `T-S014` 主要改 `scripts/runtime/**`、`docs/runtime/**`、`README` 和少量 launchd 模板
36+- `T-S015` 主要改 `scripts/runtime/**`、`docs/runtime/**`、`docs/ops/**` 和根脚本入口
37+- `T-S016` 主要改 `apps/conductor-daemon/`、`apps/status-api/`、`docs/api/**`、`docs/runtime/**`
38 
39 ## 任务文档约定
40