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
@@ -137,6 +137,8 @@ export function orchestrationSummary(input = {}) {
137
137
 
138
138
  /**
139
139
  * Decide which workers need a new wave release.
140
+ * Busy lanes still return reason "ready" — launchAgent FIFO-enqueues
141
+ * continue behind the current session (silent-wait must not block forever).
140
142
  * @param {{
141
143
  * pool: { tasks?: Array<object> },
142
144
  * workerCount: number,
@@ -153,7 +155,6 @@ export function pendingWaveReleases(input = {}) {
153
155
  ? input.doneSet
154
156
  : new Set([...(input.doneSet || [])].map(normId).filter(Boolean));
155
157
  const released = input.releasedWaves || {};
156
- const terminals = input.terminals || {};
157
158
  const out = [];
158
159
  for (let w = 1; w <= count; w += 1) {
159
160
  const workerId = `w${w}`;
@@ -162,16 +163,6 @@ export function pendingWaveReleases(input = {}) {
162
163
  const fp = fingerprintWave(wave.map((t) => t.id));
163
164
  const prior = Array.isArray(released[workerId]) ? released[workerId] : [];
164
165
  if (prior.includes(fp)) continue;
165
- const term = terminals[workerId] || {};
166
- if (term.busy || Number(term.queueDepth) > 0) {
167
- out.push({
168
- workerId,
169
- wave,
170
- fingerprint: fp,
171
- reason: "lane_busy",
172
- });
173
- continue;
174
- }
175
166
  out.push({
176
167
  workerId,
177
168
  wave,
@@ -66,7 +66,7 @@ the table for their app.
66
66
  | E2E-083 | After refresh, previously passed auto-validate keeps Confirm enabled | `npm test` + `npm run test:live` |
67
67
  | E2E-084 | Requirements fields edit as structured rows (add/remove), not raw textarea | `npm test` + `npm run test:live` |
68
68
  | E2E-085 | Setup 说明 covers Cursor/Claude install + other model wiring; header Model/Settings reopens it; setup.hint names Duaer desk model; guideDoc links locale-specific GitHub worker-models | `npm test` + `npm run test:live` |
69
- | E2E-086 | After Confirm, architecture dialogue + Archify HTML under hosting (no diagram-container height clip; every node click zooms + shows semantic passport popup in embed); gate dispatch; revise confirms plan then re-confirms architecture before dispatch; ready chat says diagram is under hosting; Confirm architecture clickable after render | `npm test` + `npm run test:live` |
69
+ | E2E-086 | After Confirm, architecture dialogue + Archify HTML under hosting (no diagram-container height clip; click diagram opens present fullscreen — embed does not zoom nodes); gate dispatch; revise confirms plan then re-confirms architecture before dispatch; ready chat says diagram is under hosting; Confirm architecture clickable after render | `npm test` + `npm run test:live` |
70
70
  | E2E-087 | After「派工并用…启动」succeeds, button stays「已派工」(persisted + restored from status); refresh / client timeout does not re-enable first launch | `npm test` + `npm run test:live` |
71
71
  | E2E-088 | After accept, Result panel always visible (查看结果 and/or 打开项目目录); progress column mirrors CTA; no-page jobs still show open-folder | `npm test` + `npm run test:live` |
72
72
  | E2E-089 | Revise keeps confirmed architecture; re-confirm only when architecture changes; prior diagram stays above new | `npm test` + `npm run test:live` |
@@ -89,7 +89,7 @@ the table for their app.
89
89
  | E2E-107 | 再改一版 reacts on first click (chat focus + kickoff before chrome; no accepted flip) | `npm run test:live` + manual |
90
90
  | E2E-108 | Revise kickoff keeps streamed model reply; safe chat done SSE (no stack-overflow wipe) | `npm run test:live` + manual |
91
91
  | E2E-109 | 再改一版 never shows Maximum call stack; persist/SSE omit deep IR | `npm run test:live` + manual |
92
- | E2E-110 | Architecture embed node click shows Archify semantic passport (`.focus-chip`) popup | `npm test` + manual |
92
+ | E2E-110 | Architecture embed click opens present fullscreen (no in-embed node zoom / passport) | `npm test` + manual |
93
93
  | E2E-111 | Served architecture HTML upgrades stale `#duaer-embed-fit` so passport CSS applies | `npm test` + manual |
94
94
  | E2E-112 | Architecture embed passport sits 100px left and 100px up from prior inset | `npm test` + manual |
95
95
  | E2E-113 | Architecture embed passport stays in-view (left flush, top 100px, high z-index) | `npm test` + manual |
@@ -103,8 +103,8 @@ the table for their app.
103
103
  | E2E-121 | After 改进方案确认, architecture panel sits below「请先确认架构」CTA | `npm run test:live` + manual |
104
104
  | E2E-122 | Architecture canvas expands to full diagram height (no iframe); node passport via Shadow mount | `npm test` + `npm run test:live` + manual |
105
105
  | E2E-123 | Architecture mounts inline (Shadow DOM), not iframe; GET `.json` for passport | `npm test` + manual |
106
- | E2E-124 | Inline mount keeps full Archify FX (theme CSS, motion, focus-chip, zoom) | `npm test` + manual |
107
- | E2E-125 | Node click boots Archify viewer (full body mount) → focus-chip + zoom | `npm test` + manual |
106
+ | E2E-124 | Inline mount keeps Archify FX (theme CSS, motion); click opens fullscreen present | `npm test` + manual |
107
+ | E2E-125 | Architecture mount click opens present fullscreen; embed reveal/zoom disabled | `npm test` + manual |
108
108
  | E2E-126 | Architecture mount height follows SVG canvas (not 100vh black box) | `npm test` + manual |
109
109
  | E2E-127 | Result bar Deploy next to 打开看看; none → GitHub Pages; POST /api/deploy | `npm test` + manual |
110
110
  | E2E-128 | Deploy click opens host picker before starting deploy | `npm test` + manual |
@@ -125,7 +125,7 @@ the table for their app.
125
125
  | E2E-143 | Cyclic architecture IR does not freeze live desk; deliverables page opens without Google Fonts | `npm test` + manual |
126
126
  | E2E-144 | Deliverables HTML has Contents TOC + structured card fields (dl/dt/dd) | `npm test` + manual |
127
127
  | E2E-145 | Multi-worker dispatch shows per-employee lanes under 派工进度 | `npm test` + `npm run test:live` + manual |
128
- | E2E-146 | Wave orchestration: only ready tasks in first prompt; status poll continues next wave when lane idle | `npm test` + manual |
128
+ | E2E-146 | Wave orchestration: only ready tasks in first prompt; status poll continues next wave when prior wave checked (enqueue even if lane busy; agent exits after wave) | `npm test` + manual |
129
129
  | E2E-147 | Deliverables HTML: numbered acceptance lists, chips, task table, confirmation registry | `npm test` + manual |
130
130
  | E2E-148 | Deliverables page two-column dossier layout (TOC rail + paper body) | `npm test` + manual |
131
131
  | E2E-149 | 初版 · 需求卡 accordion body uses structured lists/paragraphs (`req-list` / `req-para`); goal prose does not shatter on `、` | `npm test` + `npm run test:live` |
@@ -133,7 +133,7 @@ the table for their app.
133
133
  | E2E-151 | Modular `[模块]` card fields render section titles + numbered acceptance lists | `npm test` + `npm run test:live` |
134
134
  | E2E-152 | Project portfolio shows delivery status chips + open deliverables; Progress shows stage next-action | `npm test` + `npm run test:live` |
135
135
  | E2E-153 | `duaer live` starts without model config, opens desk URL, Settings guides setup | `npm test` + `npm run test:live` |
136
- | E2E-154 | 产品父目录 is read-only; 选择… opens native folder pick and saves; 清除 empties; no path typing | `npm test` + manual |
136
+ | E2E-154 | 产品父目录 is read-only; 选择… or path click opens frontmost native folder pick and saves; 清除 empties; no path typing | `npm test` + manual |
137
137
  | E2E-155 | FDE dispatch: prompts forbid Job not accepted yet; all-tasks-done nudges stamp accept | `npm test` + `npm run test:live` |
138
138
  | E2E-156 | Accepted chat bubble preview URL is a clickable `<a>` (http(s) or /api/…) | `npm test` + `npm run test:live` |
139
139
  | E2E-157 | `http://host/api/result/…` chat links render as one clean `<a>` (no nested HTML garble) | `npm test` |
@@ -143,6 +143,7 @@ the table for their app.
143
143
  | E2E-162 | Architecture / task-path click opens present=1 fullscreen in a new tab | `npm test` + `npm run test:live` |
144
144
  | E2E-163 | Task breakdown: multi-line acceptance → one atomic checkbox task per line; progress monitorable via `- [ ]`/`- [x]` | `npm test` |
145
145
  | E2E-164 | Live desk Japanese (`ja`) locale: select option, catalog parity with en, deliverables `lang=ja` | `npm test` + manual |
146
+ | E2E-165 | Top 数字员工 directory lists Implementer + Functional regression; kickoff tasks carry role | `npm test` + `npm run test:live` |
146
147
  | E2E-093 | Header top actions share one button chrome; GitHub control links to duaer-spec and shows live stars via `/api/github` | `npm run test:live` + manual |
147
148
  | E2E-063 | Duaer-spec FDE brand: header/title show Duaer-spec FDE; zh secondary mark is 现场开发; en mark is Field Development Environment; setup hint / desk-model guide name Duaer | `npm test` + manual |
148
149
  | E2E-078 | Claude Terminal launch includes `--permission-mode bypassPermissions` and stamps `hasTrustDialogAccepted` for the worktree | `npm test` + manual |
@@ -298,7 +299,7 @@ the table for their app.
298
299
  | E2E-122 | `155-arch-canvas-expand` | canvas expands; no iframe scroll |
299
300
  | E2E-123 | `156-arch-inline-mount` | Shadow DOM mount; no iframe |
300
301
  | E2E-124 | `157-arch-mount-fx` | full Archify styles + viewer FX |
301
- | E2E-125 | `158-arch-click-fx` | full body mount; click passport + zoom |
302
+ | E2E-125 | `158-arch-click-fx` | full body mount; click → present fullscreen (no embed zoom) |
302
303
  | E2E-126 | `159-arch-auto-height` | content-driven height; kill 100vh lock |
303
304
  | E2E-127 | `160-preview-deploy-btn` | result-bar Deploy; default Pages |
304
305
  | E2E-128 | `161-deploy-picker` | Deploy opens where-to-host dialog |
@@ -332,3 +333,4 @@ the table for their app.
332
333
  | E2E-162 | `201-arch-fullscreen` | architecture click → present fullscreen |
333
334
  | E2E-163 | `203-atomic-monitorable-tasks` | atomic verifiable tasks; checkbox progress |
334
335
  | E2E-164 | `204-live-i18n-ja` | Japanese locale on live desk |
336
+ | E2E-165 | `206-employee-directory` | employee directory + verify-l3 role |
@@ -46,7 +46,8 @@ HTML page generated from the project session:
46
46
  |---|---|
47
47
  | Requirements | Requirements document (version timeline) · confirmation of modules |
48
48
  | Architecture | Diagram link · confirmation note |
49
- | Kickoff & implementation | Brief/job id · task pool |
49
+ | Kickoff & implementation | Brief/job id · task pool · employee roles |
50
+
50
51
  | Delivery | Delivery stamp · preview URL |
51
52
  | Revisions | Each revise plan card (when present) |
52
53
 
@@ -74,6 +75,11 @@ registry linking into the requirements document (no duplicate full cards).
74
75
  Kickoff owns Brief creation and dispatch:
75
76
 
76
77
  - Builds a **dependency-aware task pool** (`dependsOn` edges).
78
+ - **Employee directory** (top bar): lists specialized roles — **Implementer**
79
+ and **Functional regression** (Acceptance → testing.md L0–L3 / Playwright).
80
+ Runtime remains Cursor Agent / Claude Code; kickoff tasks carry
81
+ `role: implement | verify-l3`. With workerCount ≥ 2, verify-l3 tasks go to
82
+ the last lane.
77
83
  - **Atomic + monitorable:** one checkbox task per independently verifiable
78
84
  acceptance line (plus impl / verify / shared steps). Agents must mark
79
85
  `- [x]` as each atom completes so the desk can poll progress and release waves.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duaer-spec",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Norms → satisfactory delivery: digital employees for Cursor, Claude, Codex, Copilot, Windsurf, Cline, Continue, Gemini, Aider",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@ import {
24
24
  } from "./architecture-mount.mjs";
25
25
  import { enrichChatOptions } from "./choice-options.mjs";
26
26
  import { buildTaskArchitectureIr } from "./task-graph.mjs";
27
+ import { EMPLOYEE_CATALOG } from "./employee-catalog.mjs";
27
28
 
28
29
  /** Deliverables API lang: en | ja | zh */
29
30
  function deliverablesLang() {
@@ -351,6 +352,10 @@ const el = {
351
352
  updateNotice: document.getElementById("updateNotice"),
352
353
  langSelect: document.getElementById("langSelect"),
353
354
  historyToggle: document.getElementById("historyToggle"),
355
+ employeeToggle: document.getElementById("employeeToggle"),
356
+ employeePanel: document.getElementById("employeePanel"),
357
+ employeeClose: document.getElementById("employeeClose"),
358
+ employeeList: document.getElementById("employeeList"),
354
359
  projectBadge: document.getElementById("projectBadge"),
355
360
  historyPanel: document.getElementById("historyPanel"),
356
361
  historyBackdrop: document.getElementById("historyBackdrop"),
@@ -1193,13 +1198,21 @@ function bindArchitecturePresentClick(host, getUrl) {
1193
1198
  if (!host || host.dataset.presentBound === "1") return;
1194
1199
  host.dataset.presentBound = "1";
1195
1200
  host.classList.add("architecture-mount-clickable");
1196
- host.addEventListener("click", (ev) => {
1197
- if (ev.target.closest?.("button, a, input, textarea, select")) return;
1198
- const url = typeof getUrl === "function" ? getUrl() : getUrl;
1199
- const href = String(url || host.dataset.archUrl || "").trim();
1200
- if (!href) return;
1201
- openArchitecturePresent(href);
1202
- });
1201
+ // Capture on the host so Shadow DOM node clicks open fullscreen instead of
1202
+ // Archify reveal/zoom in the embed.
1203
+ host.addEventListener(
1204
+ "click",
1205
+ (ev) => {
1206
+ if (ev.target.closest?.("button, a, input, textarea, select")) return;
1207
+ const url = typeof getUrl === "function" ? getUrl() : getUrl;
1208
+ const href = String(url || host.dataset.archUrl || "").trim();
1209
+ if (!href) return;
1210
+ ev.preventDefault();
1211
+ ev.stopPropagation();
1212
+ openArchitecturePresent(href);
1213
+ },
1214
+ true,
1215
+ );
1203
1216
  }
1204
1217
 
1205
1218
  function hydrateArchitectureMounts(root) {
@@ -2838,11 +2851,58 @@ function syncDrawerBackdrop() {
2838
2851
  if (!el.historyBackdrop) return;
2839
2852
  const open =
2840
2853
  (el.historyPanel && !el.historyPanel.hidden) ||
2841
- (el.settingsPanel && !el.settingsPanel.hidden);
2854
+ (el.settingsPanel && !el.settingsPanel.hidden) ||
2855
+ (el.employeePanel && !el.employeePanel.hidden);
2842
2856
  el.historyBackdrop.hidden = !open;
2843
2857
  document.body.classList.toggle("history-open", Boolean(open));
2844
2858
  }
2845
2859
 
2860
+ function setEmployeeOpen(open) {
2861
+ if (!el.employeePanel) return;
2862
+ const want = Boolean(open);
2863
+ if (want) {
2864
+ if (!state.ready) return;
2865
+ if (el.historyPanel && !el.historyPanel.hidden) {
2866
+ el.historyPanel.hidden = true;
2867
+ if (el.historyToggle) el.historyToggle.setAttribute("aria-expanded", "false");
2868
+ }
2869
+ if (el.settingsPanel && !el.settingsPanel.hidden) {
2870
+ el.settingsPanel.hidden = true;
2871
+ if (el.cfgOpen) el.cfgOpen.setAttribute("aria-expanded", "false");
2872
+ }
2873
+ }
2874
+ el.employeePanel.hidden = !want;
2875
+ if (el.employeeToggle) {
2876
+ el.employeeToggle.setAttribute("aria-expanded", want ? "true" : "false");
2877
+ }
2878
+ syncDrawerBackdrop();
2879
+ if (want) renderEmployeeList();
2880
+ }
2881
+
2882
+ function renderEmployeeList() {
2883
+ if (!el.employeeList) return;
2884
+ el.employeeList.replaceChildren();
2885
+ for (const emp of EMPLOYEE_CATALOG) {
2886
+ const row = document.createElement("article");
2887
+ row.className = "employee-card";
2888
+ row.setAttribute("role", "listitem");
2889
+ const name = document.createElement("h3");
2890
+ name.className = "employee-name";
2891
+ name.textContent = t(`employee.${emp.id}.name`);
2892
+ const role = document.createElement("p");
2893
+ role.className = "employee-role meta";
2894
+ role.textContent = t("employee.role", { role: emp.role });
2895
+ const cap = document.createElement("p");
2896
+ cap.className = "employee-cap";
2897
+ cap.textContent = t(`employee.${emp.id}.cap`);
2898
+ const runtime = document.createElement("p");
2899
+ runtime.className = "hint tight";
2900
+ runtime.textContent = t("employee.runtime");
2901
+ row.append(name, role, cap, runtime);
2902
+ el.employeeList.appendChild(row);
2903
+ }
2904
+ }
2905
+
2846
2906
  function setSettingsOpen(open) {
2847
2907
  if (!el.settingsPanel) return;
2848
2908
  const want = Boolean(open);
@@ -2852,6 +2912,12 @@ function setSettingsOpen(open) {
2852
2912
  el.historyToggle.setAttribute("aria-expanded", "false");
2853
2913
  }
2854
2914
  }
2915
+ if (want && el.employeePanel && !el.employeePanel.hidden) {
2916
+ el.employeePanel.hidden = true;
2917
+ if (el.employeeToggle) {
2918
+ el.employeeToggle.setAttribute("aria-expanded", "false");
2919
+ }
2920
+ }
2855
2921
  el.settingsPanel.hidden = !want;
2856
2922
  if (el.cfgOpen) {
2857
2923
  el.cfgOpen.setAttribute("aria-expanded", want ? "true" : "false");
@@ -5140,6 +5206,7 @@ el.pickProjectsRoot?.addEventListener("click", async () => {
5140
5206
  el.pickProjectsRoot.disabled = true;
5141
5207
  el.pickProjectsRoot.textContent = t("dispatch.pickProjectsRootBusy");
5142
5208
  }
5209
+ if (el.projectsRoot) el.projectsRoot.disabled = true;
5143
5210
  try {
5144
5211
  const res = await fetch("/api/projects/pick-root", { method: "POST" });
5145
5212
  const data = await res.json().catch(() => ({}));
@@ -5168,9 +5235,21 @@ el.pickProjectsRoot?.addEventListener("click", async () => {
5168
5235
  el.pickProjectsRoot.disabled = false;
5169
5236
  el.pickProjectsRoot.textContent = t("dispatch.pickProjectsRoot");
5170
5237
  }
5238
+ if (el.projectsRoot) el.projectsRoot.disabled = false;
5171
5239
  }
5172
5240
  });
5173
5241
 
5242
+ el.projectsRoot?.addEventListener("click", () => {
5243
+ if (el.pickProjectsRoot?.disabled) return;
5244
+ el.pickProjectsRoot?.click();
5245
+ });
5246
+ el.projectsRoot?.addEventListener("keydown", (ev) => {
5247
+ if (ev.key !== "Enter" && ev.key !== " ") return;
5248
+ ev.preventDefault();
5249
+ if (el.pickProjectsRoot?.disabled) return;
5250
+ el.pickProjectsRoot?.click();
5251
+ });
5252
+
5174
5253
  el.clearProjectsRoot?.addEventListener("click", async () => {
5175
5254
  if (el.historyErr) el.historyErr.hidden = true;
5176
5255
  try {
@@ -7103,6 +7182,12 @@ function setHistoryOpen(open) {
7103
7182
  el.settingsPanel.hidden = true;
7104
7183
  if (el.cfgOpen) el.cfgOpen.setAttribute("aria-expanded", "false");
7105
7184
  }
7185
+ if (want && el.employeePanel && !el.employeePanel.hidden) {
7186
+ el.employeePanel.hidden = true;
7187
+ if (el.employeeToggle) {
7188
+ el.employeeToggle.setAttribute("aria-expanded", "false");
7189
+ }
7190
+ }
7106
7191
  el.historyPanel.hidden = !want;
7107
7192
  if (el.historyToggle) {
7108
7193
  el.historyToggle.setAttribute("aria-expanded", want ? "true" : "false");
@@ -7480,6 +7565,15 @@ if (el.historyToggle) {
7480
7565
  setHistoryOpen(open);
7481
7566
  });
7482
7567
  }
7568
+ if (el.employeeToggle) {
7569
+ el.employeeToggle.addEventListener("click", () => {
7570
+ const open = el.employeePanel?.hidden !== false;
7571
+ setEmployeeOpen(open);
7572
+ });
7573
+ }
7574
+ if (el.employeeClose) {
7575
+ el.employeeClose.addEventListener("click", () => setEmployeeOpen(false));
7576
+ }
7483
7577
  el.openDeliverables?.addEventListener("click", () => {
7484
7578
  openDeliverablesPage();
7485
7579
  });
@@ -7492,6 +7586,10 @@ if (el.historyBackdrop) {
7492
7586
  if (state.ready) setSettingsOpen(false);
7493
7587
  return;
7494
7588
  }
7589
+ if (el.employeePanel && !el.employeePanel.hidden) {
7590
+ setEmployeeOpen(false);
7591
+ return;
7592
+ }
7495
7593
  setHistoryOpen(false);
7496
7594
  });
7497
7595
  }
@@ -7554,6 +7652,7 @@ onLocaleChange(() => {
7554
7652
  renderProjectList();
7555
7653
  renderWorkerCountList();
7556
7654
  syncTaskPoolPreview();
7655
+ if (el.employeePanel && !el.employeePanel.hidden) renderEmployeeList();
7557
7656
  if (el.previewPanel && !el.previewPanel.hidden && state.lastStatus) {
7558
7657
  renderPreview(state.lastStatus);
7559
7658
  } else {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Mount Archify architecture HTML into a page host (no iframe).
3
- * Full styles + viewer runtime (focus-chip, zoom, motion) via Shadow DOM
4
- * and a document proxy scoped to `.archify-root`.
3
+ * Full styles + viewer runtime (motion overlays) via Shadow DOM and a
4
+ * document proxy scoped to `.archify-root`. Desk clicks open fullscreen
5
+ * present view; embed node zoom is disabled.
5
6
  *
6
7
  * Must include the full <body> chrome (toolbar buttons etc.) — Archify's
7
8
  * viewer script null-derefs if #btn-preset / #btn-theme are missing.
@@ -61,8 +62,9 @@ function hostChromeCss() {
61
62
  box-sizing: border-box;
62
63
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
63
64
  }
64
- /* Desk: hide Archify chrome; keep diagram FX + focus-chip.
65
- DOM nodes stay present so the viewer script can bind them. */
65
+ /* Desk: hide Archify chrome; keep diagram FX.
66
+ DOM nodes stay present so the viewer script can bind them.
67
+ focus-chip stays hidden — click opens fullscreen, not node passport. */
66
68
  .archify-root .toolbar,
67
69
  .archify-root .header,
68
70
  .archify-root .cards,
@@ -114,25 +116,8 @@ function hostChromeCss() {
114
116
  max-height: none !important;
115
117
  }
116
118
  .archify-root .focus-chip {
117
- display: block !important;
118
- position: absolute !important;
119
- left: 0.75rem !important;
120
- top: 0.75rem !important;
121
- z-index: 10000 !important;
122
- width: min(22rem, calc(100% - 1.5rem - 100px)) !important;
123
- max-width: calc(100% - 1.5rem - 100px) !important;
124
- max-height: none !important;
125
- overflow: visible !important;
126
- pointer-events: auto !important;
127
- }
128
- .archify-root .focus-chip[hidden] {
129
119
  display: none !important;
130
120
  }
131
- .archify-root .focus-chip .relationship-lens-list {
132
- display: block !important;
133
- max-height: none !important;
134
- overflow: visible !important;
135
- }
136
121
  `;
137
122
  }
138
123
 
@@ -280,41 +265,17 @@ function createScopedDocument(rootEl, shadow) {
280
265
  });
281
266
  }
282
267
 
283
- function installDesktopReveal(Archify) {
268
+ /**
269
+ * Desk mounts open fullscreen on click; do not zoom/enlarge nodes in-embed.
270
+ * Keep Archify.view.reveal as a no-op so residual handlers cannot frame zoom.
271
+ */
272
+ function disableEmbedNodeZoom(Archify) {
284
273
  if (!Archify?.view || typeof Archify.view.reveal !== "function") return;
285
- if (Archify.view.__duaerEmbedZoom) return;
286
- const original = Archify.view.reveal.bind(Archify.view);
287
- Archify.view.reveal = function duaerReveal(ids, options) {
288
- const opts = Object.assign({}, options || {}, {
289
- includeNeighbors: false,
290
- maxScale: 2.6,
291
- padding: 28,
292
- });
293
- let forced = false;
294
- const desc = Object.getOwnPropertyDescriptor(window, "innerWidth");
295
- try {
296
- if ((window.innerWidth || 0) <= 720) {
297
- Object.defineProperty(window, "innerWidth", {
298
- configurable: true,
299
- get() {
300
- return 1280;
301
- },
302
- });
303
- forced = true;
304
- }
305
- return original(ids, opts);
306
- } finally {
307
- if (forced) {
308
- try {
309
- if (desc) Object.defineProperty(window, "innerWidth", desc);
310
- else delete window.innerWidth;
311
- } catch {
312
- /* ignore */
313
- }
314
- }
315
- }
274
+ if (Archify.view.__duaerEmbedNoZoom) return;
275
+ Archify.view.reveal = function duaerNoZoomReveal() {
276
+ /* desk: click opens present fullscreen instead */
316
277
  };
317
- Archify.view.__duaerEmbedZoom = true;
278
+ Archify.view.__duaerEmbedNoZoom = true;
318
279
  }
319
280
 
320
281
  function runViewerScript(code, scopedDocument) {
@@ -325,7 +286,7 @@ function runViewerScript(code, scopedDocument) {
325
286
  `"use strict";\n${code}\n;return typeof Archify !== "undefined" ? Archify : null;`,
326
287
  );
327
288
  const Archify = runner(scopedDocument, window);
328
- installDesktopReveal(Archify);
289
+ disableEmbedNodeZoom(Archify);
329
290
  return Archify;
330
291
  }
331
292
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Digital-employee directory: specialized roles for FDE kickoff / UI.
3
+ * Runtime is still Cursor Agent or Claude Code; role constrains the Brief prompt.
4
+ */
5
+
6
+ export const EMPLOYEE_ROLES = {
7
+ IMPLEMENT: "implement",
8
+ VERIFY_L3: "verify-l3",
9
+ };
10
+
11
+ /** Built-in catalog (id stable for i18n keys employee.<id>.*). */
12
+ export const EMPLOYEE_CATALOG = [
13
+ {
14
+ id: "implementer",
15
+ role: EMPLOYEE_ROLES.IMPLEMENT,
16
+ runtime: "cursor-or-claude",
17
+ defaultOn: true,
18
+ },
19
+ {
20
+ id: "regression",
21
+ role: EMPLOYEE_ROLES.VERIFY_L3,
22
+ runtime: "cursor-or-claude",
23
+ defaultOn: true,
24
+ },
25
+ ];
26
+
27
+ export function employeeByRole(role) {
28
+ return EMPLOYEE_CATALOG.find((e) => e.role === role) || null;
29
+ }
30
+
31
+ export function clipEmployeeRole(raw) {
32
+ const r = String(raw || "").trim();
33
+ if (r === EMPLOYEE_ROLES.VERIFY_L3) return EMPLOYEE_ROLES.VERIFY_L3;
34
+ return EMPLOYEE_ROLES.IMPLEMENT;
35
+ }
36
+
37
+ /**
38
+ * Chinese dispatch blurb for a worker's owned roles (inserted into kickoff prompt).
39
+ */
40
+ export function rolePromptZh(roles) {
41
+ const set = new Set((roles || []).map(clipEmployeeRole));
42
+ const parts = [];
43
+ if (set.has(EMPLOYEE_ROLES.IMPLEMENT)) {
44
+ parts.push(
45
+ "【角色·实现】只做 role=implement 任务:按 Acceptance 实现功能;不要抢做 verify-l3 回归套件(除非本波只有你一人)。",
46
+ );
47
+ }
48
+ if (set.has(EMPLOYEE_ROLES.VERIFY_L3)) {
49
+ parts.push(
50
+ "【角色·功能回归】只做 role=verify-l3 任务:对照 Acceptance,按产品 .duaer/memory/testing.md 跑确定性回归(优先 Playwright / npm test / npm run test:live 等 L0–L3);写/补测试、执行并记录证据;不要改业务功能范围。失败则留下可复现说明,不要 stamp accepted。",
51
+ );
52
+ }
53
+ return parts.join("\n");
54
+ }
@@ -20,6 +20,17 @@ export const ja = {
20
20
  "history.meta": "{status} · {time}",
21
21
  "history.detailMeta": "{id} · {status} · {time}",
22
22
  "project.toggle": "プロジェクト",
23
+ "employee.toggle": "デジタル従業員",
24
+ "employee.mark": "Employees",
25
+ "employee.title": "デジタル従業員",
26
+ "employee.close": "閉じる",
27
+ "employee.lead": "キックオフはタスク役割で割当。実行は Cursor Agent / Claude Code。",
28
+ "employee.role": "役割 {role}",
29
+ "employee.runtime": "ランタイム · Cursor / Claude",
30
+ "employee.implementer.name": "実装従業員",
31
+ "employee.implementer.cap": "Acceptance に沿って実装。README 更新、プレビュー起動、delivery stamp。",
32
+ "employee.regression.name": "機能回帰",
33
+ "employee.regression.cap": "Acceptance → testing.md L0–L3 / Playwright。テスト作成・実行・証拠。業務範囲は変更しない。",
23
34
  "project.mark": "Project",
24
35
  "project.title": "プロジェクトとチャット",
25
36
  "project.close": "閉じる",
@@ -187,7 +198,7 @@ export const ja = {
187
198
  "dispatch.taskGraph": "タスク実行パス",
188
199
  "dispatch.taskGraphHint": "システムアーキテクチャと同じレイアウト;色はワーカーレーン。図をクリックで全画面",
189
200
  "dispatch.workerCount": "デジタル従業員",
190
- "dispatch.workerCountHint": "既定は 1 の直列;並列可能なとき人数を増やす(同一 CLI)",
201
+ "dispatch.workerCountHint": "既定は 1 の直列;複数時は最終レーンが機能回帰(verify-l3)",
191
202
  "dispatch.workerSerial": "直列",
192
203
  "dispatch.workerParallel": "並列",
193
204
  "dispatch.needModules": "先に全モジュールを確認してください",
@@ -29,6 +29,17 @@ const zhCN = {
29
29
  "history.meta": "{status} · {time}",
30
30
  "history.detailMeta": "{id} · {status} · {time}",
31
31
  "project.toggle": "项目",
32
+ "employee.toggle": "数字员工",
33
+ "employee.mark": "Employees",
34
+ "employee.title": "数字员工",
35
+ "employee.close": "关闭",
36
+ "employee.lead": "派工时按任务角色分配;运行时仍是 Cursor Agent / Claude Code。",
37
+ "employee.role": "角色 {role}",
38
+ "employee.runtime": "运行时 · Cursor / Claude",
39
+ "employee.implementer.name": "实现员工",
40
+ "employee.implementer.cap": "按 Acceptance 实现功能;更新 README;启动预览并 stamp 交付。",
41
+ "employee.regression.name": "功能回归",
42
+ "employee.regression.cap": "Acceptance → testing.md L0–L3 / Playwright;写测、跑测、留证据;不改业务范围。",
32
43
  "project.mark": "Project",
33
44
  "project.title": "项目与对话",
34
45
  "project.close": "关闭",
@@ -198,7 +209,7 @@ const zhCN = {
198
209
  "dispatch.taskGraph": "任务执行路径",
199
210
  "dispatch.taskGraphHint": "与系统架构图同一排版;色条表示数字员工分工。点击图可新页面全屏查看",
200
211
  "dispatch.workerCount": "数字员工数量",
201
- "dispatch.workerCountHint": "默认 1 人串行推进;可并行时选多人(同一 CLI)",
212
+ "dispatch.workerCountHint": "默认 1 人串行;多人时末位泳道做功能回归(verify-l3)",
202
213
  "dispatch.workerSerial": "串行",
203
214
  "dispatch.workerParallel": "并行",
204
215
  "dispatch.needModules": "请先确认全部模块",
@@ -548,6 +559,17 @@ const en = {
548
559
  "history.meta": "{status} · {time}",
549
560
  "history.detailMeta": "{id} · {status} · {time}",
550
561
  "project.toggle": "Projects",
562
+ "employee.toggle": "Employees",
563
+ "employee.mark": "Employees",
564
+ "employee.title": "Digital employees",
565
+ "employee.close": "Close",
566
+ "employee.lead": "Kickoff assigns by task role; runtime is still Cursor Agent / Claude Code.",
567
+ "employee.role": "Role {role}",
568
+ "employee.runtime": "Runtime · Cursor / Claude",
569
+ "employee.implementer.name": "Implementer",
570
+ "employee.implementer.cap": "Build to Acceptance; update README; start preview and stamp delivery.",
571
+ "employee.regression.name": "Functional regression",
572
+ "employee.regression.cap": "Acceptance → testing.md L0–L3 / Playwright; write, run, evidence; no product-scope edits.",
551
573
  "project.mark": "Project",
552
574
  "project.title": "Projects & chats",
553
575
  "project.close": "Close",
@@ -722,7 +744,7 @@ const en = {
722
744
  "dispatch.taskGraphHint": "Same layout as system architecture; colors show worker lanes. Click the diagram for fullscreen",
723
745
  "dispatch.workerCount": "Digital employees",
724
746
  "dispatch.workerCountHint":
725
- "Default 1 serial; pick more when tasks can run in parallel (same CLI)",
747
+ "Default 1 serial; with more workers the last lane owns verify-l3 regression",
726
748
  "dispatch.workerSerial": "Serial",
727
749
  "dispatch.workerParallel": "Parallel",
728
750
  "dispatch.needModules": "Confirm all modules first",
@@ -32,6 +32,15 @@
32
32
  <button type="button" class="top-btn" id="historyToggle" data-i18n="project.toggle">
33
33
  项目
34
34
  </button>
35
+ <button
36
+ type="button"
37
+ class="top-btn"
38
+ id="employeeToggle"
39
+ data-i18n="employee.toggle"
40
+ aria-expanded="false"
41
+ >
42
+ 数字员工
43
+ </button>
35
44
  <button type="button" class="top-btn" id="cfgOpen" data-i18n="setup.open" aria-expanded="false">
36
45
  设置
37
46
  </button>
@@ -346,8 +355,11 @@
346
355
  id="projectsRoot"
347
356
  type="text"
348
357
  readonly
358
+ class="projects-root-field"
349
359
  data-i18n-placeholder="dispatch.projectsRootEmpty"
360
+ data-i18n-title="dispatch.pickProjectsRoot"
350
361
  placeholder="尚未选择"
362
+ title="选择…"
351
363
  autocomplete="off"
352
364
  />
353
365
  </label>
@@ -389,6 +401,20 @@
389
401
  </div>
390
402
  </aside>
391
403
 
404
+ <aside id="employeePanel" class="history-drawer panel employee-drawer" hidden>
405
+ <div class="history-head">
406
+ <div>
407
+ <p class="mark" data-i18n="employee.mark">Employees</p>
408
+ <h2 class="history-title" data-i18n="employee.title">数字员工</h2>
409
+ </div>
410
+ <button type="button" class="btn history-close" id="employeeClose" data-i18n="employee.close">
411
+ 关闭
412
+ </button>
413
+ </div>
414
+ <p class="hint tight" data-i18n="employee.lead">派工时按任务角色分配;运行时仍是 Cursor Agent / Claude Code。</p>
415
+ <div id="employeeList" class="employee-list" role="list"></div>
416
+ </aside>
417
+
392
418
  <aside id="settingsPanel" class="history-drawer panel settings-drawer" hidden>
393
419
  <div class="history-head">
394
420
  <div>