duaer-spec 0.22.0 → 0.23.0

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.
Files changed (31) hide show
  1. package/.duaer/active-job.json +2 -3
  2. package/.duaer/specs/205-release-022/delivery.json +10 -0
  3. package/.duaer/specs/205-release-022/tasks.md +2 -2
  4. package/.duaer/specs/206-employee-directory/delivery.json +13 -0
  5. package/.duaer/specs/206-employee-directory/spec.md +32 -0
  6. package/.duaer/specs/206-employee-directory/tasks.md +6 -0
  7. package/.duaer/specs/207-fix-projects-root-pick/delivery.json +10 -0
  8. package/.duaer/specs/207-fix-projects-root-pick/spec.md +20 -0
  9. package/.duaer/specs/207-fix-projects-root-pick/tasks.md +5 -0
  10. package/.duaer/specs/208-arch-click-fullscreen/delivery.json +10 -0
  11. package/.duaer/specs/208-arch-click-fullscreen/spec.md +24 -0
  12. package/.duaer/specs/208-arch-click-fullscreen/tasks.md +6 -0
  13. package/.duaer/specs/209-orch-wave-deadlock/delivery.json +10 -0
  14. package/.duaer/specs/209-orch-wave-deadlock/spec.md +27 -0
  15. package/.duaer/specs/209-orch-wave-deadlock/tasks.md +5 -0
  16. package/.duaer/specs/210-release-023/spec.md +12 -0
  17. package/.duaer/specs/210-release-023/tasks.md +6 -0
  18. package/CHANGELOG.md +24 -0
  19. package/bin/duaer-live.mjs +114 -50
  20. package/bin/live-modules.mjs +31 -3
  21. package/bin/live-orchestrate.mjs +2 -11
  22. package/docs/agent/e2e-test-plan.md +9 -7
  23. package/docs/agent/live-desk.md +7 -1
  24. package/package.json +1 -1
  25. package/web/live-dev/app.js +107 -8
  26. package/web/live-dev/architecture-mount.mjs +16 -55
  27. package/web/live-dev/employee-catalog.mjs +54 -0
  28. package/web/live-dev/i18n-ja.js +12 -1
  29. package/web/live-dev/i18n.js +24 -2
  30. package/web/live-dev/index.html +26 -0
  31. package/web/live-dev/styles.css +41 -0
@@ -1,5 +1,4 @@
1
1
  {
2
- "featureDir": ".duaer/specs/205-release-022",
3
- "slug": "205-release-022",
4
- "updatedAt": "2026-09-19T07:35:00.000Z"
2
+ "feature": "210-release-023",
3
+ "specDir": ".duaer/specs/210-release-023"
5
4
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T07:37:00.000Z",
4
+ "verification": {
5
+ "levels": ["L1"],
6
+ "commands": ["npm test (168 pass)", "gh release v0.22.0", "npm view duaer-spec version → 0.22.0"],
7
+ "result": "pass"
8
+ },
9
+ "summary": "Released 0.22.0 to GitHub and npm (ja locale + atomic tasks)."
10
+ }
@@ -2,5 +2,5 @@
2
2
 
3
3
  - [x] T001 Bump package.json to 0.22.0 and move CHANGELOG Unreleased
4
4
  - [x] T002 Merge release branch to develop
5
- - [ ] T003 Promote develop → main, push, tag, gh release, npm
6
- - [ ] T004 Stamp delivery accepted
5
+ - [x] T003 Promote develop → main, push, tag, gh release, npm
6
+ - [x] T004 Stamp delivery accepted
@@ -0,0 +1,13 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T12:45:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1", "L3"],
6
+ "commands": [
7
+ "npm test (173 pass)",
8
+ "npm run test:live"
9
+ ],
10
+ "result": "pass"
11
+ },
12
+ "summary": "Top employee directory with Implementer + Functional regression; task roles and verify-l3 lane assignment."
13
+ }
@@ -0,0 +1,32 @@
1
+ # Feature Specification: Digital employee directory + role specialization
2
+
3
+ **Feature Branch**: `feat/employee-directory`
4
+
5
+ ## Goal
6
+
7
+ Add a top-bar **数字员工** directory listing specialized employees. Seed at
8
+ least: **Implementer** and **Functional regression** (Acceptance → Playwright /
9
+ testing.md L3). Kickoff tasks carry a `role`; multi-worker dispatch prefers
10
+ verify-l3 tasks on a dedicated lane.
11
+
12
+ ## In scope
13
+
14
+ - Top button + drawer (same pattern as Projects)
15
+ - Catalog: implementer + regression (capabilities copy via i18n zh/en/ja)
16
+ - Task pool `role: implement | verify-l3`; markdown shows role
17
+ - When workerCount ≥ 2, assign verify-l3 tasks to the last worker lane
18
+ - Kickoff prompts include role blurb for owned roles
19
+ - Docs / E2E / tests
20
+
21
+ ## Out of scope
22
+
23
+ - New CLI runtimes (still Cursor / Claude)
24
+ - jev-ultrafast / browser-use agent
25
+ - User-editable custom employees
26
+
27
+ ## Acceptance
28
+
29
+ 1. Top bar has 数字员工; drawer lists implementer + regression with capability text
30
+ 2. Kickoff task-pool.json tasks include `role`
31
+ 3. workerCount≥2 puts verify-l3 tasks on last worker
32
+ 4. Unit tests + E2E catalog row; live smoke markers if needed
@@ -0,0 +1,6 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Employee catalog module + task role on pool
4
+ - [x] T002 Top directory drawer UI + i18n
5
+ - [x] T003 Assign verify-l3 to last lane; kickoff role prompts
6
+ - [x] T004 Docs tests CHANGELOG stamp
@@ -0,0 +1,10 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T12:55:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1"],
6
+ "commands": ["node --check bin/duaer-live.mjs", "npm test (173 pass)"],
7
+ "result": "pass"
8
+ },
9
+ "summary": "Parent folder picker activates Finder (frontmost) and path field click opens pick; async so HTTP stays responsive."
10
+ }
@@ -0,0 +1,20 @@
1
+ # Fix: projects parent folder picker unusable
2
+
3
+ ## Symptom
4
+
5
+ Clicking 产品父目录 / 选择… does not yield a usable folder choice (dialog
6
+ behind other windows, or readonly field ignores clicks).
7
+
8
+ ## Cause
9
+
10
+ `pickFolderNative` uses `osascript choose folder` without activating a UI
11
+ app, so the dialog often stays behind the browser. The path field is
12
+ readonly with no click handler.
13
+
14
+ ## Acceptance
15
+
16
+ 1. Clicking **选择…** or the readonly parent path opens a frontmost macOS
17
+ folder dialog (Finder-activated).
18
+ 2. Pick is async (HTTP event loop not blocked by spawnSync).
19
+ 3. Cancel leaves previous `projectsRoot` unchanged.
20
+ 4. Unit markers + L0 check.
@@ -0,0 +1,5 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Async Finder-activated pickFolderNative
4
+ - [x] T002 Path field click opens pick; tests/docs
5
+ - [x] T003 Stamp accepted
@@ -0,0 +1,10 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T13:10:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1"],
6
+ "commands": ["node --test test/arch-fullscreen.test.mjs test/live-archify.test.mjs", "npm test"],
7
+ "result": "pass"
8
+ },
9
+ "summary": "Architecture mount click opens present fullscreen only; embed node zoom disabled."
10
+ }
@@ -0,0 +1,24 @@
1
+ # Spec: Architecture click opens fullscreen only
2
+
3
+ ## Goal
4
+
5
+ On the live desk system-architecture (and same-style) mounts, a click opens the
6
+ fullscreen present view. Node zoom / enlarge-on-click in the embed is removed.
7
+
8
+ ## In scope
9
+
10
+ - Host click (capture) opens `present=1` fullscreen and stops Archify node zoom
11
+ - Disable desk embed `installDesktopReveal` zoom path
12
+ - Update E2E catalog lines that still require embed node zoom
13
+
14
+ ## Out of scope
15
+
16
+ - Changing fullscreen present page itself
17
+ - Removing the explicit「全屏查看」button
18
+
19
+ ## Acceptance
20
+
21
+ - [ ] Clicking the architecture diagram opens fullscreen present (new tab)
22
+ - [ ] Clicking a node does not zoom / enlarge that node in the desk embed
23
+ - [ ] Task-path / revise mounts keep the same click → fullscreen behavior
24
+ - [ ] Tests and E2E catalog match the new behavior
@@ -0,0 +1,6 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Capture-phase click opens present; stop Archify node handlers
4
+ - [x] T002 Disable embed node zoom in architecture-mount
5
+ - [x] T003 Update E2E / CHANGELOG / unit tests
6
+ - [x] T004 Verify and stamp accepted
@@ -0,0 +1,10 @@
1
+ {
2
+ "status": "accepted",
3
+ "acceptedAt": "2026-09-19T13:20:00.000Z",
4
+ "verification": {
5
+ "levels": ["L0", "L1"],
6
+ "commands": ["npm test (173 pass)"],
7
+ "result": "pass"
8
+ },
9
+ "summary": "Wave continue enqueues while lane busy; agents exit after wave instead of silent hang deadlock."
10
+ }
@@ -0,0 +1,27 @@
1
+ # Spec: Fix orchestration wave deadlock after silent wait
2
+
3
+ ## Goal
4
+
5
+ After a wave finishes and `tasks.md` is checked, the orchestrator must release
6
+ the next ready wave. Agents must not sit in a live Terminal session waiting —
7
+ that keeps `lane_busy` and blocks continue forever.
8
+
9
+ ## In scope
10
+
11
+ - Prompt: after a wave, exit the CLI session so the runner becomes idle /
12
+ can drain a queued `--continue`
13
+ - `pendingWaveReleases`: enqueue next wave even when the lane is busy
14
+ (FIFO serialize); do not stall on `lane_busy`
15
+ - Accept-nudge: same enqueue-when-busy behavior
16
+ - Docs / tests / E2E note
17
+
18
+ ## Out of scope
19
+
20
+ - Changing dependsOn / atomic task splitting
21
+ - Fixing polluted startCommand content from other projects (separate)
22
+
23
+ ## Acceptance
24
+
25
+ - [ ] Status with T001 done + next wave ready enqueues/releases while lane busy
26
+ - [ ] Prompts no longer tell the agent to 静默停等 with the session held open
27
+ - [ ] Unit tests updated; npm test passes
@@ -0,0 +1,5 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Enqueue next wave even when lane busy
4
+ - [x] T002 Prompt: exit session after wave (no silent hang)
5
+ - [x] T003 Tests, CHANGELOG, E2E; stamp accepted
@@ -0,0 +1,12 @@
1
+ # 210 — Release 0.23.0
2
+
3
+ ## Goal
4
+
5
+ Ship 0.23.0: bump version + CHANGELOG, promote develop → main, push,
6
+ GitHub Release `v0.23.0`, trigger npm Trusted Publishing.
7
+
8
+ ## Acceptance
9
+
10
+ 1. `package.json` is `0.23.0`; CHANGELOG has `0.23.0` section (Unreleased cleared).
11
+ 2. `develop` and `main` both contain the release; tag `v0.23.0` pushed.
12
+ 3. GitHub Release `v0.23.0` published; npm shows `duaer-spec@0.23.0`.
@@ -0,0 +1,6 @@
1
+ # Tasks
2
+
3
+ - [ ] T001 Bump package.json to 0.23.0 and move CHANGELOG Unreleased
4
+ - [ ] T002 Merge release branch to develop
5
+ - [ ] T003 Promote develop → main, push, tag, gh release, npm
6
+ - [ ] T004 Stamp delivery accepted
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.0 — 2026-09-19
6
+
7
+ ### Fix: orchestration wave release no longer deadlocks on silent wait
8
+
9
+ - Next ready wave is FIFO-enqueued even when the Terminal lane is busy
10
+ - After a wave, agents exit the CLI session (no hang waiting for continue)
11
+
12
+ ### Fix: architecture embed click opens fullscreen only
13
+
14
+ - Desk diagram click (capture) opens present fullscreen and does not zoom nodes
15
+ - Embed `reveal` zoom path disabled; focus-chip hidden on the mount
16
+
17
+ ### Fix: projects parent folder picker frontmost + clickable path
18
+
19
+ - macOS folder dialog activates Finder so it is not buried behind the browser
20
+ - Pick is async (no spawnSync on the HTTP thread)
21
+ - Clicking the readonly parent path also opens the picker
22
+
23
+ ### Feat: digital employee directory + role specialization
24
+
25
+ - Top bar **数字员工** drawer lists Implementer and Functional regression
26
+ - Kickoff tasks carry `role: implement | verify-l3`; multi-worker puts
27
+ verify-l3 on the last lane; prompts include role blurbs
28
+
5
29
  ## 0.22.0 — 2026-09-19
6
30
 
7
31
  ### Feat: Japanese (ja) live desk locale
@@ -73,6 +73,7 @@ import {
73
73
  assignTasksToWorkers,
74
74
  clipWorkerCount,
75
75
  } from "./live-modules.mjs";
76
+ import { rolePromptZh } from "../web/live-dev/employee-catalog.mjs";
76
77
  import {
77
78
  doneIdsFromProgress,
78
79
  fingerprintWave,
@@ -1577,56 +1578,120 @@ function discoverRepos({ max = 40 } = {}) {
1577
1578
  const DISPATCH_MUST_FINISH_RULES = `
1578
1579
  禁止半途收尾:
1579
1580
  - 分派的每一项任务都必须做完;不得以「Job not accepted yet」或「⏳ … not accepted」作为最终回复(那会让编码停住)
1580
- - 波次间隙可以静默停等编排器 continue;停等时不要输出任何 not accepted / 未验收话术
1581
+ - 本波勾完且还有后续波次时:输出一行「本波完成,退出等编排器」后立刻结束本次 CLI 会话/进程退出(不要挂起等待);编排器会 FIFO 排队 --continue 放行下一波
1581
1582
  - 当你负责的任务与 stamp 全部完成后:必须把 delivery.json 标为 accepted(含 preview.url),最终只允许一行:✅ Job accepted — ready for your review.
1582
1583
  `.trim();
1583
1584
 
1584
1585
  function pickFolderNative(promptText = "选择产品仓库") {
1585
1586
  const prompt = String(promptText || "选择产品仓库").trim() || "选择产品仓库";
1586
1587
  if (process.platform === "darwin") {
1587
- const r = spawnSync(
1588
- "osascript",
1589
- ["-e", `POSIX path of (choose folder with prompt ${JSON.stringify(prompt)})`],
1590
- { encoding: "utf8", timeout: 300000 },
1591
- );
1592
- const err = String(r.stderr || "").trim();
1593
- const out = String(r.stdout || "").trim();
1594
- if (r.status !== 0) {
1595
- if (
1596
- /User canceled|cancelled|canceled|取消/i.test(err) ||
1597
- /User canceled|cancelled|canceled|取消/i.test(out)
1598
- ) {
1599
- const e = new Error("已取消选择");
1600
- e.code = "CANCELLED";
1601
- throw e;
1602
- }
1603
- throw new Error(err || out || "无法打开系统文件夹选择");
1604
- }
1605
- if (!out) {
1606
- const e = new Error("已取消选择");
1607
- e.code = "CANCELLED";
1608
- throw e;
1609
- }
1610
- return normalizeRepoPath(out);
1588
+ // Activate Finder so the sheet is frontmost (LaunchAgent / browser often
1589
+ // leave a bare `choose folder` dialog buried behind Cursor / Chrome).
1590
+ const script = `
1591
+ tell application "Finder"
1592
+ activate
1593
+ try
1594
+ set theFolder to choose folder with prompt ${JSON.stringify(prompt)}
1595
+ return POSIX path of theFolder
1596
+ on error errMsg number errNum
1597
+ if errNum is -128 then
1598
+ error "User canceled" number -128
1599
+ end if
1600
+ error errMsg number errNum
1601
+ end try
1602
+ end tell
1603
+ `.trim();
1604
+ return new Promise((resolve, reject) => {
1605
+ const child = spawn("osascript", ["-e", script], {
1606
+ env: process.env,
1607
+ });
1608
+ let stdout = "";
1609
+ let stderr = "";
1610
+ const timer = setTimeout(() => {
1611
+ try {
1612
+ child.kill("SIGTERM");
1613
+ } catch {
1614
+ /* ignore */
1615
+ }
1616
+ reject(new Error("选择文件夹超时"));
1617
+ }, 300000);
1618
+ child.stdout.on("data", (d) => {
1619
+ stdout += d;
1620
+ });
1621
+ child.stderr.on("data", (d) => {
1622
+ stderr += d;
1623
+ });
1624
+ child.on("error", (err) => {
1625
+ clearTimeout(timer);
1626
+ reject(err);
1627
+ });
1628
+ child.on("close", (code) => {
1629
+ clearTimeout(timer);
1630
+ const err = String(stderr || "").trim();
1631
+ const out = String(stdout || "").trim();
1632
+ if (code !== 0) {
1633
+ if (
1634
+ /User canceled|cancelled|canceled|取消|-128/i.test(err) ||
1635
+ /User canceled|cancelled|canceled|取消|-128/i.test(out)
1636
+ ) {
1637
+ const e = new Error("已取消选择");
1638
+ e.code = "CANCELLED";
1639
+ reject(e);
1640
+ return;
1641
+ }
1642
+ reject(new Error(err || out || "无法打开系统文件夹选择"));
1643
+ return;
1644
+ }
1645
+ if (!out) {
1646
+ const e = new Error("已取消选择");
1647
+ e.code = "CANCELLED";
1648
+ reject(e);
1649
+ return;
1650
+ }
1651
+ resolve(normalizeRepoPath(out));
1652
+ });
1653
+ });
1611
1654
  }
1612
1655
  if (process.platform === "linux") {
1613
- const r = spawnSync(
1614
- "zenity",
1615
- ["--file-selection", "--directory", `--title=${prompt}`],
1616
- { encoding: "utf8", timeout: 300000 },
1617
- );
1618
- if (r.status !== 0) {
1619
- const e = new Error("已取消选择");
1620
- e.code = "CANCELLED";
1621
- throw e;
1622
- }
1623
- return normalizeRepoPath(String(r.stdout || "").trim());
1656
+ return new Promise((resolve, reject) => {
1657
+ const child = spawn(
1658
+ "zenity",
1659
+ ["--file-selection", "--directory", `--title=${prompt}`],
1660
+ { env: process.env },
1661
+ );
1662
+ let stdout = "";
1663
+ const timer = setTimeout(() => {
1664
+ try {
1665
+ child.kill("SIGTERM");
1666
+ } catch {
1667
+ /* ignore */
1668
+ }
1669
+ reject(new Error("选择文件夹超时"));
1670
+ }, 300000);
1671
+ child.stdout.on("data", (d) => {
1672
+ stdout += d;
1673
+ });
1674
+ child.on("error", (err) => {
1675
+ clearTimeout(timer);
1676
+ reject(err);
1677
+ });
1678
+ child.on("close", (code) => {
1679
+ clearTimeout(timer);
1680
+ if (code !== 0) {
1681
+ const e = new Error("已取消选择");
1682
+ e.code = "CANCELLED";
1683
+ reject(e);
1684
+ return;
1685
+ }
1686
+ resolve(normalizeRepoPath(String(stdout || "").trim()));
1687
+ });
1688
+ });
1624
1689
  }
1625
1690
  const e = new Error(
1626
1691
  "当前系统暂不支持弹窗选文件夹,请用扫描列表或:duaer live repo add",
1627
1692
  );
1628
1693
  e.code = "UNSUPPORTED";
1629
- throw e;
1694
+ return Promise.reject(e);
1630
1695
  }
1631
1696
 
1632
1697
  function listReposForUi() {
@@ -3624,7 +3689,7 @@ Brief: ${featureDir}
3624
3689
  2. 只做 Brief 范围;以 Acceptance 为准交付可让人满意的成品(可核对结果,不是过程叙事)
3625
3690
  3. 按 .duaer/memory/testing.md(若有)做风险验证
3626
3691
  4. 每完成 tasks.md 中的一步,立刻把该行改成 - [x](Duaer-spec FDE 靠此显示细粒度进度与编排放行)
3627
- 4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波勾完后:若还有后续波次则静默停等编排器 continue(不要写 Job not accepted yet);若已无后续波次则继续完成 stamp accepted
3692
+ 4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波勾完后:若还有后续波次则输出「本波完成,退出等编排器」并立刻结束本次 CLI 会话(不要挂起;编排器用 --continue 放行);若已无后续波次则继续完成 stamp accepted
3628
3693
  4b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个勾选项只覆盖一个验收点;进度只能用 tasks.md 的 - [ ]/- [x] 监控(立刻勾选,不要攒到最后);不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按 Acceptance 扩成「一条验收一勾选」(仍用 T00x),保存后再做
3629
3694
  5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted——tasks.md 全部勾完还不够,必须 stamp;禁止停在 Job not accepted yet
3630
3695
  5b. 交付前必须更新产品仓 README(说明文档):与本次交付一致——做什么、模块/验收要点、如何运行或打开;需求变了就改 README,不要只改代码。英文 README 不得出现中文;若项目是中文说明则用 README.zh-CN.md(或项目既有约定),可夹英文术语
@@ -3703,6 +3768,10 @@ Brief: ${featureDir}
3703
3768
  const waveList = wave
3704
3769
  .map((t) => `- ${t.id}: ${t.title || t.id}`)
3705
3770
  .join("\n");
3771
+ const ownedRoles = [
3772
+ ...new Set(workerTasks.map((t) => t.role).filter(Boolean)),
3773
+ ];
3774
+ const roleBlurb = rolePromptZh(ownedRoles);
3706
3775
  const workerPrompt =
3707
3776
  assigned.workerCount === 1
3708
3777
  ? `${agentPrompt}
@@ -3710,8 +3779,8 @@ Brief: ${featureDir}
3710
3779
  ——
3711
3780
  编排波次(只做这些已放行任务):
3712
3781
  ${waveList}
3713
-
3714
- 本波全部改成 - [x] 后:若还有后续波次则静默停等编排器(不要写 Job not accepted yet);若已无后续任务则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要开始未放行 / 未满足 dependsOn 的任务。
3782
+ ${roleBlurb ? `\n${roleBlurb}\n` : ""}
3783
+ 本波全部改成 - [x] 后:若还有后续波次则输出「本波完成,退出等编排器」并立刻结束本次 CLI 会话(不要挂起;不要写 Job not accepted yet);若已无后续任务则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要开始未放行 / 未满足 dependsOn 的任务。
3715
3784
 
3716
3785
  ${DISPATCH_MUST_FINISH_RULES}
3717
3786
  `
@@ -3722,8 +3791,8 @@ ${DISPATCH_MUST_FINISH_RULES}
3722
3791
  你负责的全部任务:${ownedIds.join(", ") || "(none)"}。
3723
3792
  当前编排波次(只做这些):
3724
3793
  ${waveList}
3725
-
3726
- 本波全部改成 - [x] 后:若你还有后续波次则静默停等;若你负责的任务已全部做完且轮到 stamp,则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要写 Job not accepted yet。不要开始未放行 / 未满足 dependsOn 的任务。其他任务由同事或后续波次负责。不要改 Brief 范围外的东西。
3794
+ ${roleBlurb ? `\n${roleBlurb}\n` : ""}
3795
+ 本波全部改成 - [x] 后:若你还有后续波次则输出「本波完成,退出等编排器」并立刻结束本次 CLI 会话;若你负责的任务已全部做完且轮到 stamp,则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要写 Job not accepted yet。不要开始未放行 / 未满足 dependsOn 的任务。其他任务由同事或后续波次负责。不要改 Brief 范围外的东西。
3727
3796
 
3728
3797
  ${DISPATCH_MUST_FINISH_RULES}
3729
3798
  `;
@@ -5035,7 +5104,7 @@ ${waveList}
5035
5104
  要求:
5036
5105
  1. 只做本波已放行任务;立刻把完成项改成 tasks.md 的 - [x]
5037
5106
  2. 不要开始未放行 / 未满足 dependsOn 的任务
5038
- 3. 本波勾完后:若还有后续波次则静默停等编排器;若已无后续 / 需 stamp,则完成 stamp delivery.json accepted 并输出 ✅ Job accepted
5107
+ 3. 本波勾完后:若还有后续波次则输出「本波完成,退出等编排器」并立刻结束本次 CLI 会话;若已无后续 / 需 stamp,则完成 stamp delivery.json accepted 并输出 ✅ Job accepted
5039
5108
  4. 只在上述 worktree 内改动;不要推远程除非明确要求
5040
5109
 
5041
5110
  ${DISPATCH_MUST_FINISH_RULES}
@@ -5075,10 +5144,6 @@ ${DISPATCH_MUST_FINISH_RULES}
5075
5144
  Number(progress.total) > 0 &&
5076
5145
  Number(progress.done) >= Number(progress.total);
5077
5146
  const nudgeLane = "w1";
5078
- const nudgeTerm =
5079
- (workerCount > 1
5080
- ? terminalsByLane[nudgeLane]
5081
- : terminalsByLane.w1 || terminalsByLane.default) || {};
5082
5147
  const acceptFp = "__ACCEPT_NUDGE__";
5083
5148
  const priorNudge = Array.isArray(releasedWaves[nudgeLane])
5084
5149
  ? releasedWaves[nudgeLane]
@@ -5088,7 +5153,6 @@ ${DISPATCH_MUST_FINISH_RULES}
5088
5153
  !accepted &&
5089
5154
  agentId &&
5090
5155
  worktreePath &&
5091
- !(nudgeTerm.busy || Number(nudgeTerm.queueDepth || 0) > 0) &&
5092
5156
  !priorNudge.includes(acceptFp)
5093
5157
  ) {
5094
5158
  const brief = featureDir || dispatch.featureDir || "";
@@ -6043,7 +6107,7 @@ async function handleApi(req, res) {
6043
6107
  if (req.method === "POST" && url.pathname === "/api/repos/pick") {
6044
6108
  let chosen = null;
6045
6109
  try {
6046
- chosen = pickFolderNative();
6110
+ chosen = await pickFolderNative();
6047
6111
  const probe = probeRepo(chosen, {
6048
6112
  bootstrap: true,
6049
6113
  exact: true,
@@ -6532,7 +6596,7 @@ async function handleApi(req, res) {
6532
6596
 
6533
6597
  if (req.method === "POST" && url.pathname === "/api/projects/pick-root") {
6534
6598
  try {
6535
- const chosen = pickFolderNative("选择产品父目录");
6599
+ const chosen = await pickFolderNative("选择产品父目录");
6536
6600
  const abs = path.resolve(String(chosen || "").trim()).replace(/[\\/]+$/, "");
6537
6601
  if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
6538
6602
  throw new Error("请选择一个有效的文件夹");
@@ -3,6 +3,10 @@
3
3
  */
4
4
 
5
5
  import { splitAcceptanceLines } from "../web/live-dev/acceptance-lines.mjs";
6
+ import {
7
+ EMPLOYEE_ROLES,
8
+ clipEmployeeRole,
9
+ } from "../web/live-dev/employee-catalog.mjs";
6
10
 
7
11
  function clipCard(card) {
8
12
  if (!card || typeof card !== "object") {
@@ -244,6 +248,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
244
248
  moduleId: partial.moduleId || null,
245
249
  title: String(partial.title || "").slice(0, 200),
246
250
  dependsOn: Array.isArray(partial.dependsOn) ? partial.dependsOn.filter(Boolean) : [],
251
+ role: clipEmployeeRole(partial.role),
247
252
  status: "queued",
248
253
  workerId: null,
249
254
  };
@@ -257,6 +262,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
257
262
  moduleId: m.id,
258
263
  title: `Implement module «${m.title}»: ${truncate(m.card.goal, 100)}`,
259
264
  dependsOn: [],
265
+ role: EMPLOYEE_ROLES.IMPLEMENT,
260
266
  });
261
267
  const acceptLines = splitAcceptanceLines(m.card.acceptance);
262
268
  let prevId = impl.id;
@@ -266,6 +272,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
266
272
  moduleId: m.id,
267
273
  title: `Satisfy acceptance (alone) «${m.title}»: ${truncate(line, 120)}`,
268
274
  dependsOn: [prevId],
275
+ role: EMPLOYEE_ROLES.IMPLEMENT,
269
276
  });
270
277
  prevId = acc.id;
271
278
  }
@@ -274,6 +281,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
274
281
  moduleId: m.id,
275
282
  title: `Satisfy acceptance for «${m.title}»: ${truncate(m.card.acceptance || m.card.goal, 100)}`,
276
283
  dependsOn: [impl.id],
284
+ role: EMPLOYEE_ROLES.IMPLEMENT,
277
285
  });
278
286
  prevId = acc.id;
279
287
  }
@@ -281,6 +289,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
281
289
  moduleId: m.id,
282
290
  title: `Verify «${m.title}» against acceptance`,
283
291
  dependsOn: [prevId],
292
+ role: EMPLOYEE_ROLES.VERIFY_L3,
284
293
  });
285
294
  moduleVerifyId.set(m.id, verify.id);
286
295
 
@@ -301,20 +310,23 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
301
310
 
302
311
  const verifyAll = push({
303
312
  moduleId: null,
304
- title: "Risk-based verification per testing.md",
313
+ title: "Risk-based verification per testing.md (L0–L3 / Playwright when applicable)",
305
314
  dependsOn: [...moduleVerifyId.values()],
315
+ role: EMPLOYEE_ROLES.VERIFY_L3,
306
316
  });
307
317
  const readme = push({
308
318
  moduleId: null,
309
319
  title:
310
320
  "Update product README to match delivered requirements (modules/goal/acceptance/how to run)",
311
321
  dependsOn: [verifyAll.id],
322
+ role: EMPLOYEE_ROLES.IMPLEMENT,
312
323
  });
313
324
  push({
314
325
  moduleId: null,
315
326
  title:
316
327
  "Start preview service; stamp delivery.json accepted with preview.url",
317
328
  dependsOn: [readme.id],
329
+ role: EMPLOYEE_ROLES.IMPLEMENT,
318
330
  });
319
331
  if (deployNeeded) {
320
332
  push({
@@ -323,6 +335,7 @@ export function buildTaskPoolFromModules(modules, { deployNeeded = false, deploy
323
335
  deployTaskText ||
324
336
  "Deploy with GitHub CLI (`gh`) + Actions; write public URL to preview.url",
325
337
  dependsOn: [tasks[tasks.length - 1].id],
338
+ role: EMPLOYEE_ROLES.IMPLEMENT,
326
339
  });
327
340
  }
328
341
 
@@ -339,7 +352,8 @@ export function taskPoolToMarkdown(pool) {
339
352
  const deps =
340
353
  t.dependsOn?.length > 0 ? ` (depends: ${t.dependsOn.join(", ")})` : "";
341
354
  const mod = t.moduleId ? ` [${t.moduleId}]` : "";
342
- return `- [ ] ${t.id}${mod} ${t.title}${deps}`;
355
+ const role = t.role ? ` {${t.role}}` : "";
356
+ return `- [ ] ${t.id}${mod}${role} ${t.title}${deps}`;
343
357
  });
344
358
  return `# Tasks
345
359
 
@@ -347,6 +361,8 @@ ${lines.join("\n")}
347
361
 
348
362
  做完一步就立刻把对应项改成 \`- [x]\`,方便 Duaer-spec FDE 监控进度与编排放行。
349
363
 
364
+ **角色:** \`{implement}\` 实现员工 · \`{verify-l3}\` 功能回归(Acceptance → testing.md L0–L3 / Playwright)
365
+
350
366
  **原子任务规则(强制):**
351
367
  - 每个勾选项只做一个可独立验证的功能点;不要把多项验收揉进同一条
352
368
  - 进度必须可监控:只用 \`- [ ]\` / \`- [x]\` 勾选;禁止无 checkbox 的笼统进度叙事
@@ -358,10 +374,15 @@ ${lines.join("\n")}
358
374
  /**
359
375
  * Assign tasks to 1..N workers (same CLI family). Ready roots fan out;
360
376
  * dependent tasks inherit the worker of their first dependency when possible.
377
+ * When count ≥ 2, verify-l3 tasks go to the last lane (regression employee).
361
378
  */
362
379
  export function assignTasksToWorkers(pool, workerCount = 1) {
363
380
  const count = Math.max(1, Math.min(8, Number(workerCount) || 1));
364
- const tasks = (pool?.tasks || []).map((t) => ({ ...t, workerId: null }));
381
+ const tasks = (pool?.tasks || []).map((t) => ({
382
+ ...t,
383
+ role: clipEmployeeRole(t.role),
384
+ workerId: null,
385
+ }));
365
386
  if (count === 1) {
366
387
  for (const t of tasks) t.workerId = "w1";
367
388
  return { workerCount: 1, tasks };
@@ -394,6 +415,12 @@ export function assignTasksToWorkers(pool, workerCount = 1) {
394
415
  if (!t.moduleId) t.workerId = "w1";
395
416
  }
396
417
 
418
+ // Regression employee owns the last lane when multiple workers.
419
+ const verifyLane = `w${count}`;
420
+ for (const t of tasks) {
421
+ if (t.role === EMPLOYEE_ROLES.VERIFY_L3) t.workerId = verifyLane;
422
+ }
423
+
397
424
  return { workerCount: count, tasks };
398
425
  }
399
426
 
@@ -406,6 +433,7 @@ export function clipTaskPool(raw) {
406
433
  id: String(t.id || "").slice(0, 20),
407
434
  moduleId: t.moduleId ? String(t.moduleId).slice(0, 80) : null,
408
435
  title: String(t.title || "").slice(0, 200),
436
+ role: clipEmployeeRole(t.role),
409
437
  dependsOn: Array.isArray(t.dependsOn)
410
438
  ? t.dependsOn.map((x) => String(x).slice(0, 20)).slice(0, 20)
411
439
  : [],