@jennie-shawn/starwork 0.1.0-alpha.18 → 0.1.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ StarWork 是一套面向 AI 的工作台协议和工具集,用来帮助用户
9
9
  - **Packs**:场景模板包。当前 A 测阶段默认使用通用工作能力。
10
10
  - **Skills**:给 Codex 等 AI 工具使用的工作流说明,让 AI 能更可靠地帮用户设计和生成 StarWork 工作台。
11
11
 
12
- 当前版本处于 A 测阶段。npm `latest` 为 `@jennie-shawn/starwork@0.1.0-alpha.17`,适合测试安装流程、基础命令、工作台结构、AI Skill 使用体验和 Codex 多会话协作链路。
12
+ 当前版本处于 A 测阶段。npm `latest` 为 `@jennie-shawn/starwork@0.1.0-alpha.18`,适合测试安装流程、基础命令、工作台结构、AI Skill 使用体验和 MultiAgent 会话协作链路。
13
13
 
14
14
  ## 安装 CLI
15
15
 
@@ -173,7 +173,7 @@ product/planning/features/<feature>/
173
173
  - `upgrade`:按 `starworkDoctor` skill 生成的升级蓝图,把历史模板或类似项目中心的旧工作区安全接入 StarWork 工作台。
174
174
  - `adapt`:生成 Claude Code、Cursor 等 AI 工具的适配文件。
175
175
  - `pack install`:向兼容工作台安装支持的场景能力。
176
- - `multiagent`:为同一项目建立自定义 AI 职责位、绑定会话、登记跨 lane 共享输出,并支持 Codex 多会话读取、launch 和格式化指令发送;未观察到目标 turn 完成时会返回 `started_unverified`,避免把未完成交付误说成已完成。
176
+ - `multiagent`:为同一项目建立自定义 AI 职责位、绑定会话、登记跨 lane 共享输出,并支持会话观察、launch 和格式化跨会话指令;`instruct` CLI 运行时判断宿主能力,不能标准投递时返回 `manual_handoff_required`。
177
177
 
178
178
  ## 仓库结构
179
179
 
package/cli/src/cli.js CHANGED
@@ -731,6 +731,8 @@ function parseArgs(argv) {
731
731
  options.turns = readValue(argv, ++i, arg);
732
732
  } else if (arg === "--timeout") {
733
733
  options.timeout = readValue(argv, ++i, arg);
734
+ } else if (arg === "--wait" || arg === "--wait-completion") {
735
+ options.waitCompletion = true;
734
736
  } else if (arg === "--title") {
735
737
  options.title = readValue(argv, ++i, arg);
736
738
  } else if (arg === "--path") {
@@ -1232,7 +1234,8 @@ async function lanesInstruct(argv) {
1232
1234
  });
1233
1235
  const targetSession = lanesState.lanes?.[toLane]?.current_session || targetLane.current_session;
1234
1236
  const parsedSession = parseAdapterSession(targetSession);
1235
- const canAutoSend = !options.manualHandoff && parsedSession.host === "codex" && parsedSession.id;
1237
+ const route = resolveHostRuntimeCapability({ workspaceRoot, parsedSession, command: "instruct" });
1238
+ const canAutoSend = !options.manualHandoff && route.action === "auto_send";
1236
1239
  const dryRunRequest = buildSharedRequestRow({
1237
1240
  id: requestId,
1238
1241
  from: fromLane,
@@ -1249,24 +1252,35 @@ async function lanesInstruct(argv) {
1249
1252
  agreements: shared.agreements
1250
1253
  });
1251
1254
  if (options.json && options.dryRun) {
1252
- console.log(JSON.stringify({ schema: "starwork.agent_lanes.instruct.v0.2", dry_run: true, request: dryRunRequest, formatted_message: message }, null, 2));
1255
+ console.log(JSON.stringify({ schema: "starwork.agent_lanes.instruct.v0.4", dry_run: true, request: dryRunRequest, host: renderHostRoute(route), formatted_message: message }, null, 2));
1253
1256
  return;
1254
1257
  }
1255
1258
  if (!options.json) {
1256
1259
  printGenericPlan(options.dryRun ? "跨会话指令预览(dry run):" : "跨会话指令计划:", dryPlan.actions);
1257
- if (canAutoSend) console.log(`将发送到 Codex thread:${parsedSession.id}`);
1258
- else console.log(`目标 lane 绑定的是 ${parsedSession.host},当前不能后台自动发送;将生成人工交付消息并记录为 ${MANUAL_HANDOFF_STATUS}。`);
1260
+ if (canAutoSend) console.log(`将通过宿主标准能力发送到 ${parsedSession.host}:${parsedSession.id}`);
1261
+ else console.log(`目标 lane 路由结果:${route.status};将按 CLI 返回状态记录。`);
1259
1262
  console.log("");
1260
1263
  }
1261
1264
  if (options.dryRun) return;
1262
1265
  await confirmOrThrow(options, `是否向 Lane ${toLane} 发送指令?`);
1263
- const delivery = canAutoSend
1264
- ? await sendCodexInstruction({ threadId: parsedSession.id, message, timeout: parsePositiveInt(options.timeout, 300000) })
1265
- : createManualHandoffDelivery({
1266
- parsedSession,
1266
+ let delivery;
1267
+ if (options.manualHandoff) {
1268
+ delivery = createManualHandoffDelivery({
1269
+ parsedSession,
1270
+ message,
1271
+ reason: "Manual handoff requested"
1272
+ });
1273
+ } else if (canAutoSend) {
1274
+ delivery = await sendCodexInstruction({
1275
+ threadId: parsedSession.id,
1267
1276
  message,
1268
- reason: "Target host does not support StarWork background message delivery in v0.1"
1277
+ timeout: parsePositiveInt(options.timeout, 300000),
1278
+ waitCompletion: Boolean(options.waitCompletion)
1269
1279
  });
1280
+ delivery.mode = "host_standard_api";
1281
+ } else {
1282
+ delivery = createDeliveryFromRoute({ route, parsedSession, message, workspaceRoot });
1283
+ }
1270
1284
  const finalRequest = buildSharedRequestRow({
1271
1285
  id: requestId,
1272
1286
  from: fromLane,
@@ -1299,7 +1313,7 @@ async function lanesInstruct(argv) {
1299
1313
  ]
1300
1314
  });
1301
1315
  if (options.json) {
1302
- console.log(JSON.stringify({ schema: "starwork.agent_lanes.instruct.v0.2", request: finalRequest, host_delivery: delivery }, null, 2));
1316
+ console.log(JSON.stringify({ schema: "starwork.agent_lanes.instruct.v0.4", request: finalRequest, host: renderHostRoute(route), host_delivery: delivery }, null, 2));
1303
1317
  return;
1304
1318
  }
1305
1319
  console.log("");
@@ -1364,33 +1378,53 @@ async function lanesLaunch(argv) {
1364
1378
  const launchResults = [];
1365
1379
  if (options.dryRun) {
1366
1380
  for (const lane of lanes) {
1367
- launchResults.push({ lane: lane.lane, dry_run: true, message: renderMultiagentLaunchMessage({ lane, fromLane: options.from || "user", workspaceRoot, collaboration }) });
1381
+ const sessionName = buildLaneLaunchSessionName({ lane, workspaceRoot, explicitName: options.sessionName });
1382
+ launchResults.push({
1383
+ lane: lane.lane,
1384
+ dry_run: true,
1385
+ session_name: sessionName,
1386
+ launch_status: "dry_run",
1387
+ rename_status: sessionName ? "dry_run" : "not_requested",
1388
+ binding_status: "dry_run",
1389
+ message: renderMultiagentLaunchMessage({ lane, fromLane: options.from || "user", workspaceRoot, collaboration })
1390
+ });
1368
1391
  }
1369
1392
  if (options.json) {
1370
- console.log(JSON.stringify({ schema: "starwork.agent_lanes.launch.v0.2", dry_run: true, launches: launchResults }, null, 2));
1393
+ console.log(JSON.stringify({ schema: "starwork.agent_lanes.launch.v0.3", dry_run: true, launches: launchResults }, null, 2));
1371
1394
  return;
1372
1395
  }
1373
1396
  console.log("");
1374
1397
  console.log("Codex lane launch 预览(dry run):");
1375
- lanes.forEach((lane) => console.log(`- ${lane.lane}`));
1398
+ launchResults.forEach((result) => console.log(`- ${result.lane}${result.session_name ? ` -> ${result.session_name}` : ""}`));
1376
1399
  return;
1377
1400
  }
1378
1401
  await confirmOrThrow(options, `是否 launch ${lanes.length} 个 Codex lane thread?`);
1379
1402
  let nextRegistryLanes = registry.lanes;
1380
1403
  let lanesState = readAgentLanesState(workspaceRoot);
1381
1404
  for (const lane of lanes) {
1405
+ const sessionName = buildLaneLaunchSessionName({ lane, workspaceRoot, explicitName: options.sessionName });
1382
1406
  const launchMessage = renderMultiagentLaunchMessage({ lane, fromLane: options.from || "user", workspaceRoot, collaboration });
1383
1407
  const launch = await launchCodexLane({ message: launchMessage, workspaceRoot, timeout: parsePositiveInt(options.timeout, 90000) });
1384
- const session = launch.thread_id ? `codex:${launch.thread_id}` : "unbound";
1408
+ const launchedThreadId = launch.thread_id || launch.created_thread_id || "";
1409
+ const session = launchedThreadId ? `codex:${launchedThreadId}` : "unbound";
1410
+ const sessionNameSync = launchedThreadId
1411
+ ? await renameHostSessionBestEffort({ session, sessionName })
1412
+ : createSessionNameSyncResult({
1413
+ requested: Boolean(sessionName),
1414
+ supported: false,
1415
+ status: sessionName ? "skipped" : "not_requested",
1416
+ name: sessionName,
1417
+ warning: sessionName ? "No host thread was created to rename." : null
1418
+ });
1419
+ const bindingStatus = launch.thread_id && launch.status === "completed" ? "bound" : "unbound";
1385
1420
  if (launch.thread_id && launch.status === "completed") {
1386
- const sessionNameSync = await renameHostSessionBestEffort({ session, sessionName: normalizeMarkdownCell(options.sessionName || "") });
1387
1421
  const pinSync = pinHostThreadBestEffort({ session, requested: Boolean(options.pin) });
1388
1422
  nextRegistryLanes = nextRegistryLanes.map((item) => item.lane === lane.lane ? { ...item, current_session: session } : item);
1389
1423
  lanesState = updateAgentLaneHostState(lanesState, lane.lane, {
1390
1424
  host: "codex",
1391
1425
  current_session: session,
1392
1426
  thread_id: launch.thread_id,
1393
- session_name: normalizeMarkdownCell(options.sessionName || ""),
1427
+ session_name: sessionName,
1394
1428
  pinned: pinSync.status === "ok",
1395
1429
  pin_status: pinSync.status,
1396
1430
  created_by: "starwork multiagent launch",
@@ -1403,17 +1437,32 @@ async function lanesLaunch(argv) {
1403
1437
  launch.session_name_sync = sessionNameSync;
1404
1438
  launch.pin_sync = pinSync;
1405
1439
  }
1406
- launchResults.push({ lane: lane.lane, ...launch });
1440
+ launchResults.push({
1441
+ lane: lane.lane,
1442
+ ...launch,
1443
+ session,
1444
+ session_id: launchedThreadId || undefined,
1445
+ session_name: sessionName,
1446
+ launch_status: launch.status,
1447
+ rename_status: sessionNameSync.status,
1448
+ rename_warning: sessionNameSync.warning || undefined,
1449
+ binding_status: bindingStatus,
1450
+ session_name_sync: sessionNameSync
1451
+ });
1407
1452
  }
1408
1453
  actions.push(...buildLanesRegistryPlan(workspaceRoot, nextRegistryLanes).actions);
1409
1454
  actions.push(stateFileAction(workspaceRoot, lanesState));
1410
1455
  applyPlan({ targetDir: workspaceRoot, actions: dedupeActions(actions) });
1411
1456
  if (options.json) {
1412
- console.log(JSON.stringify({ schema: "starwork.agent_lanes.launch.v0.2", launches: launchResults }, null, 2));
1457
+ console.log(JSON.stringify({ schema: "starwork.agent_lanes.launch.v0.3", launches: launchResults }, null, 2));
1413
1458
  return;
1414
1459
  }
1415
1460
  console.log("");
1416
- launchResults.forEach((result) => console.log(`Lane ${result.lane}: ${result.status}${result.thread_id ? ` (${result.thread_id})` : ""}${result.warning ? ` - ${result.warning}` : ""}`));
1461
+ launchResults.forEach((result) => {
1462
+ console.log(`Lane ${result.lane}: ${result.status}${result.thread_id ? ` (${result.thread_id})` : ""}${result.session_name ? ` -> ${result.session_name}` : ""}${result.warning ? ` - ${result.warning}` : ""}`);
1463
+ if (result.rename_warning) console.log(` Warning: host session rename skipped: ${result.rename_warning}`);
1464
+ if (result.binding_status !== "bound") console.log(" Warning: lane was not bound because launch did not complete.");
1465
+ });
1417
1466
  }
1418
1467
 
1419
1468
  async function lanesShare(argv) {
@@ -1582,7 +1631,7 @@ function collectDoctorResult(targetDir, options = {}) {
1582
1631
  if (trace) {
1583
1632
  addCheck(result, "workspace.state.exists", "fail", "疑似 StarWork 工作台,但缺少 .starwork/workspace.json。", trace);
1584
1633
  } else {
1585
- addCheck(result, "workspace.state.exists", "fail", "当前目录不是 StarWork 工作台。请先运行 starwork init。");
1634
+ addCheck(result, "workspace.state.exists", "fail", "当前目录不是 StarWork 工作台。请让 Agent 使用 starworkInit Skill 完成接入;CLI 只作为确认方案后的执行工具。");
1586
1635
  }
1587
1636
  }
1588
1637
  return result;
@@ -1656,7 +1705,7 @@ function requireWorkspaceRoot(targetDir) {
1656
1705
  }
1657
1706
  const workspaceRoot = findWorkspaceRoot(targetDir);
1658
1707
  if (!workspaceRoot) {
1659
- throw new Error("当前目录不是 StarWork 工作台。请先运行 starwork init。");
1708
+ throw new Error("当前目录不是 StarWork 工作台。请让 Agent 使用 starworkInit Skill 完成接入;CLI 只作为确认方案后的执行工具。");
1660
1709
  }
1661
1710
  return workspaceRoot;
1662
1711
  }
@@ -7043,6 +7092,13 @@ function parseLaneList(value) {
7043
7092
  .map((item) => normalizeLaneId(item, "lanes"));
7044
7093
  }
7045
7094
 
7095
+ function buildLaneLaunchSessionName({ lane, workspaceRoot, explicitName }) {
7096
+ const requestedName = normalizeMarkdownCell(explicitName || "");
7097
+ if (requestedName) return requestedName;
7098
+ const roleName = normalizeMarkdownCell(lane?.purpose || lane?.lane || "");
7099
+ return normalizeMarkdownCell(`${roleName || "Agent"} Agent`);
7100
+ }
7101
+
7046
7102
  function normalizeLaneId(value, label) {
7047
7103
  if (typeof value !== "string" || !value.trim()) {
7048
7104
  throw new Error(`${label} 必须是非空 lane ID。`);
@@ -7289,6 +7345,7 @@ function createManualHandoffDelivery({ parsedSession, message, reason }) {
7289
7345
  return {
7290
7346
  adapter: parsedSession.host,
7291
7347
  status: MANUAL_HANDOFF_STATUS,
7348
+ mode: "manual_handoff",
7292
7349
  session: parsedSession.session,
7293
7350
  session_id: parsedSession.id,
7294
7351
  formatted_message: message,
@@ -7296,6 +7353,127 @@ function createManualHandoffDelivery({ parsedSession, message, reason }) {
7296
7353
  };
7297
7354
  }
7298
7355
 
7356
+ function resolveHostRuntimeCapability({ workspaceRoot, parsedSession, command }) {
7357
+ if (!parsedSession?.id || parsedSession.host === "none") {
7358
+ return {
7359
+ host: parsedSession?.host || "none",
7360
+ session: parsedSession?.session || "unbound",
7361
+ command,
7362
+ status: "unbound",
7363
+ action: "none",
7364
+ warning: "Target lane is not bound to a host session."
7365
+ };
7366
+ }
7367
+ let profile = null;
7368
+ try {
7369
+ profile = loadAdapterProfile(parsedSession.host);
7370
+ } catch (error) {
7371
+ return {
7372
+ host: parsedSession.host,
7373
+ session: parsedSession.session,
7374
+ command,
7375
+ status: "unsupported",
7376
+ action: "none",
7377
+ warning: error.message
7378
+ };
7379
+ }
7380
+ const adaptersState = readAdaptersState(workspaceRoot);
7381
+ const adapterRecord = adaptersState.adapters?.[profile.host] || null;
7382
+ const adapterEnabled = Boolean(adapterRecord?.enabled);
7383
+ if (profile.host !== "codex" && !adapterEnabled) {
7384
+ return {
7385
+ host: profile.host,
7386
+ session: parsedSession.session,
7387
+ command,
7388
+ profile_level: profile.sessions?.send_message || "unknown",
7389
+ adapter_enabled: false,
7390
+ status: "needs_adapt",
7391
+ action: "none",
7392
+ warning: `Target host is not adapted. Run starwork adapt ${profile.host} --target <path> --dry-run before automatic routing.`
7393
+ };
7394
+ }
7395
+ const profileLevel = profile.sessions?.send_message || "unknown";
7396
+ if (profileLevel === "unsupported") {
7397
+ return {
7398
+ host: profile.host,
7399
+ session: parsedSession.session,
7400
+ command,
7401
+ profile_level: profileLevel,
7402
+ adapter_enabled: adapterEnabled,
7403
+ status: "unsupported",
7404
+ action: "none",
7405
+ warning: `${profile.label} does not support background message delivery.`
7406
+ };
7407
+ }
7408
+ const runtime = probeHostStandardSendCapability(profile.host);
7409
+ if (runtime.available) {
7410
+ return {
7411
+ host: profile.host,
7412
+ session: parsedSession.session,
7413
+ command,
7414
+ profile_level: profileLevel,
7415
+ adapter_enabled: adapterEnabled,
7416
+ runtime_available: true,
7417
+ status: "delivered",
7418
+ action: "auto_send",
7419
+ mode: runtime.mode
7420
+ };
7421
+ }
7422
+ return {
7423
+ host: profile.host,
7424
+ session: parsedSession.session,
7425
+ command,
7426
+ profile_level: profileLevel,
7427
+ adapter_enabled: adapterEnabled,
7428
+ runtime_available: false,
7429
+ status: MANUAL_HANDOFF_STATUS,
7430
+ action: "manual_handoff",
7431
+ warning: runtime.warning || `${profile.label} standard background delivery capability is not available in this CLI runtime.`
7432
+ };
7433
+ }
7434
+
7435
+ function probeHostStandardSendCapability(host) {
7436
+ return {
7437
+ host,
7438
+ available: false,
7439
+ mode: null,
7440
+ warning: `${host} standard background delivery capability is not available in this CLI runtime; low-level turn APIs are not used for multiagent instruct.`
7441
+ };
7442
+ }
7443
+
7444
+ function renderHostRoute(route) {
7445
+ return {
7446
+ id: route.host,
7447
+ session: route.session,
7448
+ command: route.command,
7449
+ profile_level: route.profile_level,
7450
+ adapter_enabled: route.adapter_enabled,
7451
+ runtime_available: route.runtime_available,
7452
+ action: route.action,
7453
+ status: route.status,
7454
+ warning: route.warning || null
7455
+ };
7456
+ }
7457
+
7458
+ function createDeliveryFromRoute({ route, parsedSession, message, workspaceRoot }) {
7459
+ if (route.status === MANUAL_HANDOFF_STATUS) {
7460
+ return createManualHandoffDelivery({
7461
+ parsedSession,
7462
+ message,
7463
+ reason: route.warning || "Target host requires manual handoff."
7464
+ });
7465
+ }
7466
+ return {
7467
+ adapter: route.host || parsedSession.host,
7468
+ status: route.status,
7469
+ mode: route.action || "none",
7470
+ session: parsedSession.session,
7471
+ session_id: parsedSession.id,
7472
+ message_path: path.relative(workspaceRoot, path.join(workspaceRoot, "_系统", "协作", "shared.md")) || "_系统/协作/shared.md",
7473
+ warning: route.warning || null
7474
+ };
7475
+ }
7476
+
7299
7477
  function buildHostContinueResult(parsedSession) {
7300
7478
  if (parsedSession.host === "claude-code" && parsedSession.id) {
7301
7479
  return {
@@ -7555,7 +7733,12 @@ async function resumeCodexThread(threadId) {
7555
7733
  }
7556
7734
 
7557
7735
  async function startCodexTurn(threadId, formattedMessage, options = {}) {
7558
- return sendCodexInstruction({ threadId, message: formattedMessage, timeout: options.timeout || 300000 });
7736
+ return sendCodexInstruction({
7737
+ threadId,
7738
+ message: formattedMessage,
7739
+ timeout: options.timeout || 300000,
7740
+ waitCompletion: Boolean(options.waitCompletion)
7741
+ });
7559
7742
  }
7560
7743
 
7561
7744
  async function listCodexThreads(options = {}) {
@@ -7569,7 +7752,7 @@ async function listCodexThreads(options = {}) {
7569
7752
  return { ok: true, threads: response.result?.data || response.result?.threads || response.result || [] };
7570
7753
  }
7571
7754
 
7572
- async function sendCodexInstruction({ threadId, message, timeout }) {
7755
+ async function sendCodexInstruction({ threadId, message, timeout, waitCompletion = false }) {
7573
7756
  const messages = [
7574
7757
  codexInitializeMessage(1),
7575
7758
  {
@@ -7592,23 +7775,26 @@ async function sendCodexInstruction({ threadId, message, timeout }) {
7592
7775
  threadId,
7593
7776
  input: [codexTextInput(message)]
7594
7777
  }
7595
- },
7596
- {
7778
+ }
7779
+ ];
7780
+ if (waitCompletion) {
7781
+ messages.push({
7597
7782
  jsonrpc: "2.0",
7598
7783
  id: 5,
7599
7784
  method: "thread/read",
7600
7785
  optional: true,
7601
7786
  params: { threadId, includeTurns: true }
7602
- }
7603
- ];
7604
- const result = await runCodexAppServer(messages, {
7787
+ });
7788
+ }
7789
+ const runOptions = waitCompletion ? {
7605
7790
  timeout,
7606
7791
  waitAfter: {
7607
7792
  id: 4,
7608
7793
  method: "turn/completed",
7609
7794
  timeout: Math.max(1000, parsePositiveInt(timeout, 300000))
7610
7795
  }
7611
- });
7796
+ } : { timeout };
7797
+ const result = await runCodexAppServer(messages, runOptions);
7612
7798
  if (!result.ok) {
7613
7799
  return { adapter: "codex", status: "failed", thread_id: threadId, warning: result.warning };
7614
7800
  }
@@ -7620,8 +7806,21 @@ async function sendCodexInstruction({ threadId, message, timeout }) {
7620
7806
  if (!start || start.error) {
7621
7807
  return { adapter: "codex", status: "failed", thread_id: threadId, warning: start?.error?.message || "Codex turn/start failed" };
7622
7808
  }
7623
- const completed = result.events.find((item) => item.method === "turn/completed");
7624
7809
  const started = result.events.find((item) => item.method === "turn/started");
7810
+ if (!waitCompletion) {
7811
+ return {
7812
+ adapter: "codex",
7813
+ status: "delivered",
7814
+ thread_id: threadId,
7815
+ turn_id: started?.params?.turnId || started?.params?.turn?.id || start.result?.turnId || null,
7816
+ completion_status: "not_waited",
7817
+ completed_at: null,
7818
+ verified_by_thread_read: false,
7819
+ warning: "Delivery only; target task completion must be verified by multiagent read, worklog, or a return instruction.",
7820
+ ui_visibility: "not_guaranteed"
7821
+ };
7822
+ }
7823
+ const completed = result.events.find((item) => item.method === "turn/completed");
7625
7824
  const finalRead = result.responses.find((item) => item.id === 5);
7626
7825
  return {
7627
7826
  adapter: "codex",
@@ -8764,7 +8963,7 @@ Subcommands:
8764
8963
  instruct 向另一个 lane 发送格式化跨会话指令。
8765
8964
  handoff 生成并记录人工交付消息,不后台发送。
8766
8965
  continue 输出继续某个 lane 宿主会话的人工命令或步骤。
8767
- launch 为已有 lane 创建并绑定 Codex thread。
8966
+ launch 为已有 lane 创建并绑定独立宿主会话。
8768
8967
  share 登记一个跨 lane 可读输出。
8769
8968
 
8770
8969
  示例:
@@ -8826,8 +9025,8 @@ Options:
8826
9025
 
8827
9026
  说明:
8828
9027
  --session-name 会在绑定成功后 best-effort 同步宿主工具的会话名称。
8829
- --pin 会尝试置顶 Codex thread;当前无稳定接口时只输出 warning,不回滚绑定。
8830
- Codex 支持自动观察和派发;Claude Code 支持 CLAUDE_CODE_SESSION_ID 自动识别和 resume 命令;Cursor / Trae v0.1 走人工交付。
9028
+ --pin 会请求宿主置顶;不支持时只输出 warning,不回滚绑定。
9029
+ instruct 的自动投递由 CLI 运行时判断;没有标准后台投递能力时返回 manual_handoff_required。
8831
9030
  `);
8832
9031
  }
8833
9032
 
@@ -8884,7 +9083,8 @@ Options:
8884
9083
  --target <path>
8885
9084
  --from <lane-id>
8886
9085
  --message <text>
8887
- --timeout <ms> 默认最多等待 300000ms,未完成时返回 started_unverified
9086
+ --timeout <ms>
9087
+ --wait, --wait-completion 显式等待目标 turn completed;默认只确认投递
8888
9088
  --json
8889
9089
  --dry-run
8890
9090
  --yes, -y
@@ -8935,7 +9135,7 @@ Usage:
8935
9135
  Options:
8936
9136
  --target <path>
8937
9137
  --lanes <lane1,lane2>
8938
- --session-name <name>
9138
+ --session-name <name> 覆盖默认 "<职责名> Agent" 会话名
8939
9139
  --pin
8940
9140
  --timeout <ms>
8941
9141
  --json
@@ -56,7 +56,7 @@ function listFiles(dir) {
56
56
  return result;
57
57
  }
58
58
 
59
- function fakeCodexBin({ exitCode = 0, stderr = "", inputPath, failTurnStart = false, omitFinalRead = false, omitTurnCompleted = false } = {}) {
59
+ function fakeCodexBin({ exitCode = 0, stderr = "", inputPath, failTurnStart = false, failThreadNameSet = false, omitFinalRead = false, omitTurnCompleted = false } = {}) {
60
60
  const dir = tempDir();
61
61
  const binDir = path.join(dir, "bin");
62
62
  fs.mkdirSync(binDir, { recursive: true });
@@ -77,6 +77,12 @@ rl.on("line", (line) => {
77
77
  if (request.method === "thread/read") {
78
78
  if (${Boolean(omitFinalRead)} && request.id >= 4) return;
79
79
  console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { thread: { id: request.params.threadId, name: "Fake Codex Thread", cwd: "/fake/project", status: "idle", turns: [{ id: "turn-1", status: "completed" }, { id: "turn-2", status: "completed" }] } } }));
80
+ } else if (request.method === "thread/name/set") {
81
+ if (${Boolean(failThreadNameSet)}) {
82
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { message: "rename failed" } }));
83
+ return;
84
+ }
85
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: {} }));
80
86
  } else if (request.method === "thread/start") {
81
87
  console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { threadId: "launched-thread-1" } }));
82
88
  } else if (request.method === "thread/list") {
@@ -117,6 +123,17 @@ test("prints version and product-oriented help", () => {
117
123
  assert.match(help.stdout, /starwork init --help/);
118
124
  });
119
125
 
126
+ test("starworkMultiagent skill delegates host routing to CLI", () => {
127
+ const skill = fs.readFileSync(path.join(root, "skills", "starworkMultiagent", "SKILL.md"), "utf8");
128
+
129
+ assert.doesNotMatch(skill, /\| Host \|/);
130
+ assert.doesNotMatch(skill, /Codex app-server/);
131
+ assert.doesNotMatch(skill, /Claude Code \|/);
132
+ assert.match(skill, /CLI 返回/);
133
+ assert.match(skill, /manual_handoff_required/);
134
+ assert.match(skill, /needs_adapt/);
135
+ });
136
+
120
137
  test("dry-run does not write files", () => {
121
138
  const dir = tempDir();
122
139
  const output = runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--dry-run"]);
@@ -844,7 +861,7 @@ test("multiagent status --host and read expose Codex observations", () => {
844
861
  assert.match(input, /"method":"thread\/resume"/);
845
862
  });
846
863
 
847
- test("multiagent instruct records shared request and sends formatted Codex instruction", () => {
864
+ test("multiagent instruct returns manual handoff for Codex when standard send is unavailable", () => {
848
865
  const dir = tempDir();
849
866
  const inputPath = path.join(tempDir(), "codex-input.jsonl");
850
867
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
@@ -865,27 +882,28 @@ test("multiagent instruct records shared request and sends formatted Codex instr
865
882
  const result = JSON.parse(instruct.stdout);
866
883
  const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
867
884
  const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
868
- const input = fs.readFileSync(inputPath, "utf8");
869
885
 
870
886
  assert.equal(instruct.status, 0);
871
- assert.equal(result.host_delivery.status, "completed");
887
+ assert.equal(result.schema, "starwork.agent_lanes.instruct.v0.4");
888
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
889
+ assert.equal(result.host_delivery.mode, "manual_handoff");
890
+ assert.match(result.host_delivery.warning, /standard background delivery capability is not available/);
872
891
  assert.match(shared, /Cross-Lane Requests/);
873
- assert.match(shared, /product-planning \| development \| 请开始实现 v0\.2。 \| completed \| completed/);
874
- assert.equal(state.requests[0].host_delivery.thread_id, "dev-thread-3");
875
- assert.match(input, /"method":"thread\/resume"/);
876
- assert.match(input, /"method":"turn\/start"/);
877
- assert.match(input, /STARWORK:MULTIAGENT_MESSAGE v1/);
892
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.2。 \| manual_handoff_required \| manual_handoff_required/);
893
+ assert.equal(state.requests[0].host_delivery.status, "manual_handoff_required");
894
+ assert.equal(fs.existsSync(inputPath), false);
878
895
  });
879
896
 
880
- test("multiagent instruct marks incomplete delivery as started_unverified", () => {
897
+ test("multiagent instruct does not use low-level Codex turn APIs even with wait requested", () => {
881
898
  const dir = tempDir();
899
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
882
900
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
883
901
  runCommand(["multiagent", "init", "--target", dir, "--yes"]);
884
902
  runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
885
903
  runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
886
904
  runCommand(["multiagent", "bind", "development", "--session", "codex:dev-thread-4", "--target", dir, "--yes"], { env: fakeCodexBin().env });
887
905
 
888
- const fakeCodex = fakeCodexBin({ omitTurnCompleted: true });
906
+ const fakeCodex = fakeCodexBin({ inputPath, omitTurnCompleted: true });
889
907
  const instruct = runCommand([
890
908
  "multiagent", "instruct", "development",
891
909
  "--from", "product-planning",
@@ -893,6 +911,7 @@ test("multiagent instruct marks incomplete delivery as started_unverified", () =
893
911
  "--target", dir,
894
912
  "--json",
895
913
  "--yes",
914
+ "--wait-completion",
896
915
  "--timeout", "1000"
897
916
  ], { env: fakeCodex.env });
898
917
  const result = JSON.parse(instruct.stdout);
@@ -900,10 +919,84 @@ test("multiagent instruct marks incomplete delivery as started_unverified", () =
900
919
  const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
901
920
 
902
921
  assert.equal(instruct.status, 0);
903
- assert.equal(result.host_delivery.status, "started_unverified");
904
- assert.match(result.host_delivery.verification_warning, /turn\/completed was not observed/);
905
- assert.match(shared, /product-planning \| development \| 请开始实现 v0\.3。 \| started_unverified \| started_unverified/);
906
- assert.equal(state.requests[0].host_delivery.status, "started_unverified");
922
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
923
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.3。 \| manual_handoff_required \| manual_handoff_required/);
924
+ assert.equal(state.requests[0].host_delivery.status, "manual_handoff_required");
925
+ assert.equal(fs.existsSync(inputPath), false);
926
+ });
927
+
928
+ test("multiagent instruct returns unbound when target lane has no session", () => {
929
+ const dir = tempDir();
930
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
931
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
932
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
933
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
934
+
935
+ const instruct = runCommand([
936
+ "multiagent", "instruct", "development",
937
+ "--from", "product-planning",
938
+ "--message", "请开始实现 v0.4。",
939
+ "--target", dir,
940
+ "--json",
941
+ "--yes"
942
+ ]);
943
+ const result = JSON.parse(instruct.stdout);
944
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
945
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
946
+
947
+ assert.equal(instruct.status, 0);
948
+ assert.equal(result.host_delivery.status, "unbound");
949
+ assert.match(result.host_delivery.warning, /Target lane is not bound/);
950
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.4。 \| unbound \| unbound/);
951
+ assert.equal(state.requests[0].host_delivery.status, "unbound");
952
+ });
953
+
954
+ test("multiagent instruct returns needs_adapt when a non-Codex host is not adapted", () => {
955
+ const dir = tempDir();
956
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
957
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
958
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
959
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
960
+ runCommand(["multiagent", "bind", "development", "--session", "cursor:cursor-thread-1", "--target", dir, "--yes"]);
961
+
962
+ const instruct = runCommand([
963
+ "multiagent", "instruct", "development",
964
+ "--from", "product-planning",
965
+ "--message", "请继续处理运行时路由。",
966
+ "--target", dir,
967
+ "--json",
968
+ "--yes"
969
+ ]);
970
+ const result = JSON.parse(instruct.stdout);
971
+
972
+ assert.equal(instruct.status, 0);
973
+ assert.equal(result.host_delivery.status, "needs_adapt");
974
+ assert.equal(result.host.id, "cursor");
975
+ assert.match(result.host_delivery.warning, /starwork adapt cursor/);
976
+ });
977
+
978
+ test("multiagent instruct returns manual handoff when adapted host lacks standard send", () => {
979
+ const dir = tempDir();
980
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
981
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
982
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
983
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
984
+ runCommand(["multiagent", "bind", "development", "--session", "cursor:cursor-thread-2", "--target", dir, "--yes"]);
985
+
986
+ const instruct = runCommand([
987
+ "multiagent", "instruct", "development",
988
+ "--from", "product-planning",
989
+ "--message", "请继续处理运行时路由。",
990
+ "--target", dir,
991
+ "--json",
992
+ "--yes"
993
+ ]);
994
+ const result = JSON.parse(instruct.stdout);
995
+
996
+ assert.equal(instruct.status, 0);
997
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
998
+ assert.equal(result.host_delivery.mode, "manual_handoff");
999
+ assert.match(result.host_delivery.formatted_message, /STARWORK:MULTIAGENT_MESSAGE v1/);
907
1000
  });
908
1001
 
909
1002
  test("multiagent bind detects Claude Code session from environment and outputs resume command", () => {
@@ -964,7 +1057,7 @@ test("multiagent read summarizes Claude Code transcript without dumping full tra
964
1057
 
965
1058
  test("multiagent instruct returns manual handoff for Trae lane instead of fake delivery", () => {
966
1059
  const dir = tempDir();
967
- runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1060
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "trae", "--yes"]);
968
1061
  runCommand(["multiagent", "init", "--target", dir, "--yes"]);
969
1062
  runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
970
1063
  runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
@@ -1027,6 +1120,59 @@ test("multiagent launch creates and binds Codex threads with launch message", ()
1027
1120
  assert.match(input, /StarWork MultiAgent Launch/);
1028
1121
  });
1029
1122
 
1123
+ test("multiagent launch names each batch-created Codex thread by lane role", () => {
1124
+ const dir = tempDir();
1125
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1126
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1127
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1128
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1129
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1130
+
1131
+ const fakeCodex = fakeCodexBin({ inputPath });
1132
+ const launch = runCommand([
1133
+ "multiagent", "launch",
1134
+ "--lanes", "product-planning,development",
1135
+ "--target", dir,
1136
+ "--json",
1137
+ "--yes",
1138
+ "--timeout", "1000"
1139
+ ], { env: fakeCodex.env });
1140
+ const result = JSON.parse(launch.stdout);
1141
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1142
+ const input = fs.readFileSync(inputPath, "utf8");
1143
+
1144
+ assert.equal(launch.status, 0);
1145
+ assert.equal(result.launches[0].session_name, "产品规划 Agent");
1146
+ assert.equal(result.launches[0].rename_status, "ok");
1147
+ assert.equal(result.launches[0].binding_status, "bound");
1148
+ assert.equal(result.launches[1].session_name, "功能开发 Agent");
1149
+ assert.equal(result.launches[1].rename_status, "ok");
1150
+ assert.equal(result.launches[1].binding_status, "bound");
1151
+ assert.equal(state.lanes["product-planning"].session_name, "产品规划 Agent");
1152
+ assert.equal(state.lanes.development.session_name, "功能开发 Agent");
1153
+ assert.match(input, /"name":"产品规划 Agent"/);
1154
+ assert.match(input, /"name":"功能开发 Agent"/);
1155
+ assert.doesNotMatch(result.launches[0].session_name, new RegExp(path.basename(dir)));
1156
+ });
1157
+
1158
+ test("multiagent launch warns when host session rename fails after creation", () => {
1159
+ const dir = tempDir();
1160
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1161
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1162
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1163
+
1164
+ const fakeCodex = fakeCodexBin({ failThreadNameSet: true });
1165
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes", "--timeout", "1000"], { env: fakeCodex.env });
1166
+ const result = JSON.parse(launch.stdout);
1167
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1168
+
1169
+ assert.equal(launch.status, 0);
1170
+ assert.equal(result.launches[0].binding_status, "bound");
1171
+ assert.equal(result.launches[0].rename_status, "warning");
1172
+ assert.match(result.launches[0].rename_warning, /rename failed/);
1173
+ assert.match(registry, /codex:launched-thread-1/);
1174
+ });
1175
+
1030
1176
  test("multiagent launch does not bind when launch message delivery fails", () => {
1031
1177
  const dir = tempDir();
1032
1178
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
@@ -1043,10 +1189,24 @@ test("multiagent launch does not bind when launch message delivery fails", () =>
1043
1189
  assert.equal(result.launches[0].status, "failed");
1044
1190
  assert.equal(result.launches[0].created_thread_id, "launched-thread-1");
1045
1191
  assert.equal(result.launches[0].thread_id, undefined);
1192
+ assert.equal(result.launches[0].binding_status, "unbound");
1046
1193
  assert.match(registry, /\| development \| 功能开发 \| unbound \|/);
1047
1194
  assert.equal(state.lanes.development?.thread_id, undefined);
1048
1195
  });
1049
1196
 
1197
+ test("multiagent launch refuses non-StarWork targets without sidecar initialization", () => {
1198
+ const dir = tempDir();
1199
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing project rules\n", "utf8");
1200
+
1201
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes"]);
1202
+
1203
+ assert.notEqual(launch.status, 0);
1204
+ assert.match(launch.stderr, /starworkInit/);
1205
+ assert.doesNotMatch(launch.stderr, /请先运行 starwork init/);
1206
+ assert.equal(fs.existsSync(path.join(dir, "AGENTS.starwork-new.md")), false);
1207
+ assert.equal(fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8"), "# Existing project rules\n");
1208
+ });
1209
+
1050
1210
  test("multiagent launch binds when final verification read times out after completion", () => {
1051
1211
  const dir = tempDir();
1052
1212
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
@@ -120,9 +120,9 @@ starwork spawn \
120
120
  starwork doctor --target ~/Desktop/starwork-alpha-project
121
121
  ```
122
122
 
123
- ### 4. 可选:验证 Codex 多会话协作
123
+ ### 4. 可选:验证 MultiAgent 会话创建与跨会话交付
124
124
 
125
- 这一步只适合正在使用 Codex,并且希望测试多个 Codex 会话协作的用户。
125
+ 这一步适合希望测试多个 Agent 职责位、独立会话创建和跨会话交付降级的用户。
126
126
 
127
127
  先初始化项目内的职责位:
128
128
 
@@ -133,15 +133,18 @@ starwork multiagent init \
133
133
  --yes
134
134
  ```
135
135
 
136
- development 职责位创建 Codex 会话并发送启动消息:
136
+ product-planning development 职责位批量创建独立会话并发送启动消息:
137
137
 
138
138
  ```bash
139
- starwork multiagent launch development \
139
+ starwork multiagent launch \
140
+ --lanes product-planning,development \
140
141
  --target ~/Desktop/starwork-alpha-project \
141
142
  --json \
142
143
  --yes
143
144
  ```
144
145
 
146
+ 检查 JSON 中每个 lane 的 `binding_status`。只有 `bound` 表示该 Agent 已创建并绑定可工作的独立会话;`rename_status` 为 `warning` 时,说明宿主会话自动命名失败,需要按返回信息处理。
147
+
145
148
  向 development 职责位发送一条跨会话指令:
146
149
 
147
150
  ```bash
@@ -153,6 +156,8 @@ starwork multiagent instruct development \
153
156
  --yes
154
157
  ```
155
158
 
159
+ 当前 CLI 只有在运行时发现宿主标准后台投递能力时才会返回 `delivered`。如果返回 `manual_handoff_required`,表示已生成可复制交付消息,需要用户手动发给目标会话;这不是失败。
160
+
156
161
  读取目标职责位当前可观察状态:
157
162
 
158
163
  ```bash
@@ -162,7 +167,7 @@ starwork multiagent read development \
162
167
  --json
163
168
  ```
164
169
 
165
- 预期:`launch` 和简单 `instruct` 应返回 `completed`。如果返回 `started_unverified`,说明 CLI 没观察到目标会话完成,应继续用 `read` 复核,不要把它当作已完成交付。
170
+ 预期:`launch` 成功时返回 `completed`;`instruct` 会根据 CLI 运行时宿主路由返回 `delivered`、`manual_handoff_required`、`needs_adapt`、`unbound` 等状态。`delivered` 只表示消息已投递,不表示目标任务完成;`manual_handoff_required` 表示需要手动复制交付消息。
166
171
 
167
172
  ### 5. 可选:验证 Host Adapter
168
173
 
@@ -198,6 +203,10 @@ starwork multiagent init \
198
203
  --target ~/Desktop/starwork-alpha-project \
199
204
  --yes
200
205
 
206
+ starwork adapt trae \
207
+ --target ~/Desktop/starwork-alpha-project \
208
+ --yes
209
+
201
210
  starwork multiagent bind development \
202
211
  --session trae:manual-dev-session \
203
212
  --target ~/Desktop/starwork-alpha-project \
@@ -243,11 +252,11 @@ Cursor / Trae 写入 Skill 目录后,宿主 UI 是否立即发现需要继续
243
252
  - 系统 skills 是否能被 Codex 识别和调用:`starworkInit`、`starworkDoctor`、`starworkMultiagent`、`starworkKnowledge`。
244
253
  - `starworkMultiagent` 是否能把“登记当前会话为常用智能体”正确转换成 `starwork multiagent init/add/bind` 建议。
245
254
  - `starwork multiagent bind --session-name` 是否能正确同步 Codex 宿主会话名;失败时是否能看懂 warning。
246
- - `starwork multiagent launch/instruct/read` Codex 中是否能完成多会话读取和指令交付;如果未完成,`started_unverified` 是否容易理解。
255
+ - `starwork multiagent launch/instruct/read` 是否能清楚区分会话创建、宿主观察、标准投递和人工交付;`manual_handoff_required` / `needs_adapt` / `unbound` 是否容易理解。
247
256
  - `starwork adapt --capabilities` 是否能帮助 Agent 判断不同宿主能力,而不是把内部字段甩给用户。
248
257
  - `starwork doctor --host <host>` 是否能区分“工作台结构问题”和“宿主入口 / Skill 目录问题”。
249
258
  - Cursor / Trae 在写入 `.cursor/skills/` / `.trae/skills/` 后是否需要重启、刷新窗口或重新打开项目。
250
- - Codex 宿主返回 `manual_handoff_required` 时,用户是否能理解“已生成交付消息,不等于已自动送达”。
259
+ - 返回 `manual_handoff_required` 时,用户是否能理解“已生成交付消息,不等于已自动送达”。
251
260
  - 项目中心自带的 `starworkSpawn`、`starworkAudit` 与项目工作台自带的 `neat-freak` 是否能在对应工作台内被发现。
252
261
 
253
262
  ## 发布前检查
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jennie-shawn/starwork",
3
- "version": "0.1.0-alpha.18",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "StarWork Core、CLI、Packs 和 Agent skills,用于构建 AI 工作台。",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/skills/README.md CHANGED
@@ -23,10 +23,10 @@ Skill 的安装、项目中心管理、Pack 推荐和项目分发规则见 `prod
23
23
 
24
24
  - `starworkInit/`:帮助 Agent 设计 `starwork init` 初始化方案和 init blueprint。详细 SPEC 见 `starworkInit-spec.md`。
25
25
  - `starworkDoctor/`:帮助 Agent 基于 `starwork doctor --json` 的探测结果,对当前工作区或历史模板做理性诊断;用户明确要求升级时,继续确认目录语义并生成 `starwork upgrade --blueprint` 升级施工图。详细 SPEC 见 `starworkDoctor-spec.md`。
26
- - `starworkMultiagent/`:帮助 Agent 把“常用智能体 / 当前会话职责 / 多 Agent 分工 / 共享输出 / 跨会话指令 / Codex lane thread 编排”翻译成安全的 `starwork multiagent` 命令组合。详细 SPEC 见 `starworkMultiagent-spec.md` 和 `product/planning/features/multiagent/specs/v0.2-codex-orchestration.md`。
26
+ - `starworkMultiagent/`:帮助 Agent 把“常用智能体 / 当前会话职责 / 多 Agent 分工 / 共享输出 / 跨会话指令 / lane session 编排”翻译成安全的 `starwork multiagent` 命令组合;宿主能力由 CLI 运行时判断。详细 SPEC 见 `starworkMultiagent-spec.md` 和 `product/planning/features/multiagent/specs/v0.4-runtime-host-routing.md`。
27
27
  - `starworkKnowledge/`:帮助 Agent 通过 `starwork knowledge` 开启、检查和维护项目本地知识库;负责判断哪些内容值得长期沉淀,区分 `pages/` 和 `synthesis/`,不默认提交项目中心。
28
28
 
29
- Host Adapter v0.1 已进入系统级 Skill:`starworkInit`、`starworkDoctor`、`starworkMultiagent`、`starworkKnowledge` 会引用 `starwork adapt` / `doctor --host` 的宿主能力结果,避免把 Codex 的自动会话能力误当成 Claude Code、Cursor 或 Trae 都支持。`starworkMultiagent` Claude Code 可生成 resume 命令和 transcript 摘要,对 Cursor / Trae 默认生成人工交付消息。
29
+ Host Adapter v0.1 已进入系统级 Skill:`starworkInit`、`starworkDoctor`、`starworkMultiagent`、`starworkKnowledge` 会引用 `starwork adapt` / `doctor --host` 的宿主准备结果。具体业务命令能否自动执行由 CLI 运行时判断;不支持标准后台投递时,`starworkMultiagent` 解释 CLI 返回的 `manual_handoff_required`、`needs_adapt`、`unbound` 等状态。
30
30
 
31
31
  `starworkAudit` 是项目中心工作台模板自带 skill,源码在 `product/kit-skills/starworkAudit/`;规格仍保留在 `starworkAudit-spec.md`。
32
32
  `starworkKnowledgeProject` 是知识库能力的项目内业务 skill,源码在 `product/core/capabilities/knowledge/skills/starworkKnowledgeProject/`;它不参与全局安装。
@@ -46,6 +46,18 @@ _system/collaboration/shared.md
46
46
 
47
47
  如果用户指定了目标目录,所有命令都加 `--target <path>`。如果没有指定,默认目标是当前工作区。
48
48
 
49
+ ## 前置边界:只在 StarWork 工作台内创建团队
50
+
51
+ `starworkInit` 负责把普通项目变成 StarWork 工作台;`starworkMultiagent` 只负责已有 StarWork 工作台里的团队协作。
52
+
53
+ 开始任何 `multiagent init/add/bind/launch` 写入前,先确认目标目录是 StarWork 工作台:
54
+
55
+ ```bash
56
+ starwork doctor --target <path> --json
57
+ ```
58
+
59
+ 如果目标不是 StarWork 工作台,立即停止 multiagent 写入,不要尝试局部初始化,不要新建 `AGENTS.starwork-new.md` 或只补 `_系统/协作/`。下一步是切换到 `starworkInit` Skill,由它采访用户、选择工作台类型和 Pack、处理已有规则入口,并在用户确认后调用 CLI。`starworkInit` 完成且 `starwork doctor --target <path>` 通过后,才继续 multiagent 流程。
60
+
49
61
  ## 判断用户意图
50
62
 
51
63
  优先把用户话语归到一个入口,不要一开始讲 CLI 子命令。
@@ -55,16 +67,34 @@ _system/collaboration/shared.md
55
67
  | “把当前会话创建为常用智能体,负责 X” | 登记当前会话为一个稳定职责位 | 必要时 `init`,再 `add`,再 `bind` |
56
68
  | “初始化多 Agent 协作层” | 创建 Agent Lanes 协议文件 | `multiagent init` |
57
69
  | “增加一个负责 X 的 Agent / lane” | 新增职责位,暂不一定绑定会话 | `multiagent add` |
58
- | “把当前 Codex / Claude Code 绑定到 X” | 将具体 session 绑定到已有 lane | `multiagent bind` |
70
+ | “把当前工具会话绑定到 X” | 将具体 session 绑定到已有 lane | `multiagent bind` |
59
71
  | “这个会话不再负责 X” | 释放 lane 当前绑定 | `multiagent release` |
60
72
  | “看看现在有哪些 Agent 分工” | 读取协作状态 | `multiagent status --json` |
61
73
  | “这个输出给其他 Agent 看” | 登记共享输出索引 | `multiagent share` |
62
74
  | “让开发 lane 开始开发” | 向目标 lane 发送格式化跨会话指令 | `multiagent instruct <lane>` |
63
75
  | “看看开发 lane 做到哪了” | 读取目标 lane 可用的宿主观察结果 | `multiagent read <lane>` 或 `multiagent status --host` |
64
76
  | “把这条消息交给另一个工具里的会话” | 生成并记录人工交付消息 | `multiagent handoff <lane>` |
65
- | “继续 Claude Code 里的这个 lane” | 输出 resume 命令或人工继续步骤 | `multiagent continue <lane>` |
66
- | “帮我创建产品、开发、验收三个智能体” | 为已有 lanes 创建并绑定 Codex threads | `multiagent launch --lanes ...` |
67
- | “把这个 lane 固定起来” | 绑定 lane best-effort 置顶 Codex thread | `multiagent bind --pin` |
77
+ | “继续这个 lane” | 输出 resume 命令或人工继续步骤 | `multiagent continue <lane>` |
78
+ | “帮我创建产品、开发、验收三个智能体” | 设计 lanes 后创建并绑定可工作的独立会话 | `multiagent add ...` + `multiagent launch --lanes ...` |
79
+ | “把这个 lane 固定起来” | 绑定 lane 后请求宿主置顶;是否支持由 CLI 返回 | `multiagent bind --pin` |
80
+
81
+ ## 常用流程:创建 Agent 团队
82
+
83
+ “创建 Agent 团队 / 创建多个智能体 / 产品、开发、验收三个智能体”不是只创建 lane。完整成功标准是:每个目标职责都有 lane、每个 lane 已绑定可工作的独立 session,或者输出中明确说明哪些 lane 未完成以及阻塞原因。
84
+
85
+ 1. 先按“前置边界”确认目标是 StarWork 工作台;非 StarWork 目标转 `starworkInit`,不要运行 `multiagent init` 做局部初始化。
86
+ 2. 读取 `agent-lanes.md` 和 `state.json`,判断哪些 lane 已存在,哪些需要新增。
87
+ 3. 对缺失 lane 先 dry-run `multiagent add`,确认 `lane-id`、职责和写入范围;用户确认后执行 `--yes`。
88
+ 4. 对需要独立 session 的 lanes,执行:
89
+
90
+ ```bash
91
+ starwork multiagent launch --lanes <lane1,lane2,lane3> --target <path> --json --yes
92
+ ```
93
+
94
+ 5. 检查 JSON 中每个 lane 的 `launch_status`、`rename_status` 和 `binding_status`。只有 `binding_status: "bound"` 才能告诉用户该 Agent 已创建并可工作;`unbound` 必须带 warning 和下一步。
95
+ 6. 默认会话名由 CLI 自动生成:`<职责名> Agent`。如果 `rename_status` 是 `warning`,说明宿主命名失败但 StarWork 绑定结果仍以 `binding_status` 为准。
96
+
97
+ 只有用户明确说“先只初始化协作层 / 先只建职责位 / lane-only”时,才可以停在 `multiagent init/add`。
68
98
 
69
99
  ## 常用流程:登记当前会话为常用智能体
70
100
 
@@ -83,7 +113,7 @@ starwork multiagent init --target <path> --dry-run
83
113
  - 可主动修改的路径范围。
84
114
  - 该 lane 的过程工作区,中文项目默认是 `_系统/协作/lanes/<lane-id>/workspace/`,英文项目默认是 `_system/collaboration/lanes/<lane-id>/workspace/`。
85
115
  - 当前 session ID;无法自动识别时,请用户提供 `agent:session-id`。
86
- - 宿主会话显示名称;仅当用户希望在 Codex 等宿主会话列表中同步改名时使用。
116
+ - 宿主会话显示名称;仅当用户希望在宿主会话列表中同步改名时使用。
87
117
  4. 生成 dry-run 命令:
88
118
 
89
119
  ```bash
@@ -99,32 +129,20 @@ starwork multiagent bind <lane> --session <agent:session-id> --session-name "<di
99
129
 
100
130
  ## 子命令使用规则
101
131
 
102
- ## 宿主能力分支
103
-
104
- 开始跨会话操作前,先看当前工作台是否已有 adapter state:
105
-
106
- ```bash
107
- starwork doctor --target <path> --host all --json
108
- ```
109
-
110
- 不同宿主能力不同,不能把 Codex 的能力当成所有工具都支持:
132
+ ## 宿主能力路由
111
133
 
112
- | Host | 使用方式 |
113
- |---|---|
114
- | Codex | 可以自动读取、创建和发送跨会话指令,但仍以 StarWork worklog 和 shared context 为准。 |
115
- | Claude Code | 可以绑定当前会话、读取 transcript 候选摘要、resume;不能后台发消息给非当前会话。 |
116
- | Cursor | 先按 partial 能力处理;自动化不可确认时生成可复制交付消息。 |
117
- | Trae | 默认是人工交付宿主;读取 StarWork worklog / handoff,不读加密数据库,不说自动送达。 |
134
+ Skill 不维护宿主能力矩阵,不根据工具名称自行判断能否自动发送、读取、创建或改名。凡是依赖宿主能力的动作,都调用 StarWork CLI,并根据 CLI 返回的结构化状态解释下一步。
118
135
 
119
- 如果用户要给 Claude Code、Cursor 或 Trae 做多会话协作,先确认对应宿主已经适配:
136
+ 常见 CLI 返回:
120
137
 
121
- ```bash
122
- starwork adapt <host> --target <path> --dry-run
123
- starwork adapt <host> --target <path> --yes
124
- starwork doctor --target <path> --host <host>
125
- ```
138
+ - `delivered`:消息已通过宿主标准能力投递;不代表目标任务完成。
139
+ - `manual_handoff_required`:CLI 已生成可复制交付消息,需要用户手动发给目标会话。
140
+ - `needs_adapt`:目标宿主还没准备好;引导用户先运行 `starwork adapt <host> --target <path> --dry-run`,确认后再执行。
141
+ - `unbound`:目标 lane 尚未绑定 session;先 `bind` 或 `launch`。
142
+ - `unsupported`:当前宿主明确不支持该能力。
143
+ - `failed`:CLI 尝试执行失败;保留项目记录,提示用户可重试或走 handoff。
126
144
 
127
- 不要把 `adapter profile`、`host_native_dirs`、`capabilities` 等内部词直接讲给用户;说“这个工具能自动做什么、哪些需要你手动复制”。
145
+ 解释时只说用户下一步,不展开宿主私有路径、数据库、transcript 或底层 API 细节。
128
146
 
129
147
  ### init
130
148
 
@@ -191,7 +209,7 @@ starwork multiagent bind <lane> --session <agent:session-id> --session-name "<di
191
209
  <项目或产品名> <职责> Agent
192
210
  ```
193
211
 
194
- 例如 `StarWork CLI 维护 Agent`。当前只有 Codex session 支持同步宿主会话名;Claude Code、Cursor、Trae 绑定时不要承诺能改宿主会话标题。失败只作为 warning,不影响绑定。
212
+ 例如 `StarWork CLI 维护 Agent`。是否能同步宿主标题由 CLI 返回;失败只作为 warning,不影响 StarWork binding。
195
213
 
196
214
  如果用户要求置顶这个 lane,可加入:
197
215
 
@@ -241,7 +259,7 @@ starwork multiagent status --host --target <path> --json
241
259
  starwork multiagent read <lane> --turns 5 --target <path> --json
242
260
  ```
243
261
 
244
- 解释时必须提醒:这是 Codex host observation,正式交接仍以 lane worklog 和 shared outputs 为准。Codex 前端不刷新不等于发送失败。
262
+ 解释时必须提醒:这是宿主观察结果,正式交接仍以 lane worklog 和 shared outputs 为准。宿主前端不刷新不等于发送失败。
245
263
 
246
264
  ### share
247
265
 
@@ -281,15 +299,15 @@ starwork multiagent share <from-lane> --title "<title>" --path "<relative-path>"
281
299
  starwork multiagent instruct <to-lane> --from <from-lane> --message "<text>" --target <path> --dry-run
282
300
  ```
283
301
 
284
- 用户确认后再执行 `--yes`。CLI 会先把请求写入 shared context,再通过 Codex app-server 发送格式化消息;发送失败时,项目内记录仍保留。
302
+ 用户确认后再执行 `--yes`。CLI 会先把请求写入 shared context,再判断宿主运行时能力:可标准投递则发送;不可标准投递则返回 handoff、needs_adapt、unbound、unsupported failed 等状态。发送失败时,项目内记录仍保留。
285
303
 
286
- 默认情况下,`instruct` 会等待目标 Codex turn 完成,避免过早关闭连接导致目标 turn 被打断。若返回 `started_unverified`,说明消息已经启动但 CLI 没有观察到完成;此时不要告诉用户“已经完成交付”,应立即运行:
304
+ 默认情况下,`instruct` 只确认消息已投递到目标 lane,返回 `delivered`。`delivered` 不等于目标任务完成,不要告诉用户“对方已经完成”。后续用 `read`、目标 lane worklog 或回传指令追踪:
287
305
 
288
306
  ```bash
289
307
  starwork multiagent read <to-lane> --turns 3 --target <path> --json
290
308
  ```
291
309
 
292
- 只有看到目标 turn 为 `completed`,或目标 lane 后续明确回报,才把这次跨会话指令视为完成。
310
+ 只有看到目标 turn 为 `completed`,或目标 lane 后续明确回报,才把这次跨会话指令视为完成。如果用户明确要求同步等待,再加 `--wait-completion`;若返回 `started_unverified`,只能说明已启动但未观察到完成。
293
311
 
294
312
  对非 Codex 宿主:
295
313
 
@@ -311,7 +329,7 @@ starwork multiagent continue <lane> --target <path> --json
311
329
 
312
330
  ### launch
313
331
 
314
- 为已有 lane 创建 Codex thread,并发送 StarWork 格式化 Launch Message。
332
+ 为已有 lane 创建独立宿主会话,并发送 StarWork 格式化 Launch Message。
315
333
 
316
334
  命令:
317
335
 
@@ -322,6 +340,12 @@ starwork multiagent launch --lanes product-planning,development,review --target
322
340
 
323
341
  不要自动创建 lane。lane 不存在时,先用 `multiagent add` 设计职责和写入范围。
324
342
 
343
+ 正式执行时优先使用 `--json`,并按结果判断:
344
+
345
+ - `launch_status` 表示宿主会话和 Launch Message 是否完成。
346
+ - `rename_status` 表示宿主会话命名是否完成;warning 需要告诉用户。
347
+ - `binding_status` 表示 StarWork lane 是否已绑定可工作的 session;不是 `bound` 时不能说团队创建完成。
348
+
325
349
  ## Lane Workspace 与正式输出
326
350
 
327
351
  每个 lane 默认有自己的过程工作区: