duaer-spec 0.20.0 → 0.22.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 (64) hide show
  1. package/.claude/rules/duaer-spec.md +5 -2
  2. package/.cursor/rules/duaer-spec.mdc +5 -2
  3. package/.cursor/skills/duaer-do/SKILL.md +20 -2
  4. package/.cursor/skills/duaer-tasks/SKILL.md +8 -0
  5. package/.duaer/active-job.json +3 -3
  6. package/.duaer/memory/constitution.md +13 -0
  7. package/.duaer/specs/192-release-020/delivery.json +19 -0
  8. package/.duaer/specs/192-release-020/tasks.md +3 -3
  9. package/.duaer/specs/193-dispatch-must-accept/delivery.json +11 -0
  10. package/.duaer/specs/193-dispatch-must-accept/spec.md +17 -0
  11. package/.duaer/specs/193-dispatch-must-accept/tasks.md +6 -0
  12. package/.duaer/specs/194-chat-preview-link/delivery.json +11 -0
  13. package/.duaer/specs/194-chat-preview-link/spec.md +14 -0
  14. package/.duaer/specs/194-chat-preview-link/tasks.md +5 -0
  15. package/.duaer/specs/195-chat-link-garble/delivery.json +10 -0
  16. package/.duaer/specs/195-chat-link-garble/spec.md +17 -0
  17. package/.duaer/specs/195-chat-link-garble/tasks.md +5 -0
  18. package/.duaer/specs/196-task-exec-graph/delivery.json +11 -0
  19. package/.duaer/specs/196-task-exec-graph/spec.md +15 -0
  20. package/.duaer/specs/196-task-exec-graph/tasks.md +6 -0
  21. package/.duaer/specs/197-task-graph-archify/delivery.json +12 -0
  22. package/.duaer/specs/197-task-graph-archify/spec.md +14 -0
  23. package/.duaer/specs/197-task-graph-archify/tasks.md +5 -0
  24. package/.duaer/specs/198-task-graph-labels/delivery.json +12 -0
  25. package/.duaer/specs/198-task-graph-labels/spec.md +12 -0
  26. package/.duaer/specs/198-task-graph-labels/tasks.md +5 -0
  27. package/.duaer/specs/199-preview-open-url/delivery.json +12 -0
  28. package/.duaer/specs/199-preview-open-url/spec.md +16 -0
  29. package/.duaer/specs/199-preview-open-url/tasks.md +5 -0
  30. package/.duaer/specs/200-preview-start-url/delivery.json +11 -0
  31. package/.duaer/specs/200-preview-start-url/spec.md +15 -0
  32. package/.duaer/specs/200-preview-start-url/tasks.md +5 -0
  33. package/.duaer/specs/201-arch-fullscreen/delivery.json +11 -0
  34. package/.duaer/specs/201-arch-fullscreen/spec.md +15 -0
  35. package/.duaer/specs/201-arch-fullscreen/tasks.md +5 -0
  36. package/.duaer/specs/202-release-021/delivery.json +18 -0
  37. package/.duaer/specs/202-release-021/spec.md +12 -0
  38. package/.duaer/specs/202-release-021/tasks.md +5 -0
  39. package/.duaer/specs/203-atomic-monitorable-tasks/delivery.json +13 -0
  40. package/.duaer/specs/203-atomic-monitorable-tasks/spec.md +44 -0
  41. package/.duaer/specs/203-atomic-monitorable-tasks/tasks.md +8 -0
  42. package/.duaer/specs/204-live-i18n-ja/delivery.json +14 -0
  43. package/.duaer/specs/204-live-i18n-ja/spec.md +34 -0
  44. package/.duaer/specs/204-live-i18n-ja/tasks.md +7 -0
  45. package/.duaer/specs/205-release-022/spec.md +12 -0
  46. package/.duaer/specs/205-release-022/tasks.md +6 -0
  47. package/.duaer/templates/tasks-template.md +4 -0
  48. package/CHANGELOG.md +64 -0
  49. package/bin/duaer-live.mjs +117 -14
  50. package/bin/live-deliverables.mjs +116 -36
  51. package/bin/live-modules.mjs +37 -7
  52. package/bin/live-preview.mjs +63 -12
  53. package/bin/live-progress.mjs +7 -38
  54. package/docs/agent/e2e-test-plan.md +20 -1
  55. package/docs/agent/live-desk.md +11 -1
  56. package/package.json +1 -1
  57. package/web/live-dev/acceptance-lines.mjs +42 -0
  58. package/web/live-dev/app.js +260 -46
  59. package/web/live-dev/chat-markdown.mjs +43 -3
  60. package/web/live-dev/i18n-ja.js +483 -0
  61. package/web/live-dev/i18n.js +34 -13
  62. package/web/live-dev/index.html +14 -13
  63. package/web/live-dev/styles.css +82 -4
  64. package/web/live-dev/task-graph.mjs +351 -0
@@ -0,0 +1,7 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Add ja catalog + LOCALES/detectLocale/setLocale wiring
4
+ - [x] T002 Language select option in index.html
5
+ - [x] T003 app.js / deliverables / task-graph ja handling
6
+ - [x] T004 Tests + E2E + docs + CHANGELOG
7
+ - [x] T005 Stamp delivery accepted
@@ -0,0 +1,12 @@
1
+ # 205 — Release 0.22.0
2
+
3
+ ## Goal
4
+
5
+ Ship 0.22.0: bump version + CHANGELOG, promote develop → main, push,
6
+ GitHub Release `v0.22.0`, trigger npm Trusted Publishing.
7
+
8
+ ## Acceptance
9
+
10
+ 1. `package.json` is `0.22.0`; CHANGELOG has `0.22.0` section (Unreleased cleared).
11
+ 2. `develop` and `main` pushed with release commits.
12
+ 3. GitHub Release `v0.22.0` published; npm shows `duaer-spec@0.22.0`.
@@ -0,0 +1,6 @@
1
+ # Tasks
2
+
3
+ - [x] T001 Bump package.json to 0.22.0 and move CHANGELOG Unreleased
4
+ - [x] T002 Merge release branch to develop
5
+ - [ ] T003 Promote develop → main, push, tag, gh release, npm
6
+ - [ ] T004 Stamp delivery accepted
@@ -15,6 +15,10 @@ TDD-style tests-before-code remain encouraged when useful.
15
15
 
16
16
  **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
17
17
 
18
+ **Atomic + monitorable**: Each checkbox is one independently verifiable atom.
19
+ Progress is only via `- [ ]` / `- [x]` — no prose-only status. Split Acceptance
20
+ into one task per criterion; no artificial upper count.
21
+
18
22
  ## Format: `[ID] [P?] [Story] Description`
19
23
 
20
24
  - **[P]**: Can run in parallel (different files, no dependencies)
package/CHANGELOG.md CHANGED
@@ -2,6 +2,70 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.22.0 — 2026-09-19
6
+
7
+ ### Feat: Japanese (ja) live desk locale
8
+
9
+ - Language select adds 日本語; catalog covers all en keys
10
+ - Deliverables API accepts `lang=ja`; dates / HTML `lang` use ja-JP
11
+
12
+ ### Feat: atomic verifiable tasks with monitorable progress
13
+
14
+ - Constitution + duaer-do / duaer-tasks: decompose to one independently
15
+ verifiable atom per checkbox; progress only via `- [ ]` / `- [x]`
16
+ - Kickoff task pool and preview graph split multi-line acceptance into
17
+ one task per criterion so FDE can monitor and release waves
18
+
19
+ ## 0.21.0 — 2026-09-19
20
+
21
+ ### Feat: click architecture diagram for fullscreen present view
22
+
23
+ - User-facing hints explain layout / worker colors and click-to-fullscreen
24
+ - Click diagram (or「全屏查看」) opens Archify HTML with `?present=1` in a new tab
25
+
26
+ ### Fix: 打开看看 prefers project start URL over docs paths
27
+
28
+ - Ignore non-HTML `delivery.preview.url` (e.g. `docs/**/*.md`) for primary open
29
+ - Prefer inferred localhost service over static HTML candidates when both exist
30
+ - Status preview / chat link stay on the runnable product URL
31
+
32
+ ### Fix: 打开看看 opens the focused preview URL
33
+
34
+ - Result-bar open uses `lastPreviewUrl` for `/api/…` and public https; local
35
+ services still go through `/api/preview/ensure` without overwriting focus
36
+ - Chat「打开看看」keeps same-origin `/api/…` hrefs (no baked host)
37
+
38
+ ### Fix: task path diagram without redundant Archify labels
39
+
40
+ - Drop edge `depends` labels, legend cards, and subtitle on the kickoff task graph
41
+
42
+ ### Feat: task execution graph uses Archify (same as architecture)
43
+
44
+ - Kickoff task path renders via `/api/architecture/render` + architecture mount
45
+ - Left-to-right layered layout and visual effect match the system architecture diagram
46
+ - Display edges are transitively reduced so Archify routing stays valid
47
+
48
+ ### Feat: worker chips + task dependency execution graph
49
+
50
+ - Digital-employee count uses chip radiogroup (串行 / 并行 labels)
51
+ - Kickoff shows task path by dependsOn; colors follow worker lanes
52
+
53
+ ### Fix: garbled chat preview links for /api/result URLs
54
+
55
+ - Stop nesting bare `/api/result/…` auto-links inside `http://host/api/result/…` hrefs
56
+ - Accepted bubble shows a single clean「打开看看」link
57
+
58
+ ### Fix: clickable preview URL in accepted chat bubble
59
+
60
+ - Bare `http(s)://` and `/api/artifact|result/…` URLs become links in chat
61
+ - Accepted「打开看看」message uses a real `<a href>` (relative paths get an open chip)
62
+
63
+ ### Fix: FDE dispatch must finish every task
64
+
65
+ - Kickoff / wave / continue / revise prompts forbid `Job not accepted yet` as a final handoff
66
+ - When all `tasks.md` boxes are checked but delivery is still open, status poll nudges stamp accept once
67
+ - `duaer-do`: live-dispatch jobs must stamp accepted before stopping
68
+
5
69
  ## 0.20.0 — 2026-09-19
6
70
 
7
71
  ### Feat: pick projects parent folder via system dialog
@@ -80,7 +80,7 @@ import {
80
80
  pendingWaveReleases,
81
81
  waveForWorker,
82
82
  } from "./live-orchestrate.mjs";
83
- import { resolvePreviewPayload, ensureLocalPreviewService, probeLocalPreviewStatus } from "./live-preview.mjs";
83
+ import { resolvePreviewPayload, ensureLocalPreviewService, probeLocalPreviewStatus, pickOpenableResultEntry, isOpenableProductPreview } from "./live-preview.mjs";
84
84
  import { markClaudeWorkspacesTrusted } from "./live-claude-trust.mjs";
85
85
 
86
86
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -1573,6 +1573,14 @@ function discoverRepos({ max = 40 } = {}) {
1573
1573
  );
1574
1574
  }
1575
1575
 
1576
+ /** Rules appended to FDE kickoff / wave / continue / revise prompts. */
1577
+ const DISPATCH_MUST_FINISH_RULES = `
1578
+ 禁止半途收尾:
1579
+ - 分派的每一项任务都必须做完;不得以「Job not accepted yet」或「⏳ … not accepted」作为最终回复(那会让编码停住)
1580
+ - 波次间隙可以静默停等编排器 continue;停等时不要输出任何 not accepted / 未验收话术
1581
+ - 当你负责的任务与 stamp 全部完成后:必须把 delivery.json 标为 accepted(含 preview.url),最终只允许一行:✅ Job accepted — ready for your review.
1582
+ `.trim();
1583
+
1576
1584
  function pickFolderNative(promptText = "选择产品仓库") {
1577
1585
  const prompt = String(promptText || "选择产品仓库").trim() || "选择产品仓库";
1578
1586
  if (process.platform === "darwin") {
@@ -3616,14 +3624,16 @@ Brief: ${featureDir}
3616
3624
  2. 只做 Brief 范围;以 Acceptance 为准交付可让人满意的成品(可核对结果,不是过程叙事)
3617
3625
  3. 按 .duaer/memory/testing.md(若有)做风险验证
3618
3626
  4. 每完成 tasks.md 中的一步,立刻把该行改成 - [x](Duaer-spec FDE 靠此显示细粒度进度与编排放行)
3619
- 4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波全部勾完后停止等待下一波(编排器会 continue)
3620
- 4b. 拆任务:每个勾选项只覆盖一个可独立验收的功能点;不要把多项验收揉进同一条;不要人为限制条数(不必卡在 12 条内)。若仍偏粗,先按 Acceptance 扩成「一条功能一勾选」(仍用 T00x),保存后再做;小步勾选,不要攒到最后一次勾完
3621
- 5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted
3627
+ 4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波勾完后:若还有后续波次则静默停等编排器 continue(不要写 Job not accepted yet);若已无后续波次则继续完成 stamp accepted
3628
+ 4b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个勾选项只覆盖一个验收点;进度只能用 tasks.md 的 - [ ]/- [x] 监控(立刻勾选,不要攒到最后);不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按 Acceptance 扩成「一条验收一勾选」(仍用 T00x),保存后再做
3629
+ 5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted——tasks.md 全部勾完还不够,必须 stamp;禁止停在 Job not accepted yet
3622
3630
  5b. 交付前必须更新产品仓 README(说明文档):与本次交付一致——做什么、模块/验收要点、如何运行或打开;需求变了就改 README,不要只改代码。英文 README 不得出现中文;若项目是中文说明则用 README.zh-CN.md(或项目既有约定),可夹英文术语
3623
- 6. 必须在 delivery.json 写入 preview.url(满意交付的必填证据):页面用相对路径如 index.html;HTTP 服务用可打开地址如 http://localhost:8788——不要因「没有页面」而省略
3631
+ 6. 必须在 delivery.json 写入 preview.url(满意交付的必填证据):必须是可打开的成品入口——HTTP 服务用 http://localhost:…;静态页用 index.html 等 HTML。禁止把 docs/**/*.md 等说明文档当作 preview.url——不要因「没有页面」而省略
3624
3632
  6b. 若交付是 HTTP 服务:验收前必须先把服务跑起来(如 npm start),确认能打开 preview.url 后再 stamp accepted;不要只写地址却不启动
3625
3633
  7. 合入 develop 并 handoff 清理 worktree
3626
3634
  8. 文档语言:英文文档不得出现中文;中文文档可夹英文术语
3635
+
3636
+ ${DISPATCH_MUST_FINISH_RULES}
3627
3637
  ${deployPrompt}${architecturePrompt}`;
3628
3638
 
3629
3639
  let agentPrompt = String(startCommand || "").trim() || defaultPrompt;
@@ -3701,7 +3711,9 @@ Brief: ${featureDir}
3701
3711
  编排波次(只做这些已放行任务):
3702
3712
  ${waveList}
3703
3713
 
3704
- 本波全部改成 - [x] 后停止;不要开始未放行 / 未满足 dependsOn 的任务。编排器会在依赖就绪后继续派发。
3714
+ 本波全部改成 - [x] 后:若还有后续波次则静默停等编排器(不要写 Job not accepted yet);若已无后续任务则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要开始未放行 / 未满足 dependsOn 的任务。
3715
+
3716
+ ${DISPATCH_MUST_FINISH_RULES}
3705
3717
  `
3706
3718
  : `${agentPrompt}
3707
3719
 
@@ -3711,7 +3723,9 @@ ${waveList}
3711
3723
  当前编排波次(只做这些):
3712
3724
  ${waveList}
3713
3725
 
3714
- 本波全部改成 - [x] 后停止;不要开始未放行 / 未满足 dependsOn 的任务。其他任务由同事或后续波次负责。不要改 Brief 范围外的东西。
3726
+ 本波全部改成 - [x] 后:若你还有后续波次则静默停等;若你负责的任务已全部做完且轮到 stamp,则 stamp delivery.json accepted 并输出 ✅ Job accepted。不要写 Job not accepted yet。不要开始未放行 / 未满足 dependsOn 的任务。其他任务由同事或后续波次负责。不要改 Brief 范围外的东西。
3727
+
3728
+ ${DISPATCH_MUST_FINISH_RULES}
3715
3729
  `;
3716
3730
  const wLog =
3717
3731
  assigned.workerCount === 1
@@ -4229,11 +4243,13 @@ ${restated.keep}
4229
4243
  0. 本轮 Revision 已在 Duaer-spec FDE 自动验收通过。直接改;不要进入 Confirming intent;不要让用户从多个风格/方向选项里再选一次;不要反复确认需求
4230
4244
  1. 只做本轮 Revision ${revN} 范围,不要重做无关功能
4231
4245
  2. 立刻把 tasks.md 里 R${revN}-* 勾成 - [x](Duaer-spec FDE 靠此显示细粒度进度)
4232
- 2b. 拆任务:每个 R${revN}-* 只覆盖一个可独立验收的改动;不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按本轮 acceptance 扩成「一条改动一勾选」(仍用 R${revN}-*),保存后再做;小步勾选
4233
- 3. 对照本轮 Revision acceptance 全部满足后,才 stamp delivery.json 为 accepted,并必须更新 preview.url(页面路径或 http://localhost:… 服务地址,必填);若是服务须先启动并可打开
4246
+ 2b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个 R${revN}-* 只覆盖一个改动;进度用 - [ ]/- [x] 立刻勾选监控;不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按本轮 acceptance 扩成「一条改动一勾选」(仍用 R${revN}-*),保存后再做
4247
+ 3. 对照本轮 Revision acceptance 全部满足后,才 stamp delivery.json 为 accepted,并必须更新 preview.url 为可打开的成品入口(http://localhost:… 或 HTML 页面,必填;禁止用 docs/**/*.md 当预览);若是服务须先启动并可打开。禁止以 Job not accepted yet 收尾
4234
4248
  3b. 交付前必须更新产品仓 README(说明文档)以反映本轮改动后的行为/用法;需求变了就改 README,不要只改代码。英文 README 不得出现中文;中文说明用 README.zh-CN.md(或项目既有约定)
4235
4249
  4. 按 testing.md 做风险验证(若有)
4236
4250
  5. 不要推远程除非用户明确要求部署/发布
4251
+
4252
+ ${DISPATCH_MUST_FINISH_RULES}
4237
4253
  `;
4238
4254
 
4239
4255
  let agentPrompt = String(startCommand || "").trim() || defaultPrompt;
@@ -5019,8 +5035,10 @@ ${waveList}
5019
5035
  要求:
5020
5036
  1. 只做本波已放行任务;立刻把完成项改成 tasks.md 的 - [x]
5021
5037
  2. 不要开始未放行 / 未满足 dependsOn 的任务
5022
- 3. 本波全部勾完后停止;编排器会继续派发
5038
+ 3. 本波勾完后:若还有后续波次则静默停等编排器;若已无后续 / 需 stamp,则完成 stamp delivery.json accepted 并输出 ✅ Job accepted
5023
5039
  4. 只在上述 worktree 内改动;不要推远程除非明确要求
5040
+
5041
+ ${DISPATCH_MUST_FINISH_RULES}
5024
5042
  `;
5025
5043
  const wLog =
5026
5044
  workerCount === 1
@@ -5051,6 +5069,66 @@ ${waveList}
5051
5069
  }
5052
5070
  }
5053
5071
 
5072
+ // All tasks checked but delivery still open → nudge idle lane once to stamp accept.
5073
+ const allTasksDone =
5074
+ progress &&
5075
+ Number(progress.total) > 0 &&
5076
+ Number(progress.done) >= Number(progress.total);
5077
+ const nudgeLane = "w1";
5078
+ const nudgeTerm =
5079
+ (workerCount > 1
5080
+ ? terminalsByLane[nudgeLane]
5081
+ : terminalsByLane.w1 || terminalsByLane.default) || {};
5082
+ const acceptFp = "__ACCEPT_NUDGE__";
5083
+ const priorNudge = Array.isArray(releasedWaves[nudgeLane])
5084
+ ? releasedWaves[nudgeLane]
5085
+ : [];
5086
+ if (
5087
+ allTasksDone &&
5088
+ !accepted &&
5089
+ agentId &&
5090
+ worktreePath &&
5091
+ !(nudgeTerm.busy || Number(nudgeTerm.queueDepth || 0) > 0) &&
5092
+ !priorNudge.includes(acceptFp)
5093
+ ) {
5094
+ const brief = featureDir || dispatch.featureDir || "";
5095
+ const prompt = `Duaer
5096
+
5097
+ 编排器催办:tasks.md 已全部勾选,但 delivery.json 尚未 accepted。工作目录: ${worktreePath}
5098
+ Brief: ${brief}
5099
+
5100
+ 立刻完成验收收尾(不要再改无关功能):
5101
+ 1. 更新产品 README(若尚未反映本次交付)
5102
+ 2. 启动可打开的服务(若是 HTTP),确认能打开
5103
+ 3. stamp delivery.json 为 accepted,必须写入 preview.url
5104
+ 4. 最终只输出:✅ Job accepted — ready for your review.
5105
+ 5. 禁止输出 Job not accepted yet / ⏳ not accepted
5106
+
5107
+ ${DISPATCH_MUST_FINISH_RULES}
5108
+ `;
5109
+ const wLog =
5110
+ workerCount === 1
5111
+ ? path.join(brief, "agent-launch.log")
5112
+ : path.join(brief, `agent-launch-${nudgeLane}.log`);
5113
+ try {
5114
+ launchAgent({
5115
+ agentId,
5116
+ worktreePath,
5117
+ agentPrompt: prompt,
5118
+ logPath: wLog,
5119
+ featureDir: brief,
5120
+ continueSession: true,
5121
+ queueLane: workerCount > 1 ? nudgeLane : null,
5122
+ });
5123
+ releasedWaves[nudgeLane] = [...priorNudge, acceptFp];
5124
+ changed = true;
5125
+ } catch (err) {
5126
+ errors.push(
5127
+ `accept-nudge: ${err?.message || String(err || "launch failed")}`,
5128
+ );
5129
+ }
5130
+ }
5131
+
5054
5132
  const summary = orchestrationSummary({
5055
5133
  pool,
5056
5134
  doneSet,
@@ -5323,17 +5401,39 @@ function dispatchStatus(jobId) {
5323
5401
  revision: resultRevision,
5324
5402
  accepted,
5325
5403
  });
5326
- // Prefer latest snapshot URL when we recorded one for this revision.
5404
+ // Prefer openable product URL (service / HTML). Never promote docs (.md) snapshots.
5327
5405
  let previewOut = showPreview ? preview : null;
5328
5406
  if (previewOut && accepted && results.length) {
5329
5407
  const latest = results[results.length - 1];
5330
- if (latest?.url && Number(latest.revision) === resultRevision) {
5408
+ const latestOpenable =
5409
+ latest?.url &&
5410
+ Number(latest.revision) === resultRevision &&
5411
+ (/^https?:\/\//i.test(String(latest.url)) ||
5412
+ isOpenableProductPreview(latest.path || "") ||
5413
+ /\.html?(?:$|[?#])/i.test(String(latest.url)));
5414
+ if (latestOpenable) {
5331
5415
  previewOut = {
5332
5416
  ...previewOut,
5333
5417
  url: latest.url,
5334
5418
  label: latest.label || previewOut.label,
5335
5419
  kind: latest.kind || previewOut.kind,
5420
+ path: latest.path || previewOut.path || null,
5336
5421
  };
5422
+ } else {
5423
+ const openable = pickOpenableResultEntry(results);
5424
+ const previewAlreadyOpenable =
5425
+ /^https?:\/\//i.test(String(previewOut.url || "")) ||
5426
+ isOpenableProductPreview(previewOut.path || "") ||
5427
+ /\.html?(?:$|[?#])/i.test(String(previewOut.url || ""));
5428
+ if (!previewAlreadyOpenable && openable?.url) {
5429
+ previewOut = {
5430
+ ...previewOut,
5431
+ url: openable.url,
5432
+ label: openable.label || previewOut.label,
5433
+ kind: openable.kind || previewOut.kind,
5434
+ path: openable.path || null,
5435
+ };
5436
+ }
5337
5437
  }
5338
5438
  }
5339
5439
  // After worktree cleanup with no previewable snapshot, hide dead current link.
@@ -6478,10 +6578,13 @@ async function handleApi(req, res) {
6478
6578
  send(res, 400, { error: "path required", code: "EMPTY_PATH" });
6479
6579
  return;
6480
6580
  }
6581
+ const langRaw = String(url.searchParams.get("lang") || "").toLowerCase();
6481
6582
  const lang =
6482
- String(url.searchParams.get("lang") || "").toLowerCase() === "en"
6583
+ langRaw === "en" || langRaw.startsWith("en")
6483
6584
  ? "en"
6484
- : "zh";
6585
+ : langRaw === "ja" || langRaw.startsWith("ja")
6586
+ ? "ja"
6587
+ : "zh";
6485
6588
  const asJson = String(url.searchParams.get("format") || "") === "json";
6486
6589
  const session = readProjectChat(liveRoot(), projectPath);
6487
6590
  let job = null;
@@ -15,12 +15,25 @@ function esc(s) {
15
15
  .replace(/"/g, "&quot;");
16
16
  }
17
17
 
18
+ function normalizeDeliverablesLang(lang) {
19
+ const l = String(lang || "").toLowerCase();
20
+ if (l === "en" || l.startsWith("en")) return "en";
21
+ if (l === "ja" || l.startsWith("ja")) return "ja";
22
+ return "zh";
23
+ }
24
+
25
+ function pickLang(lang, map) {
26
+ const l = normalizeDeliverablesLang(lang);
27
+ return map[l] ?? map.zh;
28
+ }
29
+
18
30
  function fmtAt(iso, lang) {
19
- if (!iso) return lang === "en" ? "—" : "—";
31
+ if (!iso) return "—";
20
32
  try {
21
33
  const d = new Date(iso);
22
34
  if (Number.isNaN(d.getTime())) return String(iso).slice(0, 19);
23
- return d.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
35
+ const tag = pickLang(lang, { en: "en-US", ja: "ja-JP", zh: "zh-CN" });
36
+ return d.toLocaleString(tag, {
24
37
  year: "numeric",
25
38
  month: "2-digit",
26
39
  day: "2-digit",
@@ -279,7 +292,7 @@ function renderTaskPoolHtml(body, L) {
279
292
  * @param {{ job?: object|null, lang?: string, projectTitle?: string }} [opts]
280
293
  */
281
294
  export function buildDeliverablesModel(session, opts = {}) {
282
- const lang = opts.lang === "en" ? "en" : "zh";
295
+ const lang = normalizeDeliverablesLang(opts.lang);
283
296
  const s = session && typeof session === "object" ? session : {};
284
297
  const modules = Array.isArray(s.modules) ? s.modules : [];
285
298
  const confirmed = modules.filter((m) => m.status === "confirmed");
@@ -294,7 +307,7 @@ export function buildDeliverablesModel(session, opts = {}) {
294
307
  if (allReq || confirmed.length) {
295
308
  reqVersions.push({
296
309
  id: "v0",
297
- label: lang === "en" ? "Initial" : "初版",
310
+ label: pickLang(lang, { en: "Initial", ja: "初版", zh: "初版" }),
298
311
  at: s.updatedAt || null,
299
312
  modules: (allReq ? modules : confirmed).map((m) => ({
300
313
  id: m.id,
@@ -309,8 +322,11 @@ export function buildDeliverablesModel(session, opts = {}) {
309
322
  if (rev < 1) continue;
310
323
  reqVersions.push({
311
324
  id: `r${rev}`,
312
- label:
313
- lang === "en" ? `Revision ${rev}` : `第 ${rev} 次改进`,
325
+ label: pickLang(lang, {
326
+ en: `Revision ${rev}`,
327
+ ja: `改訂 ${rev}`,
328
+ zh: `第 ${rev} 次改进`,
329
+ }),
314
330
  at: entry.at || s.updatedAt || null,
315
331
  revise: {
316
332
  goal: entry.goal || "",
@@ -324,19 +340,27 @@ export function buildDeliverablesModel(session, opts = {}) {
324
340
  const stages = [
325
341
  {
326
342
  id: "requirements",
327
- title: lang === "en" ? "Requirements" : "需求",
343
+ title: pickLang(lang, { en: "Requirements", ja: "要件", zh: "需求" }),
328
344
  status: allReq ? "done" : confirmed.length ? "partial" : "empty",
329
345
  artifacts: [
330
346
  {
331
347
  id: "req-doc",
332
- title: lang === "en" ? "Requirements document" : "需求文档",
348
+ title: pickLang(lang, {
349
+ en: "Requirements document",
350
+ ja: "要件ドキュメント",
351
+ zh: "需求文档",
352
+ }),
333
353
  kind: "timeline",
334
354
  ready: reqVersions.length > 0,
335
355
  versions: reqVersions,
336
356
  },
337
357
  {
338
358
  id: "req-confirm",
339
- title: lang === "en" ? "Requirements confirmation" : "需求确认书",
359
+ title: pickLang(lang, {
360
+ en: "Requirements confirmation",
361
+ ja: "要件確認書",
362
+ zh: "需求确认书",
363
+ }),
340
364
  kind: "confirmations",
341
365
  ready: confirmed.length > 0,
342
366
  confirmations: confirmed.map((m) => ({
@@ -350,12 +374,16 @@ export function buildDeliverablesModel(session, opts = {}) {
350
374
  },
351
375
  {
352
376
  id: "architecture",
353
- title: lang === "en" ? "Architecture" : "架构",
377
+ title: pickLang(lang, { en: "Architecture", ja: "アーキテクチャ", zh: "架构" }),
354
378
  status: arch.confirmed ? "done" : arch.url ? "partial" : "empty",
355
379
  artifacts: [
356
380
  {
357
381
  id: "arch-diagram",
358
- title: lang === "en" ? "Architecture diagram" : "架构图",
382
+ title: pickLang(lang, {
383
+ en: "Architecture diagram",
384
+ ja: "アーキテクチャ図",
385
+ zh: "架构图",
386
+ }),
359
387
  kind: "architecture",
360
388
  ready: Boolean(arch.url),
361
389
  url: arch.url || null,
@@ -364,34 +392,48 @@ export function buildDeliverablesModel(session, opts = {}) {
364
392
  },
365
393
  {
366
394
  id: "arch-confirm",
367
- title: lang === "en" ? "Architecture confirmation" : "架构确认",
395
+ title: pickLang(lang, {
396
+ en: "Architecture confirmation",
397
+ ja: "アーキテクチャ確認",
398
+ zh: "架构确认",
399
+ }),
368
400
  kind: "note",
369
401
  ready: Boolean(arch.confirmed),
370
402
  body: arch.confirmed
371
- ? lang === "en"
372
- ? "Architecture confirmed on the desk."
373
- : "已在台面确认架构。"
403
+ ? pickLang(lang, {
404
+ en: "Architecture confirmed on the desk.",
405
+ ja: "デスクでアーキテクチャを確認済みです。",
406
+ zh: "已在台面确认架构。",
407
+ })
374
408
  : "",
375
409
  },
376
410
  ],
377
411
  },
378
412
  {
379
413
  id: "kickoff",
380
- title: lang === "en" ? "Kickoff & implementation" : "开工与实现",
414
+ title: pickLang(lang, {
415
+ en: "Kickoff & implementation",
416
+ ja: "キックオフと実装",
417
+ zh: "开工与实现",
418
+ }),
381
419
  status: s.jobId || s.taskPool ? "done" : "empty",
382
420
  artifacts: [
383
421
  {
384
422
  id: "brief",
385
- title: lang === "en" ? "Brief / job" : "Brief / 工单",
423
+ title: pickLang(lang, {
424
+ en: "Brief / job",
425
+ ja: "Brief / ジョブ",
426
+ zh: "Brief / 工单",
427
+ }),
386
428
  kind: "note",
387
429
  ready: Boolean(s.jobId),
388
430
  body: s.jobId
389
- ? `${lang === "en" ? "Job" : "工单"}: ${s.jobId}`
431
+ ? `${pickLang(lang, { en: "Job", ja: "ジョブ", zh: "工单" })}: ${s.jobId}`
390
432
  : "",
391
433
  },
392
434
  {
393
435
  id: "task-pool",
394
- title: lang === "en" ? "Task pool" : "任务池",
436
+ title: pickLang(lang, { en: "Task pool", ja: "タスクプール", zh: "任务池" }),
395
437
  kind: "taskpool",
396
438
  ready: Boolean(s.taskPool),
397
439
  body: s.taskPool
@@ -399,16 +441,18 @@ export function buildDeliverablesModel(session, opts = {}) {
399
441
  : "",
400
442
  meta:
401
443
  Number(s.workerCount) > 1
402
- ? lang === "en"
403
- ? `Workers: ${s.workerCount}`
404
- : `数字员工:${s.workerCount}`
444
+ ? pickLang(lang, {
445
+ en: `Workers: ${s.workerCount}`,
446
+ ja: `デジタル従業員:${s.workerCount}`,
447
+ zh: `数字员工:${s.workerCount}`,
448
+ })
405
449
  : "",
406
450
  },
407
451
  ],
408
452
  },
409
453
  {
410
454
  id: "delivery",
411
- title: lang === "en" ? "Delivery" : "交付",
455
+ title: pickLang(lang, { en: "Delivery", ja: "デリバリー", zh: "交付" }),
412
456
  status:
413
457
  delivery?.status === "accepted" || job?.jobStatus === "accepted"
414
458
  ? "done"
@@ -418,22 +462,26 @@ export function buildDeliverablesModel(session, opts = {}) {
418
462
  artifacts: [
419
463
  {
420
464
  id: "delivery-stamp",
421
- title: lang === "en" ? "Delivery stamp" : "交付戳",
465
+ title: pickLang(lang, {
466
+ en: "Delivery stamp",
467
+ ja: "デリバリースタンプ",
468
+ zh: "交付戳",
469
+ }),
422
470
  kind: "note",
423
471
  ready: Boolean(delivery || job?.jobStatus),
424
472
  body: delivery
425
- ? `${lang === "en" ? "Status" : "状态"}: ${delivery.status || "—"}${
473
+ ? `${pickLang(lang, { en: "Status", ja: "状態", zh: "状态" })}: ${delivery.status || "—"}${
426
474
  delivery.acceptedAt
427
- ? `\n${lang === "en" ? "Accepted" : "验收"}: ${delivery.acceptedAt}`
475
+ ? `\n${pickLang(lang, { en: "Accepted", ja: "受入", zh: "验收" })}: ${delivery.acceptedAt}`
428
476
  : ""
429
477
  }`
430
478
  : job?.jobStatus
431
- ? `${lang === "en" ? "Job status" : "工单状态"}: ${job.jobStatus}`
479
+ ? `${pickLang(lang, { en: "Job status", ja: "ジョブ状態", zh: "工单状态" })}: ${job.jobStatus}`
432
480
  : "",
433
481
  },
434
482
  {
435
483
  id: "preview",
436
- title: lang === "en" ? "Preview" : "预览",
484
+ title: pickLang(lang, { en: "Preview", ja: "プレビュー", zh: "预览" }),
437
485
  kind: "link",
438
486
  ready: Boolean(preview?.url),
439
487
  url: preview?.url || null,
@@ -445,14 +493,15 @@ export function buildDeliverablesModel(session, opts = {}) {
445
493
  if (reviseCards.length || s.reviseLocked || s.lastRevision) {
446
494
  stages.push({
447
495
  id: "revise",
448
- title: lang === "en" ? "Revisions" : "改进",
496
+ title: pickLang(lang, { en: "Revisions", ja: "改訂", zh: "改进" }),
449
497
  status: reviseCards.length ? "done" : "partial",
450
498
  artifacts: reviseCards.map((entry) => ({
451
499
  id: `revise-${entry.revision}`,
452
- title:
453
- lang === "en"
454
- ? `Revision ${entry.revision} plan`
455
- : `第 ${entry.revision} 版改进方案`,
500
+ title: pickLang(lang, {
501
+ en: `Revision ${entry.revision} plan`,
502
+ ja: `改訂 ${entry.revision} プラン`,
503
+ zh: `第 ${entry.revision} 版改进方案`,
504
+ }),
456
505
  kind: "card",
457
506
  ready: true,
458
507
  card: {
@@ -474,7 +523,7 @@ export function buildDeliverablesModel(session, opts = {}) {
474
523
  projectTitle:
475
524
  String(opts.projectTitle || "").trim() ||
476
525
  path.basename(String(s.projectPath || "")) ||
477
- (lang === "en" ? "Project" : "项目"),
526
+ pickLang(lang, { en: "Project", ja: "プロジェクト", zh: "项目" }),
478
527
  updatedAt: s.updatedAt || null,
479
528
  stages,
480
529
  };
@@ -512,6 +561,37 @@ function labelsFor(lang) {
512
561
  jump: "View in requirements",
513
562
  };
514
563
  }
564
+ if (lang === "ja") {
565
+ return {
566
+ brand: "プロジェクト成果物",
567
+ pageTitle: "成果物ドシエ",
568
+ subtitle: "各段階で何ができたかを整理した記録",
569
+ generated: "生成日時",
570
+ project: "プロジェクト",
571
+ emptyStage: "まだ成果物はありません",
572
+ emptyCard: "(空)",
573
+ goal: "目的",
574
+ out: "対象外",
575
+ accept: "受入基準",
576
+ assume: "前提",
577
+ statusDone: "完了",
578
+ statusPartial: "進行中",
579
+ statusEmpty: "未着手",
580
+ timeline: "バージョンタイムライン",
581
+ toc: "目次",
582
+ artifacts: "成果物",
583
+ openArch: "図を開く",
584
+ openLink: "開く",
585
+ confirmed: "確認済み",
586
+ draft: "下書き",
587
+ module: "モジュール",
588
+ depends: "依存",
589
+ taskId: "ID",
590
+ taskTitle: "タスク",
591
+ confirmRegistry: "確認レジストリ",
592
+ jump: "要件本文を見る",
593
+ };
594
+ }
515
595
  return {
516
596
  brand: "项目交付物",
517
597
  pageTitle: "交付物档案",
@@ -631,7 +711,7 @@ function renderArtifact(art, L, lang) {
631
711
  * @param {ReturnType<typeof buildDeliverablesModel>} model
632
712
  */
633
713
  export function renderDeliverablesHtml(model) {
634
- const lang = model.lang === "en" ? "en" : "zh";
714
+ const lang = normalizeDeliverablesLang(model.lang);
635
715
  const L = labelsFor(lang);
636
716
  const stages = model.stages || [];
637
717
  const tocHtml = `<nav class="toc" aria-label="${esc(L.toc)}">
@@ -673,7 +753,7 @@ export function renderDeliverablesHtml(model) {
673
753
  .join("\n");
674
754
 
675
755
  return `<!DOCTYPE html>
676
- <html lang="${lang === "en" ? "en" : "zh-CN"}">
756
+ <html lang="${pickLang(lang, { en: "en", ja: "ja", zh: "zh-CN" })}">
677
757
  <head>
678
758
  <meta charset="utf-8" />
679
759
  <meta name="viewport" content="width=device-width, initial-scale=1" />