baa-conductor

git clone 

commit
097621a
parent
d1c9090
author
im_wower
date
2026-03-26 00:12:46 +0800 CST
merge: land T-S009 and T-S010
22 files changed,  +812, -94
M DESIGN.md
+29, -21
 1@@ -20,23 +20,26 @@
 2 - Tailscale IP: `100.71.210.78`
 3 - canonical local base: `http://100.71.210.78:4317`
 4 - 当前 conductor 已暴露:
 5+  - `/describe`
 6+  - `/health`
 7+  - `/version`
 8   - `/healthz`
 9   - `/readyz`
10   - `/rolez`
11   - `/v1/runtime`
12-
13-后续要继续并到同一接口面的能力:
14-
15-- `/describe`
16-- `/version`
17-- `/health`
18-- `/v1/capabilities`
19-- `/v1/system/state`
20-- `/v1/controllers`
21-- `/v1/tasks`
22-- `/v1/runs`
23-- `pause / resume / drain`
24-- 来自 `baa-shell` 语义的小型本机能力
25+  - `/v1/capabilities`
26+  - `/v1/system/state`
27+  - `/v1/controllers`
28+  - `/v1/tasks`
29+  - `/v1/runs`
30+  - `pause / resume / drain`
31+  - `/v1/exec`
32+  - `/v1/files/read`
33+  - `/v1/files/write`
34+  - `/v1/browser/*`
35+  - `/v1/codex*`(配置了 `codexd` 时)
36+
37+后续如还要扩展,默认继续往这同一个接口面收口,不再回到 legacy control plane。
38 
39 ### 公网入口
40 
41@@ -80,10 +83,11 @@
42 ### `apps/status-api`
43 
44 - `mini` 本地状态读取面
45-- 当前仍依赖 `BAA_CONTROL_API_BASE`
46+- 默认读取 `BAA_CONDUCTOR_LOCAL_API` 对应的 conductor `/v1/system/state`
47+- `BAA_CONTROL_API_BASE` 只剩手工兼容回退语义
48 - 定位是迁移期本地只读观察服务,不是主控制面
49 
50-### `codexd`(设计中,尚未完整实现)
51+### `codexd`(已实现独立 daemon,仍在继续收口)
52 
53 - `mini` 本地常驻的独立 Codex 代理/执行组件
54 - 不是 TUI,不要求人工盯界面
55@@ -109,8 +113,10 @@
56 当前状态:
57 
58 - 仓库里已经有 `codex` step kind、planner 和 step template 抽象
59-- `apps/codexd` 已有 daemon scaffold,但还没有完成 conductor 集成
60-- 所以 `codexd` 现在仍是半成品,不是已落地组件
61+- `apps/codexd` 已有独立 daemon、本地 HTTP 面、event stream、session / turn 状态与最近事件缓存
62+- `conductor-daemon` 已能通过本地 `/v1/codex*` 代理到 `codexd`
63+- transport 尾包冲刷和“未收到合法 completed 就提前断流”的 failure classification 已落地
64+- 但 `codexd` 仍不是已上线、已承诺稳定性的组件
65 - 正式产品口径只保留 session / turn / status,不把 `runs` / `exec` 当作 Codex 能力面
66 - 后续实现时,不再把 `exec` 当作主双工方案,也不再把它做成正式接口
67 - 原因:`codex exec` 卡顿明显且容易假死,不符合 `codexd` 的常驻双工目标
68@@ -162,8 +168,9 @@
69 
70 说明:
71 
72-- 当前脚本仍保留 `BAA_CONTROL_API_BASE` 参数以兼容 `status-api`
73-- 这不是 canonical 主路径,只是删旧前的过渡残留
74+- 当前脚本仍保留 legacy 参数名 `--control-api-base` / `BAA_CONTROL_API_BASE`
75+- 这套名字现在只影响 `conductor` 自己的 upstream/public API base,不再用于 `status-api`
76+- 这不是 canonical 主路径,只是删旧前尚未改名的过渡残留
77 
78 ## 7. Nginx 与 DNS
79 
80@@ -211,10 +218,11 @@
81 
82 ## 11. 当前已知残留
83 
84-- `status-api` 仍通过 `BAA_CONTROL_API_BASE` 读取 legacy truth source
85+- `conductor-daemon` 和 runtime 脚本仍使用 legacy 名字 `BAA_CONTROL_API_BASE` / `--control-api-base`
86+- `status-api` 仍是独立的本地只读观察面,是否并入 `conductor-daemon` 尚未定案
87 - `conductor.makefile.so` 目前只回源 `conductor-daemon` 已有路由,尚未承接完整业务 API
88 - 线上和历史文档里仍可能残留 `control-api.makefile.so`、Cloudflare / D1 相关资产或表述
89-- `codexd` 尚未实现;当前仍没有“Codex 常驻代理 / Codex daemon”能力
90+- 如果后续再次出现 app-server 未发出合法 `turn/completed` 就提前断流,应视为新的 child / transport 故障,而不是 reopen 已修复的 BUG-008 / BUG-010
91 
92 ## 12. 历史回溯
93 
M README.md
+5, -4
 1@@ -72,7 +72,6 @@ scripts/
 2   runtime/
 3 plans/
 4   STATUS_SUMMARY.md
 5-  WORKFLOW.md
 6 tasks/
 7   TASK_OVERVIEW.md
 8 bugs/
 9@@ -121,12 +120,14 @@ legacy 兼容说明:
10 
11 - 保持 `mini` launchd、自启动和本地探针稳定
12 - 保持 `conductor.makefile.so -> 100.71.210.78:4317` 的链路稳定
13-- 推进 local-api cutover,清掉 legacy control plane 依赖
14+- 继续收掉剩余 legacy 配置名和兼容入口
15+- 给仓库根补齐真实可跑的验证入口
16 
17 ## 当前已知 gap
18 
19-- `status-api`、launchd 模板和部分运行脚本仍引用 `BAA_CONTROL_API_BASE`
20-- `status-api` 还需要完全切到 `conductor-daemon local-api /v1/system/state`
21+- `conductor-daemon`、launchd 安装/检查脚本仍用 legacy 名字 `BAA_CONTROL_API_BASE` / `--control-api-base` 表示 conductor upstream/public API base
22+- 仓库根 `pnpm lint` / `pnpm test` 还是占位脚本,缺少统一质量入口
23+- `status-api` 已经默认读取 `conductor-daemon` 的 `/v1/system/state`,但仍是单独的本地只读观察服务;是否继续保留还是并入主接口,仍待后续任务决定
24 
25 ## 本机能力层
26 
M apps/conductor-daemon/src/index.test.js
+50, -2
 1@@ -1113,7 +1113,7 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
 2     BAA_NODE_ID: "mini-main",
 3     BAA_CONDUCTOR_HOST: "mini",
 4     BAA_CONDUCTOR_ROLE: "primary",
 5-    BAA_CONTROL_API_BASE: "https://control.example.test/",
 6+    BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
 7     BAA_CODEXD_LOCAL_API_BASE: "http://127.0.0.1:4323/",
 8     BAA_CONDUCTOR_LOCAL_API: "http://127.0.0.1:4317/",
 9     BAA_SHARED_TOKEN: "replace-me",
10@@ -1129,12 +1129,60 @@ test("parseConductorCliRequest merges launchd env defaults with CLI overrides",
11   assert.equal(request.runOnce, true);
12   assert.equal(request.config.role, "standby");
13   assert.equal(request.config.nodeId, "mini-main");
14-  assert.equal(request.config.controlApiBase, "https://control.example.test");
15+  assert.equal(request.config.publicApiBase, "https://public.example.test");
16+  assert.equal(request.config.controlApiBase, "https://public.example.test");
17   assert.equal(request.config.codexdLocalApiBase, "http://127.0.0.1:4323");
18   assert.equal(request.config.localApiBase, "http://127.0.0.1:4317");
19   assert.equal(request.config.paths.runsDir, "/tmp/runs");
20 });
21 
22+test("parseConductorCliRequest prefers the canonical public API env name over the legacy alias", () => {
23+  const request = parseConductorCliRequest(["config"], {
24+    BAA_NODE_ID: "mini-main",
25+    BAA_CONDUCTOR_HOST: "mini",
26+    BAA_CONDUCTOR_ROLE: "primary",
27+    BAA_CONDUCTOR_PUBLIC_API_BASE: "https://public.example.test/",
28+    BAA_CONTROL_API_BASE: "https://legacy.example.test/",
29+    BAA_SHARED_TOKEN: "replace-me"
30+  });
31+
32+  assert.equal(request.action, "config");
33+
34+  if (request.action !== "config") {
35+    throw new Error("expected config action");
36+  }
37+
38+  assert.equal(request.config.publicApiBase, "https://public.example.test");
39+  assert.equal(request.config.controlApiBase, "https://public.example.test");
40+});
41+
42+test("parseConductorCliRequest prefers --public-api-base over --control-api-base", () => {
43+  const request = parseConductorCliRequest(
44+    [
45+      "config",
46+      "--control-api-base",
47+      "https://legacy-cli.example.test/",
48+      "--public-api-base",
49+      "https://public-cli.example.test/"
50+    ],
51+    {
52+      BAA_NODE_ID: "mini-main",
53+      BAA_CONDUCTOR_HOST: "mini",
54+      BAA_CONDUCTOR_ROLE: "primary",
55+      BAA_SHARED_TOKEN: "replace-me"
56+    }
57+  );
58+
59+  assert.equal(request.action, "config");
60+
61+  if (request.action !== "config") {
62+    throw new Error("expected config action");
63+  }
64+
65+  assert.equal(request.config.publicApiBase, "https://public-cli.example.test");
66+  assert.equal(request.config.controlApiBase, "https://public-cli.example.test");
67+});
68+
69 test("parseConductorCliRequest allows an explicitly listed Tailscale local API host", () => {
70   const request = parseConductorCliRequest(["config"], {
71     BAA_NODE_ID: "mini-main",
M apps/conductor-daemon/src/index.ts
+71, -20
  1@@ -101,7 +101,8 @@ export interface ConductorConfig {
  2   nodeId: string;
  3   host: string;
  4   role: ConductorRole;
  5-  controlApiBase: string;
  6+  publicApiBase?: string;
  7+  controlApiBase?: string;
  8   priority?: number;
  9   version?: string | null;
 10   preferred?: boolean;
 11@@ -128,7 +129,9 @@ export interface ConductorRuntimeConfig extends ConductorConfig {
 12   sharedToken?: string | null;
 13 }
 14 
 15-export interface ResolvedConductorRuntimeConfig extends ConductorConfig {
 16+export interface ResolvedConductorRuntimeConfig
 17+  extends Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> {
 18+  controlApiBase: string;
 19   heartbeatIntervalMs: number;
 20   leaseRenewIntervalMs: number;
 21   localApiAllowedHosts: string[];
 22@@ -138,6 +141,7 @@ export interface ResolvedConductorRuntimeConfig extends ConductorConfig {
 23   paths: ConductorRuntimePaths;
 24   preferred: boolean;
 25   priority: number;
 26+  publicApiBase: string;
 27   renewFailureThreshold: number;
 28   sharedToken: string | null;
 29   version: string | null;
 30@@ -320,6 +324,7 @@ interface CliValueOverrides {
 31   nodeId?: string;
 32   preferred?: boolean;
 33   priority?: string;
 34+  publicApiBase?: string;
 35   renewFailureThreshold?: string;
 36   role?: string;
 37   runOnce: boolean;
 38@@ -1046,7 +1051,7 @@ export class ConductorDaemon {
 39   private readonly autoStartLoops: boolean;
 40   private readonly clearIntervalImpl: (handle: TimerHandle) => void;
 41   private readonly client: ConductorControlApiClient;
 42-  private readonly config: ConductorConfig;
 43+  private readonly config: CanonicalConductorConfig;
 44   private heartbeatTimer: TimerHandle | null = null;
 45   private readonly hooks?: ConductorDaemonHooks;
 46   private leaseTimer: TimerHandle | null = null;
 47@@ -1065,13 +1070,23 @@ export class ConductorDaemon {
 48   };
 49 
 50   constructor(config: ConductorConfig, options: ConductorDaemonOptions = {}) {
 51-    this.config = config;
 52+    const publicApiBase = resolveConfiguredPublicApiBase(config);
 53+
 54+    if (!publicApiBase) {
 55+      throw new Error("Conductor config requires a non-empty publicApiBase.");
 56+    }
 57+
 58+    this.config = {
 59+      ...config,
 60+      controlApiBase: normalizeBaseUrl(publicApiBase),
 61+      publicApiBase: normalizeBaseUrl(publicApiBase)
 62+    };
 63     this.autoStartLoops = options.autoStartLoops ?? true;
 64     this.clearIntervalImpl =
 65       options.clearIntervalImpl ?? ((handle) => globalThis.clearInterval(handle));
 66     this.client =
 67       options.client ??
 68-      createFetchControlApiClient(config.controlApiBase, {
 69+      createFetchControlApiClient(this.config.publicApiBase, {
 70         bearerToken: options.controlApiBearerToken,
 71         fetchImpl: options.fetchImpl
 72       });
 73@@ -1079,7 +1094,7 @@ export class ConductorDaemon {
 74     this.now = options.now ?? defaultNowUnixSeconds;
 75     this.setIntervalImpl =
 76       options.setIntervalImpl ?? ((handler, intervalMs) => globalThis.setInterval(handler, intervalMs));
 77-    this.startedAt = config.startedAt ?? this.now();
 78+    this.startedAt = this.config.startedAt ?? this.now();
 79   }
 80 
 81   getStartupChecklist(): StartupChecklistItem[] {
 82@@ -1380,12 +1395,35 @@ function resolvePathConfig(paths?: Partial<ConductorRuntimePaths>): ConductorRun
 83   };
 84 }
 85 
 86+function resolveConfiguredPublicApiBase(
 87+  values: Pick<ConductorConfig, "controlApiBase" | "publicApiBase">
 88+): string | null {
 89+  return normalizeOptionalString(values.publicApiBase ?? values.controlApiBase);
 90+}
 91+
 92+function resolveSourcePublicApiBase(
 93+  env: ConductorEnvironment,
 94+  overrides: CliValueOverrides
 95+): string | null {
 96+  return normalizeOptionalString(
 97+    overrides.publicApiBase ??
 98+      overrides.controlApiBase ??
 99+      env.BAA_CONDUCTOR_PUBLIC_API_BASE ??
100+      env.BAA_CONTROL_API_BASE
101+  );
102+}
103+
104+type CanonicalConductorConfig = Omit<ConductorConfig, "controlApiBase" | "publicApiBase"> & {
105+  controlApiBase: string;
106+  publicApiBase: string;
107+};
108+
109 export function resolveConductorRuntimeConfig(
110   config: ConductorRuntimeConfig
111 ): ResolvedConductorRuntimeConfig {
112   const nodeId = normalizeOptionalString(config.nodeId);
113   const host = normalizeOptionalString(config.host);
114-  const controlApiBase = normalizeOptionalString(config.controlApiBase);
115+  const publicApiBase = resolveConfiguredPublicApiBase(config);
116 
117   if (!nodeId) {
118     throw new Error("Conductor config requires a non-empty nodeId.");
119@@ -1395,8 +1433,8 @@ export function resolveConductorRuntimeConfig(
120     throw new Error("Conductor config requires a non-empty host.");
121   }
122 
123-  if (!controlApiBase) {
124-    throw new Error("Conductor config requires a non-empty controlApiBase.");
125+  if (!publicApiBase) {
126+    throw new Error("Conductor config requires a non-empty publicApiBase.");
127   }
128 
129   const heartbeatIntervalMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
130@@ -1427,7 +1465,7 @@ export function resolveConductorRuntimeConfig(
131     nodeId,
132     host,
133     role: parseConductorRole("Conductor role", config.role),
134-    controlApiBase: normalizeBaseUrl(controlApiBase),
135+    controlApiBase: normalizeBaseUrl(publicApiBase),
136     codexdLocalApiBase: resolveLocalApiBase(config.codexdLocalApiBase),
137     heartbeatIntervalMs,
138     leaseRenewIntervalMs,
139@@ -1437,6 +1475,7 @@ export function resolveConductorRuntimeConfig(
140     paths: resolvePathConfig(config.paths),
141     preferred: config.preferred ?? config.role === "primary",
142     priority,
143+    publicApiBase: normalizeBaseUrl(publicApiBase),
144     renewFailureThreshold,
145     sharedToken: normalizeOptionalString(config.sharedToken),
146     version: normalizeOptionalString(config.version)
147@@ -1468,18 +1507,20 @@ function resolveRuntimeConfigFromSources(
148   const host = normalizeOptionalString(overrides.host ?? env.BAA_CONDUCTOR_HOST) ?? "localhost";
149   const nodeId =
150     normalizeOptionalString(overrides.nodeId ?? env.BAA_NODE_ID) ?? createDefaultNodeId(host, role);
151-  // Keep reading the legacy env name until conductor-daemon gets a dedicated upstream base setting.
152-  const controlApiBase = normalizeOptionalString(overrides.controlApiBase ?? env.BAA_CONTROL_API_BASE);
153+  const publicApiBase = resolveSourcePublicApiBase(env, overrides);
154 
155-  if (!controlApiBase) {
156-    throw new Error("Missing control API base URL. Use --control-api-base or BAA_CONTROL_API_BASE.");
157+  if (!publicApiBase) {
158+    throw new Error(
159+      "Missing conductor public API base URL. Use --public-api-base or BAA_CONDUCTOR_PUBLIC_API_BASE. Legacy aliases: --control-api-base / BAA_CONTROL_API_BASE."
160+    );
161   }
162 
163   return resolveConductorRuntimeConfig({
164     nodeId,
165     host,
166     role,
167-    controlApiBase,
168+    publicApiBase,
169+    controlApiBase: publicApiBase,
170     priority: parseIntegerValue("Conductor priority", overrides.priority ?? env.BAA_CONDUCTOR_PRIORITY, {
171       minimum: 0
172     }),
173@@ -1579,6 +1620,10 @@ export function parseConductorCliRequest(
174         overrides.role = readOptionValue(tokens, token, index);
175         index += 1;
176         break;
177+      case "--public-api-base":
178+        overrides.publicApiBase = readOptionValue(tokens, token, index);
179+        index += 1;
180+        break;
181       case "--control-api-base":
182         overrides.controlApiBase = readOptionValue(tokens, token, index);
183         index += 1;
184@@ -1721,7 +1766,7 @@ function formatChecklistText(checklist: StartupChecklistItem[]): string {
185 function formatConfigText(config: ResolvedConductorRuntimeConfig): string {
186   return [
187     `identity: ${config.nodeId}@${config.host}(${config.role})`,
188-    `control_api_base: ${config.controlApiBase}`,
189+    `public_api_base: ${config.publicApiBase}`,
190     `codexd_local_api_base: ${config.codexdLocalApiBase ?? "not-configured"}`,
191     `local_api_base: ${config.localApiBase ?? "not-configured"}`,
192     `firefox_ws_url: ${buildFirefoxWebSocketUrl(config.localApiBase) ?? "not-configured"}`,
193@@ -1766,7 +1811,8 @@ function getUsageText(): string {
194     "  --node-id <id>",
195     "  --host <host>",
196     "  --role <primary|standby>",
197-    "  --control-api-base <url>  conductor upstream/public API base",
198+    "  --public-api-base <url>   conductor upstream/public API base",
199+    "  --control-api-base <url>  legacy alias for --public-api-base",
200     "  --codexd-local-api <url>",
201     "  --local-api <url>",
202     "  --shared-token <token>",
203@@ -1790,7 +1836,8 @@ function getUsageText(): string {
204     "  BAA_NODE_ID",
205     "  BAA_CONDUCTOR_HOST",
206     "  BAA_CONDUCTOR_ROLE",
207-    "  BAA_CONTROL_API_BASE (legacy env name for conductor upstream/public API base)",
208+    "  BAA_CONDUCTOR_PUBLIC_API_BASE",
209+    "  BAA_CONTROL_API_BASE (legacy env alias for conductor upstream/public API base)",
210     "  BAA_CODEXD_LOCAL_API_BASE",
211     "  BAA_CONDUCTOR_LOCAL_API",
212     "  BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS",
213@@ -1806,7 +1853,11 @@ function getUsageText(): string {
214     "  BAA_WORKTREES_DIR",
215     "  BAA_LOGS_DIR",
216     "  BAA_TMP_DIR",
217-    "  BAA_STATE_DIR"
218+    "  BAA_STATE_DIR",
219+    "",
220+    "Precedence:",
221+    "  CLI values override environment values.",
222+    "  --public-api-base / BAA_CONDUCTOR_PUBLIC_API_BASE override legacy control-api aliases."
223   ].join("\n");
224 }
225 
226@@ -1893,7 +1944,7 @@ export class ConductorRuntime {
227       loops: this.daemon.getLoopStatus(),
228       paths: { ...this.config.paths },
229       controlApi: {
230-        baseUrl: this.config.controlApiBase,
231+        baseUrl: this.config.publicApiBase,
232         firefoxWsUrl,
233         localApiBase,
234         hasSharedToken: this.config.sharedToken != null,
M docs/api/README.md
+2, -2
 1@@ -277,8 +277,8 @@ truth source:
 2 
 3 - 当前默认应优先回源 `BAA_CONDUCTOR_LOCAL_API`,也就是当前 canonical local API `http://100.71.210.78:4317/v1/system/state`
 4 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口;只有本地 `4317` 不可达时才需要显式改到公网
 5-- `BAA_CONTROL_API_BASE` 只保留两个兼容点:`conductor-daemon` 仍读取这个历史变量名作为 upstream/public API base,`status-api` 只在手工或旧配置缺少 `BAA_CONDUCTOR_LOCAL_API` 时回退使用
 6-- 默认 launchd 不再给 `status-api` 写入 `BAA_CONTROL_API_BASE`
 7+- `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` 时回退使用
 8+- conductor launchd 安装副本会同时写入 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 legacy `BAA_CONTROL_API_BASE`;默认 launchd 不再给 `status-api` 写入这些变量
 9 - 如果旧文档或配置里还出现 legacy `control-api.makefile.so`,只能按迁移兼容 / 残留依赖盘点目标理解,绝不再是默认或 canonical truth source
10 - `status-api` 负责把该状态整理成 JSON 或 HTML
11 
M docs/ops/README.md
+4, -0
 1@@ -11,6 +11,10 @@
 2 
 3 主备切换、直连 `mac` 的公网域名和历史切换 runbook 已从当前主线移除。
 4 
 5+仓库根验证入口说明见:
 6+
 7+- [`repo-verification.md`](./repo-verification.md): `pnpm lint` / `pnpm test` 的覆盖范围与边界
 8+
 9 ## 当前 inventory
10 
11 使用一份最小 inventory:
A docs/ops/repo-verification.md
+55, -0
 1@@ -0,0 +1,55 @@
 2+# Repo Verification
 3+
 4+仓库根提供两条默认验证入口:
 5+
 6+- `pnpm lint`
 7+- `pnpm test`
 8+
 9+它们的目标是给协作者一个在仓库根可重复执行的最小验收口径,而不是替代所有 smoke / runtime / on-node 检查。
10+
11+## `pnpm lint`
12+
13+`pnpm lint` 依次执行:
14+
15+1. `pnpm typecheck`
16+2. `git diff --check`
17+
18+覆盖范围:
19+
20+- `pnpm typecheck` 会跑 `pnpm-workspace.yaml` 里的全部 workspace 包级 `typecheck`
21+- `git diff --check` 负责收口常见 repo hygiene 问题,例如尾随空格或冲突标记
22+
23+刻意不覆盖:
24+
25+- ESLint / Prettier 之类的新 lint 框架
26+- 需要启动服务或依赖外部环境的 smoke / runtime 检查
27+
28+如果只想单独跑 TypeScript 静态检查,直接执行 `pnpm typecheck` 即可。
29+
30+## `pnpm test`
31+
32+`pnpm test` 当前收口到已有稳定 `test` 脚本的关键包:
33+
34+- `@baa-conductor/db`
35+- `@baa-conductor/host-ops`
36+- `@baa-conductor/codex-app-server`
37+- `@baa-conductor/codex-exec`
38+- `@baa-conductor/conductor-daemon`
39+- `@baa-conductor/codexd`
40+
41+脚本会按顺序执行每个包自己的 `pnpm --filter <pkg> test`,这样失败点能直接定位到具体包。
42+
43+刻意不覆盖:
44+
45+- `apps/status-api`、`apps/worker-runner` 等当前没有稳定 `test` 脚本的工作区
46+- 包内 `smoke` 命令
47+- `scripts/runtime/*.sh`、`tests/**/*.mjs` 这类更慢或需要额外运行时条件的端到端检查
48+
49+## 额外检查
50+
51+以下验证仍建议按场景单独执行:
52+
53+- `pnpm typecheck`
54+- `./scripts/runtime/check-node.sh --node mini`
55+- `./scripts/runtime/codexd-e2e-smoke.sh`
56+- `./scripts/runtime/browser-control-e2e-smoke.sh`
M docs/runtime/README.md
+2, -1
 1@@ -20,7 +20,8 @@
 2 - canonical public host: `https://conductor.makefile.so`
 3 - `status-api` `http://100.71.210.78:4318` 只作为本地只读观察面,默认回源 `BAA_CONDUCTOR_LOCAL_API`,当前 canonical 值是 `http://100.71.210.78:4317`
 4 - `https://conductor.makefile.so` 是同一套 conductor 主接口的公网入口
 5-- 默认 launchd 只把 legacy 变量名 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 只保留代码层兼容回退,不再把它当 canonical truth source
 6+- `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 `conductor` upstream/public API base 的 canonical 变量名;launchd 会连同 legacy `BAA_CONTROL_API_BASE` 一起写给 `conductor`
 7+- `status-api` 只保留对 legacy `BAA_CONTROL_API_BASE` 的代码层兼容回退,不再把它当 canonical truth source
 8 - 推荐仓库路径:`/Users/george/code/baa-conductor`
 9 - repo 内的 plist 只作为模板;真正加载的是脚本渲染出来的安装副本
10 
M docs/runtime/environment.md
+10, -5
 1@@ -9,7 +9,8 @@
 2 - canonical public host: `https://conductor.makefile.so`
 3 - local status view: `http://100.71.210.78:4318`
 4 - `status-api` 默认真相源:`BAA_CONDUCTOR_LOCAL_API` -> `http://100.71.210.78:4317/v1/system/state`
 5-- legacy 变量名 `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 运行时仍用它解析 upstream/public API base,`status-api` 只在手工兼容场景下回退读取
 6+- `conductor` upstream/public API base 的 canonical 变量名是 `BAA_CONDUCTOR_PUBLIC_API_BASE`
 7+- legacy 变量名 `BAA_CONTROL_API_BASE` 只剩两个保留点:`conductor` 把它当兼容别名,`status-api` 只在手工兼容场景下回退读取
 8 
 9 ## 共享变量
10 
11@@ -24,13 +25,16 @@
12 
13 - `codexd` 独立安装时不要求 `BAA_SHARED_TOKEN`
14 
15-## 兼容变量
16+## conductor public API 变量
17 
18+- `BAA_CONDUCTOR_PUBLIC_API_BASE`
19 - `BAA_CONTROL_API_BASE`
20 
21 说明:
22 
23-- 变量名本身是历史兼容名,但 `conductor-daemon` 当前仍用它解析 upstream/public API base;默认 launchd 只把它写给 `conductor`
24+- `BAA_CONDUCTOR_PUBLIC_API_BASE` 是 canonical 配置名,`BAA_CONTROL_API_BASE` 是 legacy 兼容别名
25+- 如果两个 env 同时存在,`BAA_CONDUCTOR_PUBLIC_API_BASE` 优先
26+- launchd 安装脚本会把这两个名字都写给 `conductor` 安装副本,避免新旧副本/脚本混跑时失配
27 - `status-api` 现在会优先读取 `BAA_CONDUCTOR_LOCAL_API`,只有手工启动或旧配置缺少该值时才回退到 `BAA_CONTROL_API_BASE`
28 - mini 默认兼容值仍是 `https://conductor.makefile.so`
29 
30@@ -86,10 +90,11 @@ BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317
31 BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS=100.71.210.78
32 BAA_CODEXD_LOCAL_API_BASE=http://127.0.0.1:4319
33 BAA_STATUS_API_HOST=100.71.210.78
34+BAA_CONDUCTOR_PUBLIC_API_BASE=https://conductor.makefile.so
35 BAA_CONTROL_API_BASE=https://conductor.makefile.so
36 ```
37 
38-最后一项现在只写给 `conductor` 安装副本;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`,默认 launchd 不再额外携带 `BAA_CONTROL_API_BASE`。
39+最后两项现在都只写给 `conductor` 安装副本;`status-api` 的默认真相源仍然是 `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317`,默认 launchd 不再额外携带这些 conductor public-api base 变量。
40 
41 说明:
42 
43@@ -124,4 +129,4 @@ Firefox WS 派生规则:
44   --status-api-host 100.71.210.78
45 ```
46 
47-默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身也是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 默认仍然优先读取 `--local-api-base` 对应的 conductor 主接口。
48+默认 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 主接口。
M docs/runtime/launchd.md
+2, -2
 1@@ -46,7 +46,7 @@
 2 
 3 - `codexd` 独立安装时不需要共享 token
 4 - `status-api` 默认真相源是 `BAA_CONDUCTOR_LOCAL_API`
 5-- `--control-api-base` 仍然保留为 legacy 兼容参数名,但只影响 `conductor` 安装副本;默认值就是 `https://conductor.makefile.so`
 6+- `--public-api-base` 是 canonical 参数名;`--control-api-base` 仍保留为 legacy 兼容别名,但都只影响 `conductor` 安装副本
 7 - `--codexd-local-api-base` 会同时写给 `codexd` 和 `conductor`
 8 - `codexd` 正式运行面只写入 `app-server` 会话链路所需默认值
 9 - `codexd` 正式服务面只保留 `/healthz`、`/v1/codexd/status`、`/v1/codexd/sessions`、`/v1/codexd/turn`、`/v1/codexd/events`
10@@ -113,7 +113,7 @@
11   --status-api-host 100.71.210.78
12 ```
13 
14-默认 mini 安装不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖。即使显式传入,脚本现在也只会把它写给 `conductor` 安装副本;`status-api` 实际默认仍然优先读 `--local-api-base http://100.71.210.78:4317`。
15+默认 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`。
16 
17 单独安装 `codexd`:
18 
M docs/runtime/node-verification.md
+2, -2
 1@@ -28,9 +28,9 @@ npx --yes pnpm -r build
 2 
 3 说明:
 4 
 5-- 默认 mini 静态检查不需要显式传 `--control-api-base`;这个参数名本身是 legacy 兼容名,只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
 6+- 默认 mini 静态检查不需要显式传 `--public-api-base`;只有 `conductor` 的 upstream/public API base 不是 `https://conductor.makefile.so` 时才需要覆盖
 7 - `status-api` 的有效默认真相源仍然是 `--local-api-base http://100.71.210.78:4317`
 8-- `check-launchd.sh` 现在只校验 `conductor` 安装副本里的 `BAA_CONTROL_API_BASE`;其他服务要求该变量不存在
 9+- `check-launchd.sh` 现在会优先校验 `conductor` 安装副本里的 `BAA_CONDUCTOR_PUBLIC_API_BASE`,同时接受只带 legacy `BAA_CONTROL_API_BASE` 的旧安装副本;其他服务要求这两个变量都不存在
10 - `check-launchd.sh` 现在也会校验 `conductor` 安装副本里的 `BAA_CODEXD_LOCAL_API_BASE`
11 - `check-launchd.sh` 现在会校验 `codexd` 的监听地址、事件流路径、日志目录、状态目录和 `app-server` child 配置
12 - 这些静态检查不包含 run/exec 路线
M package.json
+2, -2
 1@@ -6,8 +6,8 @@
 2     "build": "pnpm -r build",
 3     "build:runtime-postprocess": "node --input-type=module -e \"import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; const distDir = process.env.BAA_DIST_DIR; const entry = process.env.BAA_DIST_ENTRY; if (!distDir || !entry) { throw new Error('BAA_DIST_DIR and BAA_DIST_ENTRY are required.'); } const aliasPairs = (process.env.BAA_IMPORT_ALIASES ?? '').split(';').filter(Boolean).map((pair) => { const separatorIndex = pair.indexOf('='); if (separatorIndex <= 0) { throw new Error('Invalid alias pair: ' + pair); } return [pair.slice(0, separatorIndex), pair.slice(separatorIndex + 1)]; }); const fixRelativeExtensions = process.env.BAA_FIX_RELATIVE_EXTENSIONS === 'true'; const jsFiles = []; const collect = (directory) => { for (const entryName of readdirSync(directory)) { const entryPath = join(directory, entryName); const stats = statSync(entryPath); if (stats.isDirectory()) { collect(entryPath); continue; } if (entryPath.endsWith('.js')) { jsFiles.push(entryPath); } } }; collect(distDir); for (const filePath of jsFiles) { let source = readFileSync(filePath, 'utf8'); for (const [from, to] of aliasPairs) { source = source.replaceAll('\\\"' + from + '\\\"', '\\\"' + to + '\\\"').replaceAll(\\\"'\\\" + from + \\\"'\\\", \\\"'\\\" + to + \\\"'\\\"); } if (fixRelativeExtensions) { source = source.replace(/((?:from|import)\\s*[\\\"'])(\\.\\.?\\/[^\\\"'()]+?)([\\\"'])/g, (match, prefix, specifier, suffix) => /\\.[cm]?[jt]sx?$/.test(specifier) || specifier.endsWith('.json') ? match : prefix + specifier + '.js' + suffix); } writeFileSync(filePath, source); } mkdirSync(distDir, { recursive: true }); const entrySpecifier = './' + entry; const shimLines = ['export * from ' + JSON.stringify(entrySpecifier) + ';']; if (process.env.BAA_EXPORT_DEFAULT === 'true') { shimLines.push('export { default } from ' + JSON.stringify(entrySpecifier) + ';'); } writeFileSync(join(distDir, 'index.js'), shimLines.join('\\n') + '\\n');\"",
 4     "typecheck": "pnpm -r typecheck",
 5-    "lint": "echo 'TODO: add linting'",
 6-    "test": "echo 'TODO: add tests'",
 7+    "lint": "node ./scripts/verify-workspace.mjs lint",
 8+    "test": "node ./scripts/verify-workspace.mjs test",
 9     "tasks": "ls tasks"
10   },
11   "devDependencies": {
M plans/STATUS_SUMMARY.md
+7, -4
 1@@ -6,10 +6,10 @@
 2 
 3 ## 当前代码基线
 4 
 5-- 主线基线:`main@d349be1`
 6+- 主线基线:`main@d1c9090`
 7 - 任务文档已统一收口到 `tasks/`
 8 - 当前活动任务见 `tasks/TASK_OVERVIEW.md`
 9-- `T-S001` 到 `T-S004` 已经合入主线
10+- `T-S001` 到 `T-S008` 已经合入主线
11 
12 ## 当前状态
13 
14@@ -51,15 +51,18 @@
15 - `T-S005`:默认 launchd 现在只把 `BAA_CONTROL_API_BASE` 写给 `conductor`;`status-api` 和 `worker-runner` 不再携带这个变量
16 - `T-S006`:`tasks/`、`plans/`、`README` 与 `docs/api/**` / `docs/runtime/**` 已把 `control-api` 收口为 legacy 背景,不再写成当前默认入口
17 - `T-S007`:`ConductorRuntime.stop()` 已补专项测试,覆盖 listener 释放、Firefox bridge client 关闭和重复 stop 的幂等行为
18+- `T-S008`:`codexd` 现在会把“未收到合法 completed 就提前断流”单独归类成新的 child / transport 故障,并写入 reopen 规则
19 
20 ## 下一步任务
21 
22-- `T-S008`:给 codexd child / transport 提前断流补诊断与 reopen 规则
23+- `T-S009`:把 conductor upstream/public API base 从 legacy 名字里解耦
24+- `T-S010`:补仓库根验证入口
25 
26 ## 当前仍需关注
27 
28 - 如果后续再次出现 app-server 根本没有发出合法 `turn/completed` 就提前断流,这属于新的 child / transport 故障,应单独新开 bug
29-- `BAA_CONTROL_API_BASE` 仍保留在仓库里,定位是兼容入口,不是 canonical truth source
30+- `BAA_CONTROL_API_BASE` / `--control-api-base` 仍保留在运行时代码和脚本里,定位是兼容名字,不是 canonical truth source
31 - 仍保留的 `control-api` 命名已经限定在历史任务卡、legacy 测试路径、兼容变量名和外部残留资产说明里;如果未来要继续删旧,需要单独评估文件名和兼容面
32 - 如果未来新增 runtime 测试绕开 `withRuntimeFixture(...)`,同类 listener 泄漏仍可能重新出现
33 - 这次没有改 `ConductorRuntime.stop()` 内部逻辑;如果未来关闭路径本身阻塞,还需要单独补运行时层测试
34+- 仓库根 `pnpm lint` / `pnpm test` 仍不是可信入口;后续应把常用验证收口到根脚本
M scripts/runtime/check-launchd.sh
+54, -7
  1@@ -20,7 +20,8 @@ Options:
  2   --install-dir PATH        Validate installed copies under this directory.
  3   --shared-token TOKEN      Expect this exact token in installed copies.
  4   --shared-token-file PATH  Read the expected token from a file.
  5-  --control-api-base URL    Expected conductor BAA_CONTROL_API_BASE.
  6+  --public-api-base URL     Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
  7+  --control-api-base URL    Legacy alias for --public-api-base.
  8   --local-api-base URL      Expected BAA_CONDUCTOR_LOCAL_API.
  9   --local-api-allowed-hosts CSV
 10                             Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
 11@@ -41,8 +42,10 @@ Options:
 12 Notes:
 13   If no service is specified, conductor + codexd + status-api are checked.
 14   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
 15-  longer writes BAA_CONTROL_API_BASE for it.
 16-  --control-api-base only applies when conductor is part of the checked set.
 17+  longer writes conductor public-api base env vars for it.
 18+  --public-api-base only applies when conductor is part of the checked set.
 19+  check mode accepts legacy conductor copies that only carry
 20+  BAA_CONTROL_API_BASE, but new installs are expected to write both names.
 21   codexd static checks only validate app-server launchd wiring; they do not
 22   require /v1/codexd/runs* or codex exec as formal runtime capabilities.
 23 EOF
 24@@ -58,7 +61,8 @@ home_dir="$(default_home_dir)"
 25 install_dir=""
 26 shared_token=""
 27 shared_token_file=""
 28-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
 29+public_api_base=""
 30+legacy_control_api_base=""
 31 local_api_base="http://100.71.210.78:4317"
 32 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
 33 codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
 34@@ -117,8 +121,12 @@ while [[ $# -gt 0 ]]; do
 35       shared_token_file="$2"
 36       shift 2
 37       ;;
 38+    --public-api-base)
 39+      public_api_base="$2"
 40+      shift 2
 41+      ;;
 42     --control-api-base)
 43-      control_api_base="$2"
 44+      legacy_control_api_base="$2"
 45       shift 2
 46       ;;
 47     --local-api-base)
 48@@ -178,6 +186,18 @@ done
 49 validate_node "$node"
 50 validate_scope "$scope"
 51 
 52+if [[ -z "$public_api_base" ]]; then
 53+  if [[ -n "$legacy_control_api_base" ]]; then
 54+    public_api_base="$legacy_control_api_base"
 55+  elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
 56+    public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
 57+  elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
 58+    public_api_base="${BAA_CONTROL_API_BASE}"
 59+  else
 60+    public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
 61+  fi
 62+fi
 63+
 64 if [[ "${#services[@]}" -eq 0 ]]; then
 65   while IFS= read -r service; do
 66     services+=("$service")
 67@@ -243,6 +263,32 @@ check_key_missing() {
 68   fi
 69 }
 70 
 71+check_public_api_base_keys() {
 72+  local service="$1"
 73+  local plist_path="$2"
 74+  local found_any="0"
 75+
 76+  if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"; then
 77+    found_any="1"
 78+    check_string_equals \
 79+      "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" \
 80+      "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE")" \
 81+      "$public_api_base"
 82+  fi
 83+
 84+  if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"; then
 85+    found_any="1"
 86+    check_string_equals \
 87+      "${service}:BAA_CONTROL_API_BASE" \
 88+      "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" \
 89+      "$public_api_base"
 90+  fi
 91+
 92+  if [[ "$found_any" != "1" ]]; then
 93+    die "${service}: missing conductor public API base env keys"
 94+  fi
 95+}
 96+
 97 check_installed_plist() {
 98   local service="$1"
 99   local plist_path="$2"
100@@ -270,9 +316,10 @@ check_installed_plist() {
101   check_string_equals "${service}:stderr" "$(plist_print_value "$plist_path" ":StandardErrorPath")" "$stderr_path"
102   check_string_equals "${service}:entry" "$(plist_print_value "$plist_path" ":ProgramArguments:2")" "$dist_entry"
103 
104-  if service_uses_control_api_base "$service"; then
105-    check_string_equals "${service}:BAA_CONTROL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" "$control_api_base"
106+  if service_uses_public_api_base "$service"; then
107+    check_public_api_base_keys "$service" "$plist_path"
108   else
109+    check_key_missing "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
110     check_key_missing "${service}:BAA_CONTROL_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
111   fi
112 
M scripts/runtime/check-node.sh
+24, -6
 1@@ -20,7 +20,8 @@ Options:
 2   --install-dir PATH           Validate installed copies under this directory.
 3   --shared-token TOKEN         Expect this exact token in installed copies.
 4   --shared-token-file PATH     Read the expected token from a file.
 5-  --control-api-base URL       Expected conductor BAA_CONTROL_API_BASE in installed copies.
 6+  --public-api-base URL        Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE in installed copies.
 7+  --control-api-base URL       Legacy alias for --public-api-base.
 8   --local-api-base URL         Conductor local API base URL. Defaults to 127.0.0.1:4317.
 9   --local-api-allowed-hosts CSV
10                                Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS in installed copies.
11@@ -42,7 +43,7 @@ Notes:
12   The default runtime check set is conductor + codexd + status-api. Use
13   --service to narrow the scope or --all-services to include worker-runner.
14   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
15-  longer writes BAA_CONTROL_API_BASE for it.
16+  longer writes conductor public-api base env vars for it.
17   conductor HTTP probes include /v1/codex to ensure proxy wiring to codexd.
18   For codexd, the HTTP probes only cover /healthz and /v1/codexd/status.
19   /v1/codexd/runs* and codex exec are not part of node verification.
20@@ -61,7 +62,8 @@ home_dir="$(default_home_dir)"
21 install_dir=""
22 shared_token=""
23 shared_token_file=""
24-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
25+public_api_base=""
26+legacy_control_api_base=""
27 local_api_base="http://100.71.210.78:4317"
28 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
29 codexd_api_base="${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}"
30@@ -123,8 +125,12 @@ while [[ $# -gt 0 ]]; do
31       shared_token_file="$2"
32       shift 2
33       ;;
34+    --public-api-base)
35+      public_api_base="$2"
36+      shift 2
37+      ;;
38     --control-api-base)
39-      control_api_base="$2"
40+      legacy_control_api_base="$2"
41       shift 2
42       ;;
43     --local-api-base)
44@@ -196,6 +202,18 @@ done
45 validate_node "$node"
46 validate_scope "$scope"
47 
48+if [[ -z "$public_api_base" ]]; then
49+  if [[ -n "$legacy_control_api_base" ]]; then
50+    public_api_base="$legacy_control_api_base"
51+  elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
52+    public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
53+  elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
54+    public_api_base="${BAA_CONTROL_API_BASE}"
55+  else
56+    public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
57+  fi
58+fi
59+
60 case "$expected_rolez" in
61   any | leader) ;;
62   *)
63@@ -353,8 +371,8 @@ run_static_checks() {
64   )
65 
66   for service in "${services[@]}"; do
67-    if service_uses_control_api_base "$service"; then
68-      static_args+=(--control-api-base "$control_api_base")
69+    if service_uses_public_api_base "$service"; then
70+      static_args+=(--public-api-base "$public_api_base")
71       break
72     fi
73   done
M scripts/runtime/common.sh
+7, -2
 1@@ -7,7 +7,8 @@ fi
 2 readonly BAA_RUNTIME_COMMON_SH_LOADED=1
 3 readonly BAA_RUNTIME_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
 4 readonly BAA_RUNTIME_REPO_DIR_DEFAULT="$(cd -- "${BAA_RUNTIME_SCRIPT_DIR}/../.." && pwd)"
 5-readonly BAA_RUNTIME_DEFAULT_CONTROL_API_BASE="https://conductor.makefile.so"
 6+readonly BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE="https://conductor.makefile.so"
 7+readonly BAA_RUNTIME_DEFAULT_CONTROL_API_BASE="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
 8 readonly BAA_RUNTIME_DEFAULT_LOCAL_API="http://127.0.0.1:4317"
 9 readonly BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API="http://127.0.0.1:4319"
10 readonly BAA_RUNTIME_DEFAULT_CODEXD_EVENT_STREAM_PATH="/v1/codexd/events"
11@@ -102,7 +103,7 @@ service_requires_shared_token() {
12   esac
13 }
14 
15-service_uses_control_api_base() {
16+service_uses_public_api_base() {
17   case "$1" in
18     conductor)
19       return 0
20@@ -113,6 +114,10 @@ service_uses_control_api_base() {
21   esac
22 }
23 
24+service_uses_control_api_base() {
25+  service_uses_public_api_base "$1"
26+}
27+
28 service_label() {
29   case "$1" in
30     conductor)
M scripts/runtime/install-launchd.sh
+29, -9
 1@@ -20,7 +20,8 @@ Options:
 2   --install-dir PATH        Override launchd install directory.
 3   --shared-token TOKEN      Shared token written into the install copy.
 4   --shared-token-file PATH  Read the shared token from a file.
 5-  --control-api-base URL    Override conductor BAA_CONTROL_API_BASE.
 6+  --public-api-base URL     Override conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
 7+  --control-api-base URL    Legacy alias for --public-api-base.
 8   --local-api-base URL      Override BAA_CONDUCTOR_LOCAL_API.
 9   --local-api-allowed-hosts CSV
10                             Override BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
11@@ -40,9 +41,10 @@ Notes:
12   Use --service codexd to render codexd independently; it does not require a
13   shared token.
14   status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
15-  longer writes BAA_CONTROL_API_BASE for it.
16-  --control-api-base only affects conductor install copies, and the default is
17-  already https://conductor.makefile.so.
18+  longer writes conductor public-api base env vars for it.
19+  --public-api-base only affects conductor install copies, and the default is
20+  already https://conductor.makefile.so. install copies write both
21+  BAA_CONDUCTOR_PUBLIC_API_BASE and legacy BAA_CONTROL_API_BASE.
22   codexd launchd wiring stays on app-server mode and does not expose
23   /v1/codexd/runs* or codex exec as a formal service contract.
24 EOF
25@@ -59,7 +61,8 @@ home_dir="$(default_home_dir)"
26 install_dir=""
27 shared_token="${BAA_SHARED_TOKEN:-}"
28 shared_token_file=""
29-control_api_base="${BAA_RUNTIME_DEFAULT_CONTROL_API_BASE}"
30+public_api_base=""
31+legacy_control_api_base=""
32 local_api_base="http://100.71.210.78:4317"
33 local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
34 codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
35@@ -116,8 +119,12 @@ while [[ $# -gt 0 ]]; do
36       shared_token_file="$2"
37       shift 2
38       ;;
39+    --public-api-base)
40+      public_api_base="$2"
41+      shift 2
42+      ;;
43     --control-api-base)
44-      control_api_base="$2"
45+      legacy_control_api_base="$2"
46       shift 2
47       ;;
48     --local-api-base)
49@@ -166,6 +173,18 @@ done
50 validate_node "$node"
51 validate_scope "$scope"
52 
53+if [[ -z "$public_api_base" ]]; then
54+  if [[ -n "$legacy_control_api_base" ]]; then
55+    public_api_base="$legacy_control_api_base"
56+  elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
57+    public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
58+  elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
59+    public_api_base="${BAA_CONTROL_API_BASE}"
60+  else
61+    public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
62+  fi
63+fi
64+
65 if [[ "${#services[@]}" -eq 0 ]]; then
66   while IFS= read -r service; do
67     services+=("$service")
68@@ -260,10 +279,11 @@ for service in "${services[@]}"; do
69     plist_delete_key "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN"
70   fi
71 
72-  if service_uses_control_api_base "$service"; then
73-    # conductor-daemon still reads the legacy env name for its upstream/public API base.
74-    plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$control_api_base"
75+  if service_uses_public_api_base "$service"; then
76+    plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE" "$public_api_base"
77+    plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$public_api_base"
78   else
79+    plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
80     plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
81   fi
82 
A scripts/runtime/public-api-base.test.mjs
+145, -0
  1@@ -0,0 +1,145 @@
  2+import assert from "node:assert/strict";
  3+import { execFileSync } from "node:child_process";
  4+import { mkdtempSync, rmSync } from "node:fs";
  5+import { tmpdir } from "node:os";
  6+import { join } from "node:path";
  7+import test from "node:test";
  8+import { fileURLToPath } from "node:url";
  9+
 10+const repoDir = fileURLToPath(new URL("../..", import.meta.url));
 11+const installScript = fileURLToPath(new URL("./install-launchd.sh", import.meta.url));
 12+const checkLaunchdScript = fileURLToPath(new URL("./check-launchd.sh", import.meta.url));
 13+const checkNodeScript = fileURLToPath(new URL("./check-node.sh", import.meta.url));
 14+const plistBuddy = "/usr/libexec/PlistBuddy";
 15+const sharedToken = "script-test-shared-token";
 16+const publicApiBase = "https://public.example.test";
 17+const conductorPlistLabel = "so.makefile.baa-conductor.plist";
 18+
 19+function runScript(scriptPath, args) {
 20+  execFileSync("bash", [scriptPath, ...args], {
 21+    cwd: repoDir,
 22+    env: process.env,
 23+    stdio: "pipe"
 24+  });
 25+}
 26+
 27+function plistValue(plistPath, key) {
 28+  return execFileSync(plistBuddy, ["-c", `Print ${key}`, plistPath], {
 29+    encoding: "utf8"
 30+  }).trim();
 31+}
 32+
 33+function deletePlistKey(plistPath, key) {
 34+  execFileSync(plistBuddy, ["-c", `Delete ${key}`, plistPath], {
 35+    stdio: "pipe"
 36+  });
 37+}
 38+
 39+function conductorInstallArgs(installDir, extraArgs = []) {
 40+  return [
 41+    "--repo-dir",
 42+    repoDir,
 43+    "--install-dir",
 44+    installDir,
 45+    "--service",
 46+    "conductor",
 47+    "--shared-token",
 48+    sharedToken,
 49+    "--local-api-base",
 50+    "http://100.71.210.78:4317",
 51+    "--local-api-allowed-hosts",
 52+    "100.71.210.78",
 53+    ...extraArgs
 54+  ];
 55+}
 56+
 57+test("install-launchd writes canonical and legacy conductor public API env names", (t) => {
 58+  const installDir = mkdtempSync(join(tmpdir(), "baa-conductor-public-api-install-"));
 59+  const plistPath = join(installDir, conductorPlistLabel);
 60+
 61+  t.after(() => {
 62+    rmSync(installDir, { force: true, recursive: true });
 63+  });
 64+
 65+  runScript(
 66+    installScript,
 67+    conductorInstallArgs(installDir, [
 68+      "--public-api-base",
 69+      publicApiBase,
 70+      "--codexd-local-api-base",
 71+      "http://127.0.0.1:4319"
 72+    ])
 73+  );
 74+
 75+  assert.equal(
 76+    plistValue(plistPath, ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"),
 77+    publicApiBase
 78+  );
 79+  assert.equal(plistValue(plistPath, ":EnvironmentVariables:BAA_CONTROL_API_BASE"), publicApiBase);
 80+
 81+  runScript(
 82+    checkLaunchdScript,
 83+    conductorInstallArgs(installDir, [
 84+      "--public-api-base",
 85+      publicApiBase,
 86+      "--codexd-local-api-base",
 87+      "http://127.0.0.1:4319"
 88+    ])
 89+  );
 90+
 91+  runScript(
 92+    checkNodeScript,
 93+    conductorInstallArgs(installDir, [
 94+      "--public-api-base",
 95+      publicApiBase,
 96+      "--codexd-api-base",
 97+      "http://127.0.0.1:4319",
 98+      "--skip-port-check",
 99+      "--skip-process-check",
100+      "--skip-http-check",
101+      "--skip-log-check"
102+    ])
103+  );
104+});
105+
106+test("check-launchd accepts legacy conductor install copies with only BAA_CONTROL_API_BASE", (t) => {
107+  const installDir = mkdtempSync(join(tmpdir(), "baa-conductor-public-api-legacy-"));
108+  const plistPath = join(installDir, conductorPlistLabel);
109+
110+  t.after(() => {
111+    rmSync(installDir, { force: true, recursive: true });
112+  });
113+
114+  runScript(
115+    installScript,
116+    conductorInstallArgs(installDir, [
117+      "--control-api-base",
118+      publicApiBase,
119+      "--codexd-local-api-base",
120+      "http://127.0.0.1:4319"
121+    ])
122+  );
123+
124+  deletePlistKey(plistPath, ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE");
125+
126+  assert.equal(plistValue(plistPath, ":EnvironmentVariables:BAA_CONTROL_API_BASE"), publicApiBase);
127+
128+  runScript(
129+    checkLaunchdScript,
130+    conductorInstallArgs(installDir, [
131+      "--public-api-base",
132+      publicApiBase,
133+      "--codexd-local-api-base",
134+      "http://127.0.0.1:4319"
135+    ])
136+  );
137+  runScript(
138+    checkLaunchdScript,
139+    conductorInstallArgs(installDir, [
140+      "--control-api-base",
141+      publicApiBase,
142+      "--codexd-local-api-base",
143+      "http://127.0.0.1:4319"
144+    ])
145+  );
146+});
A scripts/verify-workspace.mjs
+65, -0
 1@@ -0,0 +1,65 @@
 2+#!/usr/bin/env node
 3+
 4+import { spawnSync } from "node:child_process";
 5+import { dirname, resolve } from "node:path";
 6+import { fileURLToPath } from "node:url";
 7+
 8+const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
 9+
10+const testPackages = [
11+  "@baa-conductor/db",
12+  "@baa-conductor/host-ops",
13+  "@baa-conductor/codex-app-server",
14+  "@baa-conductor/codex-exec",
15+  "@baa-conductor/conductor-daemon",
16+  "@baa-conductor/codexd"
17+];
18+
19+const entrypoints = {
20+  lint: [
21+    {
22+      label: "workspace typecheck",
23+      command: ["pnpm", "typecheck"]
24+    },
25+    {
26+      label: "git diff hygiene",
27+      command: ["git", "diff", "--check"]
28+    }
29+  ],
30+  test: testPackages.map((pkg) => ({
31+    label: `${pkg} test`,
32+    command: ["pnpm", "--filter", pkg, "test"]
33+  }))
34+};
35+
36+const mode = process.argv[2];
37+const steps = entrypoints[mode];
38+
39+if (!steps) {
40+  console.error("Usage: node scripts/verify-workspace.mjs <lint|test>");
41+  process.exit(1);
42+}
43+
44+for (const step of steps) {
45+  console.log(`\n==> ${step.label}`);
46+  console.log(`$ ${step.command.join(" ")}`);
47+
48+  const result = spawnSync(step.command[0], step.command.slice(1), {
49+    cwd: repoRoot,
50+    env: process.env,
51+    stdio: "inherit"
52+  });
53+
54+  if (result.error) {
55+    throw result.error;
56+  }
57+
58+  if (typeof result.status === "number" && result.status !== 0) {
59+    process.exit(result.status);
60+  }
61+
62+  if (result.signal) {
63+    console.error(`Command terminated by signal: ${result.signal}`);
64+    process.exit(1);
65+  }
66+}
A tasks/T-S009.md
+120, -0
  1@@ -0,0 +1,120 @@
  2+# Task T-S009:把 conductor upstream/public API base 从 legacy 名字里解耦
  3+
  4+## 直接给对话的提示词
  5+
  6+读 `/Users/george/code/baa-conductor/tasks/T-S009.md` 任务文档,完成开发任务。
  7+
  8+如需补背景,再读:
  9+
 10+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.ts`
 11+- `/Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js`
 12+- `/Users/george/code/baa-conductor/scripts/runtime/install-launchd.sh`
 13+- `/Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
 14+- `/Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
 15+- `/Users/george/code/baa-conductor/docs/runtime/environment.md`
 16+- `/Users/george/code/baa-conductor/docs/runtime/launchd.md`
 17+
 18+## 当前基线
 19+
 20+- 仓库:`/Users/george/code/baa-conductor`
 21+- 分支:`main`
 22+- 提交:`d1c9090`
 23+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
 24+
 25+## 建议分支名
 26+
 27+- `chore/rename-conductor-public-api-base`
 28+
 29+## 目标
 30+
 31+把 conductor upstream/public API base 的配置入口从 legacy `control-api` 命名里解耦,同时保留兼容别名,不破坏现有安装副本和脚本。
 32+
 33+## 背景
 34+
 35+- 当前主线已经不再把 `control-api.makefile.so` 当默认控制面。
 36+- 但 `conductor-daemon`、`install-launchd.sh`、`check-launchd.sh`、`check-node.sh` 仍用 `BAA_CONTROL_API_BASE` / `--control-api-base` 这套历史名字表示 conductor 自己的 upstream/public API base。
 37+- 这会继续误导协作者,把“legacy 名字残留”误读成“系统仍依赖 legacy control plane”。
 38+
 39+## 涉及仓库
 40+
 41+- `/Users/george/code/baa-conductor`
 42+
 43+## 范围
 44+
 45+- 给 conductor 增加 dedicated 的 env / CLI 名字
 46+- 把运行脚本、帮助文本、测试和文档切到新名字
 47+- 保留 `BAA_CONTROL_API_BASE` / `--control-api-base` 作为兼容别名,并明确优先级和弃用语义
 48+
 49+## 路径约束
 50+
 51+- 这个任务只处理 conductor upstream/public API base 的命名收口
 52+- 不要顺手改 `status-api` 的 truth-source 逻辑
 53+- 不要扩展成新的部署设计或域名迁移任务
 54+
 55+## 推荐实现边界
 56+
 57+建议优先做:
 58+
 59+- 为 `conductor-daemon` 引入 dedicated 名字,例如 `BAA_CONDUCTOR_PUBLIC_API_BASE` 和 `--public-api-base`
 60+- 让旧名字继续可用,但只作为兼容别名,并在帮助文本/文档里标明 legacy
 61+- 同步更新 launchd 安装/检查脚本与测试
 62+
 63+## 允许修改的目录
 64+
 65+- `/Users/george/code/baa-conductor/apps/conductor-daemon/`
 66+- `/Users/george/code/baa-conductor/scripts/runtime/`
 67+- `/Users/george/code/baa-conductor/docs/runtime/`
 68+- `/Users/george/code/baa-conductor/docs/api/README.md`
 69+
 70+## 尽量不要修改
 71+
 72+- `/Users/george/code/baa-conductor/apps/status-api/`
 73+- `/Users/george/code/baa-conductor/apps/codexd/`
 74+- `/Users/george/code/baa-conductor/tasks/`
 75+
 76+## 必须完成
 77+
 78+### 1. 引入 dedicated 配置名字
 79+
 80+- conductor 运行时代码应支持 dedicated env / CLI 名字
 81+- 旧名字继续兼容,但不能再被描述为 canonical 配置名
 82+
 83+### 2. 保持兼容行为可解释
 84+
 85+- 明确新旧名字同时存在时谁优先
 86+- 帮助文本、脚本 usage、runtime 文档都要写清兼容关系
 87+
 88+### 3. 补测试和验证
 89+
 90+- 至少覆盖 CLI / env 解析和脚本预期
 91+- 验证安装/检查脚本没有被这次改名打坏
 92+
 93+## 需要特别注意
 94+
 95+- 不要破坏现有 launchd 安装副本读取旧环境变量的兼容性
 96+- 不要顺手改 service 名字或 plist label
 97+- 与根脚本任务并行时,尽量不要修改同一批总览文档
 98+
 99+## 验收标准
100+
101+- conductor 代码和脚本已有 dedicated 配置名
102+- `BAA_CONTROL_API_BASE` / `--control-api-base` 只剩兼容别名语义
103+- conductor 测试和 runtime 脚本检查通过
104+- `git diff --check` 通过
105+
106+## 推荐验证命令
107+
108+- `npx --yes pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon test`
109+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/install-launchd.sh`
110+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-launchd.sh`
111+- `bash -n /Users/george/code/baa-conductor/scripts/runtime/check-node.sh`
112+- `git -C /Users/george/code/baa-conductor diff --check`
113+
114+## 交付要求
115+
116+完成后请说明:
117+
118+- 修改了哪些文件
119+- 新旧配置名分别是什么,兼容优先级如何
120+- 跑了哪些测试
121+- 还有哪些剩余风险
A tasks/T-S010.md
+120, -0
  1@@ -0,0 +1,120 @@
  2+# Task T-S010:补仓库根验证入口
  3+
  4+## 直接给对话的提示词
  5+
  6+读 `/Users/george/code/baa-conductor/tasks/T-S010.md` 任务文档,完成开发任务。
  7+
  8+如需补背景,再读:
  9+
 10+- `/Users/george/code/baa-conductor/package.json`
 11+- `/Users/george/code/baa-conductor/apps/conductor-daemon/package.json`
 12+- `/Users/george/code/baa-conductor/apps/codexd/package.json`
 13+- `/Users/george/code/baa-conductor/apps/status-api/package.json`
 14+- `/Users/george/code/baa-conductor/packages/host-ops/package.json`
 15+- `/Users/george/code/baa-conductor/packages/db/package.json`
 16+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
 17+
 18+## 当前基线
 19+
 20+- 仓库:`/Users/george/code/baa-conductor`
 21+- 分支:`main`
 22+- 提交:`d1c9090`
 23+- 开工要求:不要从其他任务分支切出;如需新分支,从当前 `main` 新切
 24+
 25+## 建议分支名
 26+
 27+- `chore/add-root-verification-entrypoints`
 28+
 29+## 目标
 30+
 31+把仓库根的 `pnpm lint` / `pnpm test` 从占位脚本改成真实、可重复执行的验证入口。
 32+
 33+## 背景
 34+
 35+- 当前根 `package.json` 里的 `lint` 和 `test` 只是 `echo TODO`。
 36+- 实际仓库已经有不少稳定可跑的包级 `build` / `typecheck` / `test`,但缺少统一入口。
 37+- 这会让协作者不知道“在根目录该跑什么”,也让主线验收口径分散在聊天记录里。
 38+
 39+## 涉及仓库
 40+
 41+- `/Users/george/code/baa-conductor`
 42+
 43+## 范围
 44+
 45+- 让根 `pnpm lint` / `pnpm test` 成为真实入口
 46+- 如有必要,新增少量 helper 脚本收口验证矩阵
 47+- 写清这两个入口覆盖什么、不覆盖什么
 48+
 49+## 路径约束
 50+
 51+- 优先复用现有包级脚本,不要大范围改包内部测试逻辑
 52+- 不要引入重量级新 lint 框架,除非确有必要且收益明确
 53+- 这个任务是“入口收口”,不是“给全仓新增 ESLint/Prettier/CI 平台”
 54+
 55+## 推荐实现边界
 56+
 57+建议优先做:
 58+
 59+- 把根 `test` 收口到当前稳定的关键包测试矩阵
 60+- 把根 `lint` 改成真实的仓库级静态检查或 repo-hygiene 检查
 61+- 在文档里说明根入口的覆盖范围和刻意不覆盖的部分
 62+
 63+## 允许修改的目录
 64+
 65+- `/Users/george/code/baa-conductor/package.json`
 66+- `/Users/george/code/baa-conductor/scripts/`
 67+- `/Users/george/code/baa-conductor/docs/ops/`
 68+- `/Users/george/code/baa-conductor/docs/runtime/README.md`
 69+
 70+## 尽量不要修改
 71+
 72+- `/Users/george/code/baa-conductor/apps/status-api/`
 73+- `/Users/george/code/baa-conductor/apps/codexd/`
 74+- `/Users/george/code/baa-conductor/apps/conductor-daemon/`
 75+- `/Users/george/code/baa-conductor/tasks/`
 76+
 77+## 必须完成
 78+
 79+### 1. 去掉根占位脚本
 80+
 81+- 根 `pnpm lint` 不能再只是 `echo`
 82+- 根 `pnpm test` 不能再只是 `echo`
 83+
 84+### 2. 让入口可重复执行
 85+
 86+- 默认在干净仓库即可运行
 87+- 不依赖人工交互或线上环境
 88+
 89+### 3. 写清覆盖边界
 90+
 91+- 说明根入口覆盖哪些包或脚本
 92+- 说明哪些更慢或更特殊的验证仍需单独跑
 93+
 94+## 需要特别注意
 95+
 96+- 不要把“根入口”做成超长、脆弱、难维护的一串命令
 97+- 尽量让失败点可定位,不要只给一个笼统失败
 98+- 与 runtime 命名收口任务并行时,避免同时改同一段 runtime 文档
 99+
100+## 验收标准
101+
102+- 根 `pnpm lint` / `pnpm test` 都是可执行的真实检查
103+- 文档已说明这两个入口的覆盖范围
104+- 相关命令在当前主线可跑通
105+- `git diff --check` 通过
106+
107+## 推荐验证命令
108+
109+- `pnpm -C /Users/george/code/baa-conductor lint`
110+- `pnpm -C /Users/george/code/baa-conductor test`
111+- `pnpm -C /Users/george/code/baa-conductor typecheck`
112+- `git -C /Users/george/code/baa-conductor diff --check`
113+
114+## 交付要求
115+
116+完成后请说明:
117+
118+- 根入口最终跑了哪些子检查
119+- 有没有新增 helper 脚本
120+- 跑了哪些验证
121+- 还有哪些检查仍未被根入口覆盖
M tasks/TASK_OVERVIEW.md
+7, -5
 1@@ -9,7 +9,7 @@
 2 - `control-api.makefile.so`、Cloudflare Worker、D1 只剩迁移期 legacy 兼容残留和依赖盘点用途
 3 - `baa-hand` / `baa-shell` 只保留为接口语义参考,不再作为主系统维护
 4 - 当前任务卡都放在本目录
 5-- 当前任务基线:`main@d349be1`
 6+- 当前任务基线:`main@d1c9090`
 7 
 8 ## 最近完成任务
 9 
10@@ -22,6 +22,7 @@
11 5. [`T-S005.md`](./T-S005.md):收口 `BAA_CONTROL_API_BASE` 兼容入口
12 6. [`T-S006.md`](./T-S006.md):清理历史 `control-api` 命名残留
13 7. [`T-S007.md`](./T-S007.md):补 `ConductorRuntime.stop()` 关闭路径专项测试
14+8. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
15 
16 说明:
17 
18@@ -29,14 +30,15 @@
19 
20 ## 当前活动任务
21 
22-围绕剩余风险,当前建议继续推进这 1 张任务卡:
23+围绕剩余技术债,当前建议继续推进这 2 张任务卡:
24 
25-1. [`T-S008.md`](./T-S008.md):补 codexd child / transport 断流诊断与 reopen 规则
26+1. [`T-S009.md`](./T-S009.md):把 conductor upstream/public API base 从 legacy 名字里解耦
27+2. [`T-S010.md`](./T-S010.md):补仓库根验证入口
28 
29 说明:
30 
31-- `T-S006` 已完成并回写当前文档口径
32-- `T-S008` 主要改 codexd 与 bug/runtime 文档
33+- `T-S009` 主要改 `apps/conductor-daemon`、`scripts/runtime/**` 和 `docs/runtime/**`
34+- `T-S010` 主要改根 `package.json` 与新增的验证脚本;尽量不要和 `T-S009` 同时改同一批文档
35 
36 ## 任务文档约定
37