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.
- package/.claude/rules/duaer-spec.md +5 -2
- package/.cursor/rules/duaer-spec.mdc +5 -2
- package/.cursor/skills/duaer-do/SKILL.md +20 -2
- package/.cursor/skills/duaer-tasks/SKILL.md +8 -0
- package/.duaer/active-job.json +3 -3
- package/.duaer/memory/constitution.md +13 -0
- package/.duaer/specs/192-release-020/delivery.json +19 -0
- package/.duaer/specs/192-release-020/tasks.md +3 -3
- package/.duaer/specs/193-dispatch-must-accept/delivery.json +11 -0
- package/.duaer/specs/193-dispatch-must-accept/spec.md +17 -0
- package/.duaer/specs/193-dispatch-must-accept/tasks.md +6 -0
- package/.duaer/specs/194-chat-preview-link/delivery.json +11 -0
- package/.duaer/specs/194-chat-preview-link/spec.md +14 -0
- package/.duaer/specs/194-chat-preview-link/tasks.md +5 -0
- package/.duaer/specs/195-chat-link-garble/delivery.json +10 -0
- package/.duaer/specs/195-chat-link-garble/spec.md +17 -0
- package/.duaer/specs/195-chat-link-garble/tasks.md +5 -0
- package/.duaer/specs/196-task-exec-graph/delivery.json +11 -0
- package/.duaer/specs/196-task-exec-graph/spec.md +15 -0
- package/.duaer/specs/196-task-exec-graph/tasks.md +6 -0
- package/.duaer/specs/197-task-graph-archify/delivery.json +12 -0
- package/.duaer/specs/197-task-graph-archify/spec.md +14 -0
- package/.duaer/specs/197-task-graph-archify/tasks.md +5 -0
- package/.duaer/specs/198-task-graph-labels/delivery.json +12 -0
- package/.duaer/specs/198-task-graph-labels/spec.md +12 -0
- package/.duaer/specs/198-task-graph-labels/tasks.md +5 -0
- package/.duaer/specs/199-preview-open-url/delivery.json +12 -0
- package/.duaer/specs/199-preview-open-url/spec.md +16 -0
- package/.duaer/specs/199-preview-open-url/tasks.md +5 -0
- package/.duaer/specs/200-preview-start-url/delivery.json +11 -0
- package/.duaer/specs/200-preview-start-url/spec.md +15 -0
- package/.duaer/specs/200-preview-start-url/tasks.md +5 -0
- package/.duaer/specs/201-arch-fullscreen/delivery.json +11 -0
- package/.duaer/specs/201-arch-fullscreen/spec.md +15 -0
- package/.duaer/specs/201-arch-fullscreen/tasks.md +5 -0
- package/.duaer/specs/202-release-021/delivery.json +18 -0
- package/.duaer/specs/202-release-021/spec.md +12 -0
- package/.duaer/specs/202-release-021/tasks.md +5 -0
- package/.duaer/specs/203-atomic-monitorable-tasks/delivery.json +13 -0
- package/.duaer/specs/203-atomic-monitorable-tasks/spec.md +44 -0
- package/.duaer/specs/203-atomic-monitorable-tasks/tasks.md +8 -0
- package/.duaer/specs/204-live-i18n-ja/delivery.json +14 -0
- package/.duaer/specs/204-live-i18n-ja/spec.md +34 -0
- package/.duaer/specs/204-live-i18n-ja/tasks.md +7 -0
- package/.duaer/specs/205-release-022/spec.md +12 -0
- package/.duaer/specs/205-release-022/tasks.md +6 -0
- package/.duaer/templates/tasks-template.md +4 -0
- package/CHANGELOG.md +64 -0
- package/bin/duaer-live.mjs +117 -14
- package/bin/live-deliverables.mjs +116 -36
- package/bin/live-modules.mjs +37 -7
- package/bin/live-preview.mjs +63 -12
- package/bin/live-progress.mjs +7 -38
- package/docs/agent/e2e-test-plan.md +20 -1
- package/docs/agent/live-desk.md +11 -1
- package/package.json +1 -1
- package/web/live-dev/acceptance-lines.mjs +42 -0
- package/web/live-dev/app.js +260 -46
- package/web/live-dev/chat-markdown.mjs +43 -3
- package/web/live-dev/i18n-ja.js +483 -0
- package/web/live-dev/i18n.js +34 -13
- package/web/live-dev/index.html +14 -13
- package/web/live-dev/styles.css +82 -4
- 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`.
|
|
@@ -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
|
package/bin/duaer-live.mjs
CHANGED
|
@@ -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 /
|
|
3620
|
-
4b.
|
|
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
|
|
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]
|
|
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]
|
|
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.
|
|
4233
|
-
3. 对照本轮 Revision acceptance 全部满足后,才 stamp delivery.json 为 accepted,并必须更新 preview.url
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6583
|
+
langRaw === "en" || langRaw.startsWith("en")
|
|
6483
6584
|
? "en"
|
|
6484
|
-
: "
|
|
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, """);
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
372
|
-
|
|
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
|
|
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
|
|
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
|
|
431
|
+
? `${pickLang(lang, { en: "Job", ja: "ジョブ", zh: "工单" })}: ${s.jobId}`
|
|
390
432
|
: "",
|
|
391
433
|
},
|
|
392
434
|
{
|
|
393
435
|
id: "task-pool",
|
|
394
|
-
title: lang
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
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
|
|
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
|
|
473
|
+
? `${pickLang(lang, { en: "Status", ja: "状態", zh: "状态" })}: ${delivery.status || "—"}${
|
|
426
474
|
delivery.acceptedAt
|
|
427
|
-
? `\n${lang
|
|
475
|
+
? `\n${pickLang(lang, { en: "Accepted", ja: "受入", zh: "验收" })}: ${delivery.acceptedAt}`
|
|
428
476
|
: ""
|
|
429
477
|
}`
|
|
430
478
|
: job?.jobStatus
|
|
431
|
-
? `${lang
|
|
479
|
+
? `${pickLang(lang, { en: "Job status", ja: "ジョブ状態", zh: "工单状态" })}: ${job.jobStatus}`
|
|
432
480
|
: "",
|
|
433
481
|
},
|
|
434
482
|
{
|
|
435
483
|
id: "preview",
|
|
436
|
-
title: lang
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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
|
|
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
|
|
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" />
|