@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 +2 -2
- package/cli/src/cli.js +235 -35
- package/cli/test/init.test.js +176 -16
- package/docs/alpha-test-guide.md +16 -7
- package/package.json +1 -1
- package/skills/README.md +2 -2
- package/skills/starworkMultiagent/SKILL.md +57 -33
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.
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
1258
|
-
else console.log(`目标 lane
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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({
|
|
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.
|
|
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) =>
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
8830
|
-
|
|
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>
|
|
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
|
package/cli/test/init.test.js
CHANGED
|
@@ -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
|
|
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.
|
|
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。 \|
|
|
874
|
-
assert.equal(state.requests[0].host_delivery.
|
|
875
|
-
assert.
|
|
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
|
|
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, "
|
|
904
|
-
assert.match(
|
|
905
|
-
assert.
|
|
906
|
-
assert.equal(
|
|
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"]);
|
package/docs/alpha-test-guide.md
CHANGED
|
@@ -120,9 +120,9 @@ starwork spawn \
|
|
|
120
120
|
starwork doctor --target ~/Desktop/starwork-alpha-project
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### 4. 可选:验证
|
|
123
|
+
### 4. 可选:验证 MultiAgent 会话创建与跨会话交付
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
这一步适合希望测试多个 Agent 职责位、独立会话创建和跨会话交付降级的用户。
|
|
126
126
|
|
|
127
127
|
先初始化项目内的职责位:
|
|
128
128
|
|
|
@@ -133,15 +133,18 @@ starwork multiagent init \
|
|
|
133
133
|
--yes
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
为
|
|
136
|
+
为 product-planning 和 development 职责位批量创建独立会话并发送启动消息:
|
|
137
137
|
|
|
138
138
|
```bash
|
|
139
|
-
starwork multiagent launch
|
|
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`
|
|
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`
|
|
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
|
-
-
|
|
259
|
+
- 返回 `manual_handoff_required` 时,用户是否能理解“已生成交付消息,不等于已自动送达”。
|
|
251
260
|
- 项目中心自带的 `starworkSpawn`、`starworkAudit` 与项目工作台自带的 `neat-freak` 是否能在对应工作台内被发现。
|
|
252
261
|
|
|
253
262
|
## 发布前检查
|
package/package.json
CHANGED
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 分工 / 共享输出 / 跨会话指令 /
|
|
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`
|
|
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
|
-
|
|
|
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
|
-
|
|
|
66
|
-
| “帮我创建产品、开发、验收三个智能体” |
|
|
67
|
-
| “把这个 lane 固定起来” | 绑定 lane
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
常见 CLI 返回:
|
|
120
137
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
starwork adapt <host> --target <path> --
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
302
|
+
用户确认后再执行 `--yes`。CLI 会先把请求写入 shared context,再判断宿主运行时能力:可标准投递则发送;不可标准投递则返回 handoff、needs_adapt、unbound、unsupported 或 failed 等状态。发送失败时,项目内记录仍保留。
|
|
285
303
|
|
|
286
|
-
默认情况下,`instruct`
|
|
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
|
|
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 默认有自己的过程工作区:
|