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.
- package/.duaer/active-job.json +2 -3
- package/.duaer/specs/205-release-022/delivery.json +10 -0
- package/.duaer/specs/205-release-022/tasks.md +2 -2
- package/.duaer/specs/206-employee-directory/delivery.json +13 -0
- package/.duaer/specs/206-employee-directory/spec.md +32 -0
- package/.duaer/specs/206-employee-directory/tasks.md +6 -0
- package/.duaer/specs/207-fix-projects-root-pick/delivery.json +10 -0
- package/.duaer/specs/207-fix-projects-root-pick/spec.md +20 -0
- package/.duaer/specs/207-fix-projects-root-pick/tasks.md +5 -0
- package/.duaer/specs/208-arch-click-fullscreen/delivery.json +10 -0
- package/.duaer/specs/208-arch-click-fullscreen/spec.md +24 -0
- package/.duaer/specs/208-arch-click-fullscreen/tasks.md +6 -0
- package/.duaer/specs/209-orch-wave-deadlock/delivery.json +10 -0
- package/.duaer/specs/209-orch-wave-deadlock/spec.md +27 -0
- package/.duaer/specs/209-orch-wave-deadlock/tasks.md +5 -0
- package/.duaer/specs/210-release-023/spec.md +12 -0
- package/.duaer/specs/210-release-023/tasks.md +6 -0
- package/CHANGELOG.md +24 -0
- package/bin/duaer-live.mjs +114 -50
- package/bin/live-modules.mjs +31 -3
- package/bin/live-orchestrate.mjs +2 -11
- package/docs/agent/e2e-test-plan.md +9 -7
- package/docs/agent/live-desk.md +7 -1
- package/package.json +1 -1
- package/web/live-dev/app.js +107 -8
- package/web/live-dev/architecture-mount.mjs +16 -55
- package/web/live-dev/employee-catalog.mjs +54 -0
- package/web/live-dev/i18n-ja.js +12 -1
- package/web/live-dev/i18n.js +24 -2
- package/web/live-dev/index.html +26 -0
- package/web/live-dev/styles.css +41 -0
package/.duaer/active-job.json
CHANGED
|
@@ -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
|
-
- [
|
|
6
|
-
- [
|
|
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,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,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,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,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`.
|
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
|
package/bin/duaer-live.mjs
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
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 /
|
|
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]
|
|
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]
|
|
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.
|
|
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("请选择一个有效的文件夹");
|
package/bin/live-modules.mjs
CHANGED
|
@@ -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
|
-
|
|
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) => ({
|
|
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
|
: [],
|