coding-agent-harness 1.0.1 → 1.0.2
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/CHANGELOG.md +19 -0
- package/README.en-US.md +14 -0
- package/README.md +111 -86
- package/README.zh-CN.md +270 -0
- package/SKILL.md +116 -189
- package/docs-release/README.md +72 -5
- package/docs-release/architecture/overview.md +286 -28
- package/docs-release/architecture/overview.zh-CN.md +288 -0
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/docs-release/assets/harness-architecture.svg +163 -0
- package/docs-release/assets/harness-workflow.svg +64 -0
- package/docs-release/guides/agent-installation.en-US.md +214 -0
- package/docs-release/guides/agent-installation.md +123 -26
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
- package/docs-release/guides/document-audience-and-surfaces.md +112 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
- package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
- package/docs-release/guides/migration-playbook.en-US.md +325 -0
- package/docs-release/guides/migration-playbook.md +329 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
- package/docs-release/guides/parent-control-repository-pattern.md +252 -0
- package/docs-release/guides/repository-operating-models.en-US.md +196 -0
- package/docs-release/guides/repository-operating-models.md +196 -0
- package/docs-release/intl/README.md +15 -0
- package/docs-release/intl/de-DE.md +18 -0
- package/docs-release/intl/en-US.md +18 -0
- package/docs-release/intl/es-ES.md +18 -0
- package/docs-release/intl/fr-FR.md +18 -0
- package/docs-release/intl/ja-JP.md +18 -0
- package/docs-release/intl/ko-KR.md +18 -0
- package/docs-release/intl/zh-CN.md +18 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
- package/package.json +3 -1
- package/references/agents-md-pattern.md +3 -3
- package/references/docs-directory-standard.md +47 -3
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +5 -3
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +23 -6
- package/references/planning-loop.md +41 -3
- package/references/project-onboarding-audit.md +10 -0
- package/references/repo-governance-standard.md +2 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +6 -5
- package/scripts/check-harness.mjs +76 -35
- package/scripts/harness.mjs +303 -12
- package/scripts/lib/capability-registry.mjs +533 -0
- package/scripts/lib/check-profiles.mjs +510 -0
- package/scripts/lib/core-shared.mjs +186 -0
- package/scripts/lib/dashboard-data.mjs +389 -0
- package/scripts/lib/dashboard-workbench.mjs +217 -0
- package/scripts/lib/dashboard-writer.mjs +93 -2
- package/scripts/lib/harness-core.mjs +10 -1318
- package/scripts/lib/lesson-maintenance.mjs +145 -0
- package/scripts/lib/markdown-utils.mjs +158 -0
- package/scripts/lib/migration-planner.mjs +478 -0
- package/scripts/lib/migration-support.mjs +312 -0
- package/scripts/lib/task-lifecycle.mjs +755 -0
- package/scripts/lib/task-scanner.mjs +682 -0
- package/scripts/smoke-dashboard.mjs +22 -0
- package/scripts/test-harness.mjs +926 -14
- package/templates/AGENTS.md.template +41 -30
- package/templates/architecture/Architecture-SSoT.md +21 -0
- package/templates/architecture/README.md +49 -0
- package/templates/architecture/critical-flows.md +22 -0
- package/templates/architecture/local-repo-context.md +20 -0
- package/templates/architecture/service-catalog.md +17 -0
- package/templates/architecture/services/service-template.md +31 -0
- package/templates/architecture/system-map.md +22 -0
- package/templates/dashboard/assets/app-src/00-state.js +41 -0
- package/templates/dashboard/assets/app-src/10-router.js +76 -0
- package/templates/dashboard/assets/app-src/20-overview.js +235 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +128 -0
- package/templates/dashboard/assets/app-src/50-migration.js +169 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
- package/templates/dashboard/assets/app.css +2575 -310
- package/templates/dashboard/assets/app.js +1498 -307
- package/templates/dashboard/assets/app.manifest.json +11 -0
- package/templates/dashboard/assets/i18n.js +429 -44
- package/templates/dashboard/assets/mermaid-renderer.js +58 -8
- package/templates/development/README.md +52 -0
- package/templates/development/codebase-map.md +11 -0
- package/templates/development/cross-repo-debugging.md +18 -0
- package/templates/development/external-context/service-template.md +33 -0
- package/templates/development/external-source-packs/README.md +24 -0
- package/templates/development/external-source-packs/digest-template.md +28 -0
- package/templates/development/local-setup.md +16 -0
- package/templates/development/stubs-and-mocks.md +11 -0
- package/templates/integrations/README.md +40 -0
- package/templates/integrations/api-contract.md +42 -0
- package/templates/integrations/event-contract.md +46 -0
- package/templates/integrations/third-party/vendor-template.md +42 -0
- package/templates/integrations/webhook-contract.md +41 -0
- package/templates/planning/brief.md +32 -0
- package/templates/planning/lesson_candidates.md +58 -0
- package/templates/planning/long-running-task-contract.md +7 -0
- package/templates/planning/module_brief.md +25 -0
- package/templates/planning/module_session_prompt.md +6 -0
- package/templates/planning/task_plan.md +7 -5
- package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +4 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/repo-governance-standard.md +6 -4
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +69 -70
- package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
- package/templates-zh-CN/architecture/README.md +51 -0
- package/templates-zh-CN/architecture/critical-flows.md +24 -0
- package/templates-zh-CN/architecture/local-repo-context.md +20 -0
- package/templates-zh-CN/architecture/service-catalog.md +17 -0
- package/templates-zh-CN/architecture/services/service-template.md +31 -0
- package/templates-zh-CN/architecture/system-map.md +22 -0
- package/templates-zh-CN/development/README.md +54 -0
- package/templates-zh-CN/development/codebase-map.md +11 -0
- package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
- package/templates-zh-CN/development/external-context/service-template.md +33 -0
- package/templates-zh-CN/development/external-source-packs/README.md +24 -0
- package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
- package/templates-zh-CN/development/local-setup.md +16 -0
- package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
- package/templates-zh-CN/integrations/README.md +42 -0
- package/templates-zh-CN/integrations/api-contract.md +42 -0
- package/templates-zh-CN/integrations/event-contract.md +46 -0
- package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
- package/templates-zh-CN/integrations/webhook-contract.md +41 -0
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/lesson_candidates.md +58 -0
- package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
- package/templates-zh-CN/planning/module_brief.md +25 -0
- package/templates-zh-CN/planning/module_plan.md +2 -2
- package/templates-zh-CN/planning/module_session_prompt.md +4 -3
- package/templates-zh-CN/planning/task_plan.md +10 -4
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/docs-library-standard.md +35 -0
- package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/dashboard/assets/app.css +0 -399
- package/templates-zh-CN/dashboard/assets/app.js +0 -435
- package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
- package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
- package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
- package/templates-zh-CN/dashboard/index.html +0 -18
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
function clampCompletion(value) {
|
|
2
|
+
const number = Number(value) || 0;
|
|
3
|
+
return Math.max(0, Math.min(100, Math.round(number)));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function stateToColorVar(state) {
|
|
7
|
+
const map = { in_progress: "--accent", review: "--accent-2", blocked: "--danger", done: "--ok", planned: "--muted", not_started: "--muted" };
|
|
8
|
+
return map[state] || "--muted";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function taskToolbarCard(filteredCount) {
|
|
12
|
+
return `<section class="sidebar-card">
|
|
13
|
+
<h3>${t("filterTitle")}</h3>
|
|
14
|
+
<div class="input-group">
|
|
15
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
16
|
+
</div>
|
|
17
|
+
<div class="select-group">
|
|
18
|
+
<label>${t("stateFilter")}</label>
|
|
19
|
+
<select data-state-filter aria-label="${t("stateFilter")}">
|
|
20
|
+
${["all", "in_progress", "review", "blocked", "planned", "done", "unknown"].map((value) => `<option value="${value}" ${state.taskState === value ? "selected" : ""}>${label(value)}</option>`).join("")}
|
|
21
|
+
</select>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="select-group">
|
|
24
|
+
<label>${t("groupBy")}</label>
|
|
25
|
+
<select data-group-mode aria-label="${t("groupBy")}">
|
|
26
|
+
${["migration", "module", "month", "state"].map((value) => `<option value="${value}" ${state.taskGroupMode === value ? "selected" : ""}>${t(`group_${value}`)}</option>`).join("")}
|
|
27
|
+
</select>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="select-group">
|
|
30
|
+
<label>${t("layout")}</label>
|
|
31
|
+
<div class="layout-toggle-group">
|
|
32
|
+
<button class="layout-btn ${state.taskLayout === "list" ? "active" : ""}" data-layout="list" aria-label="${t("layoutList")}">
|
|
33
|
+
<svg style="width:12px;height:12px;vertical-align:middle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
34
|
+
${t("layoutList")}
|
|
35
|
+
</button>
|
|
36
|
+
<button class="layout-btn ${state.taskLayout === "grid" ? "active" : ""}" data-layout="grid" aria-label="${t("layoutGrid")}">
|
|
37
|
+
<svg style="width:12px;height:12px;vertical-align:middle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
|
38
|
+
${t("layoutGrid")}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="search-stats">
|
|
43
|
+
${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
|
|
44
|
+
</div>
|
|
45
|
+
</section>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function taskStatsCard() {
|
|
49
|
+
const allTasks = bundle.status?.tasks || [];
|
|
50
|
+
const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
|
|
51
|
+
return `<section class="sidebar-card">
|
|
52
|
+
<h3>${t("releaseHealth")}</h3>
|
|
53
|
+
<div class="stats-hero-gauge">
|
|
54
|
+
<span class="gauge-percentage">${avgCompletion}%</span>
|
|
55
|
+
<span class="gauge-label">${t("statOverall")}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="stats-breakdown">
|
|
58
|
+
${[
|
|
59
|
+
{ state: "in_progress", label: t("statInProgress"), colorVar: "--accent" },
|
|
60
|
+
{ state: "review", label: t("statReview"), colorVar: "--accent-2" },
|
|
61
|
+
{ state: "blocked", label: t("statBlocked"), colorVar: "--danger" },
|
|
62
|
+
{ state: "done", label: t("statDone"), colorVar: "--ok" }
|
|
63
|
+
].map(({ state, label, colorVar }) => {
|
|
64
|
+
const count = allTasks.filter(t => t.state === state).length;
|
|
65
|
+
return `<div class="stats-breakdown-row">
|
|
66
|
+
<span class="stat-label">
|
|
67
|
+
<span class="state-dot" style="background:var(${colorVar})"></span>
|
|
68
|
+
${label}
|
|
69
|
+
</span>
|
|
70
|
+
<span class="stat-value">${count}</span>
|
|
71
|
+
</div>`;
|
|
72
|
+
}).join("")}
|
|
73
|
+
</div>
|
|
74
|
+
</section>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function taskLegendCard() {
|
|
78
|
+
return `<section class="sidebar-card">
|
|
79
|
+
<h3>${t("legendTitle")}</h3>
|
|
80
|
+
<div class="legend-list">
|
|
81
|
+
<div class="legend-item">
|
|
82
|
+
<span class="badge brief ready" style="margin-top:2px">
|
|
83
|
+
<svg style="width:10px;height:10px;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
84
|
+
${t("badgeBrief")}
|
|
85
|
+
</span>
|
|
86
|
+
<span>${t("legendBriefDesc")}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="legend-item">
|
|
89
|
+
<span class="badge map ready" style="margin-top:2px">
|
|
90
|
+
<svg style="width:10px;height:10px;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
91
|
+
${t("badgeMap")}
|
|
92
|
+
</span>
|
|
93
|
+
<span>${t("legendMapDesc")}</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</section>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function taskStatsBar() {
|
|
100
|
+
const allTasks = bundle.status?.tasks || [];
|
|
101
|
+
const inProgress = allTasks.filter(t => t.state === "in_progress").length;
|
|
102
|
+
const blocked = allTasks.filter(t => t.state === "blocked").length;
|
|
103
|
+
const done = allTasks.filter(t => t.state === "done").length;
|
|
104
|
+
const review = allTasks.filter(t => t.state === "review").length;
|
|
105
|
+
const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
|
|
106
|
+
|
|
107
|
+
return `<section class="task-stats-bar">
|
|
108
|
+
<div class="stat-chip">
|
|
109
|
+
<span class="stat-value">${allTasks.length}</span>
|
|
110
|
+
<span class="stat-label">${t("statTotal")}</span>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="stat-chip in-progress">
|
|
113
|
+
<span class="stat-value">${inProgress}</span>
|
|
114
|
+
<span class="stat-label">${t("statInProgress")}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="stat-chip review">
|
|
117
|
+
<span class="stat-value">${review}</span>
|
|
118
|
+
<span class="stat-label">${t("statReview")}</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="stat-chip blocked">
|
|
121
|
+
<span class="stat-value">${blocked}</span>
|
|
122
|
+
<span class="stat-label">${t("statBlocked")}</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="stat-chip done">
|
|
125
|
+
<span class="stat-value">${done}</span>
|
|
126
|
+
<span class="stat-label">${t("statDone")}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="stat-chip completion">
|
|
129
|
+
<div class="stat-bar-track"><div class="stat-bar-fill" style="width:${avgCompletion}%"></div></div>
|
|
130
|
+
<div style="text-align:right">
|
|
131
|
+
<span class="stat-value">${avgCompletion}%</span>
|
|
132
|
+
<span class="stat-label" style="display:block;margin-top:2px">${t("statOverall")}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</section>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function taskRow(task) {
|
|
139
|
+
const completion = clampCompletion(task.completion);
|
|
140
|
+
const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
|
|
141
|
+
const mapReady = !!taskDocument(task, "visual_map.md");
|
|
142
|
+
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
143
|
+
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
144
|
+
|
|
145
|
+
return `<a class="task-row-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
146
|
+
<div class="row-accent-bar"></div>
|
|
147
|
+
<div class="row-main">
|
|
148
|
+
<strong>${escapeHtml(task.title)}</strong>
|
|
149
|
+
<span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="row-status">${tag(task.state)}</div>
|
|
152
|
+
<div class="row-progress">
|
|
153
|
+
<div class="mini-progress-track"><div class="mini-progress-fill" style="width:${completion}%"></div></div>
|
|
154
|
+
<span class="row-pct">${completion}%</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="row-brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}" aria-label="${escapeAttr(briefLabel)}">
|
|
157
|
+
<span class="badge brief ${briefReady ? "ready" : "missing"}">
|
|
158
|
+
<svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
159
|
+
${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="row-map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}" aria-label="${escapeAttr(mapLabel)}">
|
|
163
|
+
<span class="badge map ${mapReady ? "ready" : "missing"}">
|
|
164
|
+
<svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
165
|
+
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
</a>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function taskIndex() {
|
|
172
|
+
const tasks = filteredTasks();
|
|
173
|
+
const groups = taskGroups(tasks);
|
|
174
|
+
const orderedGroups = orderedTaskGroups(groups);
|
|
175
|
+
const groupPageCount = Math.max(1, Math.ceil(orderedGroups.length / taskGroupsPerPage));
|
|
176
|
+
const groupPage = Math.min(Math.max(1, Number(state.taskGroupPage) || 1), groupPageCount);
|
|
177
|
+
const visibleGroups = orderedGroups.slice((groupPage - 1) * taskGroupsPerPage, groupPage * taskGroupsPerPage);
|
|
178
|
+
|
|
179
|
+
return `<div class="tasks-grid">
|
|
180
|
+
<div class="tasks-main stack">
|
|
181
|
+
${taskStatsBar()}
|
|
182
|
+
${visibleGroups.map(([group, groupTasks]) => taskGroup(group, groupTasks)).join("")}
|
|
183
|
+
<section class="group-pager">
|
|
184
|
+
<span>${t("showingGroups")} ${visibleGroups.length ? (groupPage - 1) * taskGroupsPerPage + 1 : 0}-${Math.min(groupPage * taskGroupsPerPage, orderedGroups.length)} / ${orderedGroups.length}</span>
|
|
185
|
+
${pager("task-groups", groupPage, groupPageCount)}
|
|
186
|
+
</section>
|
|
187
|
+
</div>
|
|
188
|
+
<aside class="tasks-sidebar stack">
|
|
189
|
+
${taskToolbarCard(tasks.length)}
|
|
190
|
+
${taskStatsCard()}
|
|
191
|
+
${taskLegendCard()}
|
|
192
|
+
</aside>
|
|
193
|
+
</div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function orderedTaskGroups(groups) {
|
|
197
|
+
const rank = (group) => {
|
|
198
|
+
if (group.startsWith("module:")) return 2;
|
|
199
|
+
if (group.startsWith("state:")) return 2;
|
|
200
|
+
if (group.startsWith("month:")) return 2;
|
|
201
|
+
if (group === "active") return 0;
|
|
202
|
+
if (group === "brief-ready") return 1;
|
|
203
|
+
if (group.startsWith("legacy:")) return 2;
|
|
204
|
+
if (group === "unknown") return 3;
|
|
205
|
+
return 4;
|
|
206
|
+
};
|
|
207
|
+
return Object.entries(groups).sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function taskGroups(tasks) {
|
|
211
|
+
if (state.taskGroupMode === "module") {
|
|
212
|
+
return groupBy(tasks, (task) => `module:${taskModuleKey(task)}`);
|
|
213
|
+
}
|
|
214
|
+
if (state.taskGroupMode === "month") {
|
|
215
|
+
return groupBy(tasks, (task) => {
|
|
216
|
+
const match = task.shortId?.match(/^(\d{4}-\d{2})/);
|
|
217
|
+
return match ? `month:${match[1]}` : "month:unknown";
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (state.taskGroupMode === "state") {
|
|
221
|
+
return groupBy(tasks, (task) => `state:${task.state || "unknown"}`);
|
|
222
|
+
}
|
|
223
|
+
return groupBy(tasks, (task) => {
|
|
224
|
+
if (["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state)) return "active";
|
|
225
|
+
if (task.briefSource === "standalone") return "brief-ready";
|
|
226
|
+
const match = task.shortId?.match(/^(\d{4}-\d{2})/);
|
|
227
|
+
return match ? `legacy:${match[1]}` : task.state || "unknown";
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function taskGroup(group, tasks) {
|
|
232
|
+
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
233
|
+
const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
|
|
234
|
+
const start = (page - 1) * taskPageSize;
|
|
235
|
+
const visibleTasks = tasks.slice(start, start + taskPageSize);
|
|
236
|
+
const avgCompletion = tasks.length ? clampCompletion(tasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / tasks.length) : 0;
|
|
237
|
+
|
|
238
|
+
const isGrid = state.taskLayout === "grid";
|
|
239
|
+
const layoutClass = isGrid ? "task-card-grid" : "task-list";
|
|
240
|
+
const itemRenderer = isGrid ? taskCard : taskRow;
|
|
241
|
+
const listHeader = isGrid ? "" : `<div class="task-list-header">
|
|
242
|
+
<div class="col-main">${t("columnTask")}</div>
|
|
243
|
+
<div class="col-status">${t("columnState")}</div>
|
|
244
|
+
<div class="col-progress">${t("columnCompletion")}</div>
|
|
245
|
+
<div class="col-brief">${t("columnBrief")}</div>
|
|
246
|
+
<div class="col-map">${t("badgeMap")}</div>
|
|
247
|
+
</div>`;
|
|
248
|
+
|
|
249
|
+
return `<section class="task-group">
|
|
250
|
+
<div class="section-head">
|
|
251
|
+
<div>
|
|
252
|
+
<h2>${taskGroupLabel(group)}</h2>
|
|
253
|
+
<p class="subtle">${t("showing")} ${Math.min(start + 1, tasks.length)}-${Math.min(start + visibleTasks.length, tasks.length)} / ${tasks.length}</p>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="group-actions">
|
|
256
|
+
<div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
|
|
257
|
+
<div class="group-progress-track"><div class="group-progress-fill" style="width:${avgCompletion}%"></div></div>
|
|
258
|
+
<span>${avgCompletion}%</span>
|
|
259
|
+
</div>
|
|
260
|
+
${pager("task", page, pageCount, group)}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="${layoutClass}">
|
|
264
|
+
${listHeader}
|
|
265
|
+
${visibleTasks.map(itemRenderer).join("")}
|
|
266
|
+
</div>
|
|
267
|
+
</section>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function taskCard(task) {
|
|
271
|
+
const completion = clampCompletion(task.completion);
|
|
272
|
+
const stateColor = stateToColorVar(task.state);
|
|
273
|
+
const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
|
|
274
|
+
const mapReady = !!taskDocument(task, "visual_map.md");
|
|
275
|
+
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
276
|
+
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
277
|
+
|
|
278
|
+
return `<a class="task-card" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
|
|
279
|
+
<div class="card-header">
|
|
280
|
+
<span class="card-id">${escapeHtml(task.id)}</span>
|
|
281
|
+
${tag(task.state)}
|
|
282
|
+
</div>
|
|
283
|
+
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
284
|
+
<div class="card-meta">
|
|
285
|
+
<span class="meta-module" title="${escapeAttr(taskModuleKey(task))}">
|
|
286
|
+
<svg style="width:12px;height:12px;vertical-align:middle;margin-right:2px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
287
|
+
${escapeHtml(taskModuleKey(task))}
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="card-progress">
|
|
291
|
+
<div class="card-progress-track"><div class="card-progress-fill" style="width:${completion}%"></div></div>
|
|
292
|
+
<span class="progress-pct">${completion}%</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="card-badges">
|
|
295
|
+
<span class="badge brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}">
|
|
296
|
+
<svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
297
|
+
${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
|
|
298
|
+
</span>
|
|
299
|
+
<span class="badge map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}">
|
|
300
|
+
<svg style="width:10px;height:10px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
|
301
|
+
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
</a>`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function taskGroupLabel(group) {
|
|
308
|
+
if (group === "active") return t("activeCurrent");
|
|
309
|
+
if (group === "brief-ready") return t("briefReadyGroup");
|
|
310
|
+
if (group.startsWith("legacy:")) return `${t("legacyMonth")} ${group.slice("legacy:".length)}`;
|
|
311
|
+
if (group.startsWith("module:")) return `${t("inferredModule")} · ${group.slice("module:".length)}`;
|
|
312
|
+
if (group.startsWith("month:")) return `${t("legacyMonth")} ${group.slice("month:".length)}`;
|
|
313
|
+
if (group.startsWith("state:")) return `${t("columnState")} · ${label(group.slice("state:".length))}`;
|
|
314
|
+
return label(group);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function filteredTasks() {
|
|
318
|
+
const query = state.query.trim().toLowerCase();
|
|
319
|
+
return (bundle.status?.tasks || []).filter((task) => {
|
|
320
|
+
const stateMatch = state.taskState === "all" || task.state === state.taskState;
|
|
321
|
+
if (!stateMatch) return false;
|
|
322
|
+
if (!query) return true;
|
|
323
|
+
return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function taskModuleKey(task) {
|
|
328
|
+
return task.module || task.inferredModule || "legacy-unclassified";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function taskDetail(route) {
|
|
332
|
+
const taskId = route.id;
|
|
333
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
|
|
334
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
335
|
+
return `<main class="task-detail">
|
|
336
|
+
<nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
337
|
+
<section class="detail-hero">
|
|
338
|
+
<div>
|
|
339
|
+
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
340
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
341
|
+
<p>${escapeHtml(task.path)}</p>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="detail-score">${task.completion}%</div>
|
|
344
|
+
</section>
|
|
345
|
+
${taskStateSummary(task)}
|
|
346
|
+
${phaseTimeline(task)}
|
|
347
|
+
<section class="detail-grid">
|
|
348
|
+
<article class="detail-main">
|
|
349
|
+
${taskDocumentLibrary(task, route.doc)}
|
|
350
|
+
</article>
|
|
351
|
+
<aside class="detail-side">
|
|
352
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
353
|
+
${migrationSnapshotPanel(task)}
|
|
354
|
+
${openFindings(task)}
|
|
355
|
+
${evidenceList(task)}
|
|
356
|
+
${documentTabs(task)}
|
|
357
|
+
</aside>
|
|
358
|
+
</section>
|
|
359
|
+
</main>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function taskStateSummary(task) {
|
|
363
|
+
return `<section class="task-state-summary">
|
|
364
|
+
<div>
|
|
365
|
+
<span>${t("legacyState")}</span>
|
|
366
|
+
${tag(task.state)}
|
|
367
|
+
</div>
|
|
368
|
+
<div>
|
|
369
|
+
<span>${t("lifecycleState")}</span>
|
|
370
|
+
${tag(task.lifecycleState || "unknown")}
|
|
371
|
+
</div>
|
|
372
|
+
<div>
|
|
373
|
+
<span>${t("reviewStatus")}</span>
|
|
374
|
+
${tag(task.reviewStatus || "missing")}
|
|
375
|
+
</div>
|
|
376
|
+
<div>
|
|
377
|
+
<span>${t("closeoutStatus")}</span>
|
|
378
|
+
${tag(task.closeoutStatus || "missing")}
|
|
379
|
+
</div>
|
|
380
|
+
</section>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function phaseTimeline(task) {
|
|
384
|
+
return `<section class="phase-timeline">
|
|
385
|
+
<h2>${t("phaseTimeline")}</h2>
|
|
386
|
+
${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
|
|
387
|
+
<strong>${escapeHtml(phase.id)}</strong>
|
|
388
|
+
<span>${phase.completion}%</span>
|
|
389
|
+
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
390
|
+
${progressBar(phase.completion)}
|
|
391
|
+
</div>`).join("") || emptyState(t("noPhaseData"))}
|
|
392
|
+
</section>`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function taskDocSection(task, fileName, title, required) {
|
|
396
|
+
const doc = taskDocument(task, fileName);
|
|
397
|
+
if (!doc && !required) return "";
|
|
398
|
+
return `<section class="doc-section">
|
|
399
|
+
<div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
|
|
400
|
+
<div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
|
|
401
|
+
</section>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function taskDocumentLibrary(task, selectedTab) {
|
|
405
|
+
const docs = orderedTaskDocuments(task);
|
|
406
|
+
if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
|
|
407
|
+
const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
|
|
408
|
+
return `<section class="doc-library">
|
|
409
|
+
<div class="section-head">
|
|
410
|
+
<div>
|
|
411
|
+
<p class="eyebrow">${t("taskDocuments")}</p>
|
|
412
|
+
<h2>${escapeHtml(t("sourceDocuments"))}</h2>
|
|
413
|
+
</div>
|
|
414
|
+
<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="doc-accordion-list">
|
|
417
|
+
${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
|
|
418
|
+
</div>
|
|
419
|
+
</section>`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function orderedTaskDocuments(task) {
|
|
423
|
+
const docs = taskDocTabs
|
|
424
|
+
.map(([key, file]) => {
|
|
425
|
+
const doc = taskDocument(task, file);
|
|
426
|
+
if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
|
|
427
|
+
if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
|
|
428
|
+
return null;
|
|
429
|
+
})
|
|
430
|
+
.filter(Boolean);
|
|
431
|
+
const priority = taskDocumentPriority(task);
|
|
432
|
+
const rank = new Map(priority.map((key, index) => [key, index]));
|
|
433
|
+
return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function taskDocumentPriority(task) {
|
|
437
|
+
const stateName = task?.state || "";
|
|
438
|
+
const lifecycle = task?.lifecycleState || "";
|
|
439
|
+
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
440
|
+
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
441
|
+
}
|
|
442
|
+
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
443
|
+
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
444
|
+
}
|
|
445
|
+
if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
|
|
446
|
+
return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
|
|
447
|
+
}
|
|
448
|
+
return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function defaultTaskDocumentKey(task, docs) {
|
|
452
|
+
const priority = taskDocumentPriority(task);
|
|
453
|
+
return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function documentAccordion(item, open) {
|
|
457
|
+
return `<details class="doc-accordion" ${open ? "open" : ""}>
|
|
458
|
+
<summary>
|
|
459
|
+
<span>${escapeHtml(item.title)}</span>
|
|
460
|
+
<small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
|
|
461
|
+
</summary>
|
|
462
|
+
<div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
|
|
463
|
+
</details>`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function documentTabs(task) {
|
|
467
|
+
const docs = orderedTaskDocuments(task);
|
|
468
|
+
return `<section class="side-panel">
|
|
469
|
+
<h3>${t("sourceDocuments")}</h3>
|
|
470
|
+
${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
|
|
471
|
+
</section>`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function selectedSourceDocument(task, tab) {
|
|
475
|
+
if (!tab) return "";
|
|
476
|
+
const match = taskDocTabs.find(([key]) => key === tab);
|
|
477
|
+
if (!match) return "";
|
|
478
|
+
const doc = taskDocument(task, match[1]);
|
|
479
|
+
if (!doc) return "";
|
|
480
|
+
return `<section class="doc-section selected-source">
|
|
481
|
+
<div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
|
|
482
|
+
<div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
|
|
483
|
+
</section>`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function openFindings(task) {
|
|
487
|
+
const risks = task.risks || [];
|
|
488
|
+
return `<section class="side-panel">
|
|
489
|
+
<h3>${t("openFindings")}</h3>
|
|
490
|
+
${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
|
|
491
|
+
</section>`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function migrationSnapshotPanel(task) {
|
|
495
|
+
const snapshot = task.migrationSnapshot;
|
|
496
|
+
if (!snapshot) return "";
|
|
497
|
+
return `<section class="side-panel">
|
|
498
|
+
<h3>${t("migrationSnapshot")}</h3>
|
|
499
|
+
<p>${escapeHtml(t("taskPreset"))}: ${tag(task.taskPreset || "none")}</p>
|
|
500
|
+
<p>${escapeHtml(t("targetLevel"))}: ${tag(snapshot.targetLevel || "unknown")}</p>
|
|
501
|
+
<p>${escapeHtml(t("achievedLevel"))}: ${tag(snapshot.achievedLevel || "unknown")}</p>
|
|
502
|
+
<p>${escapeHtml(t("strictDeferred"))}: ${tag(snapshot.strictDeferred ? "yes" : "no")}</p>
|
|
503
|
+
<p>${escapeHtml(t("evidenceBundle"))}: <code>${escapeHtml(snapshot.evidenceBundle || "missing")}</code></p>
|
|
504
|
+
</section>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
508
|
+
if (!isTaskInReviewStage(task)) return "";
|
|
509
|
+
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
510
|
+
const confirmed = task.reviewStatus === "confirmed";
|
|
511
|
+
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
512
|
+
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
513
|
+
if (mode !== "workspace") {
|
|
514
|
+
return `<section class="side-panel review-actions">
|
|
515
|
+
<h3>${t("reviewActions")}</h3>
|
|
516
|
+
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
517
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
518
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
519
|
+
</section>`;
|
|
520
|
+
}
|
|
521
|
+
if (!canUseWorkbenchAction("review-complete")) {
|
|
522
|
+
return `<section class="side-panel review-actions">
|
|
523
|
+
<h3>${t("reviewActions")}</h3>
|
|
524
|
+
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
525
|
+
</section>`;
|
|
526
|
+
}
|
|
527
|
+
if (confirmed) {
|
|
528
|
+
return `<section class="side-panel review-actions">
|
|
529
|
+
<h3>${t("reviewActions")}</h3>
|
|
530
|
+
<p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
|
|
531
|
+
</section>`;
|
|
532
|
+
}
|
|
533
|
+
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
534
|
+
const disabled = blocking || missingWalkthrough || candidateBlocked;
|
|
535
|
+
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : t("reviewWorkbenchReady");
|
|
536
|
+
return `<section class="side-panel review-actions">
|
|
537
|
+
<h3>${t("reviewActions")}</h3>
|
|
538
|
+
<p>${escapeHtml(message)}</p>
|
|
539
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
540
|
+
<label class="review-check">
|
|
541
|
+
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
542
|
+
<span>${t("reviewConfirmChecklist")}</span>
|
|
543
|
+
</label>
|
|
544
|
+
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
545
|
+
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
546
|
+
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
547
|
+
</section>`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function isTaskInReviewStage(task) {
|
|
551
|
+
const state = task?.state || "";
|
|
552
|
+
const lifecycle = task?.lifecycleState || "";
|
|
553
|
+
if (["not_started", "planned", "in_progress"].includes(state)) return false;
|
|
554
|
+
return state === "review" || ["in_review", "review-blocked"].includes(lifecycle);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function evidenceList(task) {
|
|
558
|
+
const evidence = task.evidence || [];
|
|
559
|
+
return `<section class="side-panel">
|
|
560
|
+
<h3>${t("evidence")}</h3>
|
|
561
|
+
${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
|
|
562
|
+
</section>`;
|
|
563
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function modulesView(moduleId = "") {
|
|
2
|
+
const graph = bundle.graph || { nodes: [], edges: [] };
|
|
3
|
+
const explicitModules = (graph.nodes || []).filter((node) => node.type === "module");
|
|
4
|
+
const moduleMap = new Map(explicitModules.map((module) => [module.id.replace(/^module:/, ""), module]));
|
|
5
|
+
for (const task of bundle.status?.tasks || []) {
|
|
6
|
+
const key = taskModuleKey(task);
|
|
7
|
+
if (!moduleMap.has(key)) moduleMap.set(key, { id: `module:${key}`, type: "module", label: key, state: task.classificationSource || "inferred" });
|
|
8
|
+
}
|
|
9
|
+
const modules = [...moduleMap.values()];
|
|
10
|
+
return `<main class="stack">
|
|
11
|
+
<section class="module-grid">
|
|
12
|
+
${modules.map((module) => moduleCard(module)).join("") || emptyState(t("noModules"))}
|
|
13
|
+
</section>
|
|
14
|
+
</main>`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function moduleTaskRow(task) {
|
|
18
|
+
const dotClass = /fail|blocked|open/i.test(task.state) ? "state-fail" : /warn|advice|planned|missing|unknown/i.test(task.state) ? "state-warn" : "state-pass";
|
|
19
|
+
return `<a class="module-task-row" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}">
|
|
20
|
+
<div class="module-task-left">
|
|
21
|
+
<i class="module-task-dot ${dotClass}" title="${escapeAttr(task.state)}"></i>
|
|
22
|
+
<span class="module-task-title">${escapeHtml(task.title)}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<span class="module-task-pct">${task.completion}%</span>
|
|
25
|
+
</a>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function moduleCard(module) {
|
|
29
|
+
const moduleKey = module.id.replace(/^module:/, "");
|
|
30
|
+
const tasks = (bundle.status?.tasks || []).filter((task) => taskModuleKey(task) === moduleKey);
|
|
31
|
+
|
|
32
|
+
// Inline Pagination
|
|
33
|
+
state.modulePages = state.modulePages || {};
|
|
34
|
+
const currentPage = state.modulePages[moduleKey] || 1;
|
|
35
|
+
const pageCount = Math.ceil(tasks.length / 8) || 1;
|
|
36
|
+
const visibleTasks = tasks.slice((currentPage - 1) * 8, currentPage * 8);
|
|
37
|
+
|
|
38
|
+
const brief = findDocument(`TARGET:docs/09-PLANNING/MODULES/${moduleKey}/brief.md`);
|
|
39
|
+
|
|
40
|
+
let pagerHtml = "";
|
|
41
|
+
if (tasks.length > 8) {
|
|
42
|
+
pagerHtml = `<div class="module-pager">
|
|
43
|
+
<button ${currentPage <= 1 ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage - 1})">${t("prevPage")}</button>
|
|
44
|
+
<span>${currentPage} / ${pageCount}</span>
|
|
45
|
+
<button ${currentPage >= pageCount ? "disabled" : ""} onclick="window.setModulePage('${escapeAttr(moduleKey)}', ${currentPage + 1})">${t("nextPage")}</button>
|
|
46
|
+
</div>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `<article class="module-card">
|
|
50
|
+
<div class="card-head"><h2>${escapeHtml(module.label || moduleKey)}</h2>${tag(module.state || "unknown")}</div>
|
|
51
|
+
<div class="markdown">${brief ? window.HarnessMarkdown.render(brief.content, "rendered") : `<p>${t("moduleBriefMissing")}</p>`}</div>
|
|
52
|
+
<h3>${t("moduleTasks")} · ${tasks.length}</h3>
|
|
53
|
+
<div class="module-task-list">
|
|
54
|
+
${visibleTasks.map(moduleTaskRow).join("") || `<p>${t("noModuleTasks")}</p>`}
|
|
55
|
+
</div>
|
|
56
|
+
${pagerHtml}
|
|
57
|
+
</article>`;
|
|
58
|
+
}
|