coding-agent-harness 1.0.1 → 1.0.4
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 +44 -0
- package/CONTRIBUTING.md +98 -0
- package/README.en-US.md +14 -0
- package/README.md +230 -80
- package/README.zh-CN.md +290 -0
- package/SKILL.md +132 -198
- package/docs-release/README.md +80 -9
- package/docs-release/architecture/overview.md +298 -28
- package/docs-release/architecture/overview.zh-CN.md +292 -0
- package/docs-release/assets/dashboard-overview.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 +237 -0
- package/docs-release/guides/agent-installation.md +149 -27
- package/docs-release/guides/contributing.md +100 -0
- package/docs-release/guides/contributing.zh-CN.md +99 -0
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +113 -0
- package/docs-release/guides/document-audience-and-surfaces.md +113 -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 +373 -0
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +350 -0
- package/docs-release/guides/migration-playbook.en-US.md +324 -0
- package/docs-release/guides/migration-playbook.md +328 -0
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +254 -0
- package/docs-release/guides/parent-control-repository-pattern.md +254 -0
- package/docs-release/guides/preset-development.md +214 -0
- package/docs-release/guides/repository-operating-models.en-US.md +197 -0
- package/docs-release/guides/repository-operating-models.md +197 -0
- package/docs-release/guides/task-state-machine.en-US.md +207 -0
- package/docs-release/guides/task-state-machine.md +214 -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/findings.md +7 -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 +10 -3
- package/presets/legacy-migration/checks/preset-check.mjs +3 -0
- package/presets/legacy-migration/preset.yaml +134 -0
- package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
- package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
- package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
- package/presets/legacy-migration/templates/findings.seed.md +17 -0
- package/presets/legacy-migration/templates/review.seed.md +12 -0
- package/presets/legacy-migration/templates/task_plan.append.md +9 -0
- package/presets/legacy-migration/templates/visual_map.append.md +12 -0
- package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
- package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
- package/presets/lesson-sedimentation/preset.yaml +23 -0
- package/presets/lesson-sedimentation/templates/prompt.md +23 -0
- package/presets/module/preset.yaml +25 -0
- package/presets/module/templates/execution_strategy.append.md +8 -0
- package/presets/module/templates/task_plan.append.md +17 -0
- package/presets/standard-task/preset.yaml +31 -0
- package/presets/standard-task/templates/task_plan.append.md +7 -0
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +5 -5
- package/references/delivery-operating-model-standard.md +3 -3
- package/references/docs-directory-standard.md +53 -10
- package/references/external-source-intake-standard.md +75 -0
- package/references/harness-ledger.md +53 -94
- package/references/legacy-12-phase-bootstrap.md +41 -0
- package/references/lessons-governance.md +100 -88
- package/references/module-parallel-standard.md +14 -14
- package/references/planning-loop.md +51 -7
- package/references/project-onboarding-audit.md +10 -0
- package/references/pull-request-standard.md +118 -0
- package/references/repo-governance-standard.md +12 -1
- package/references/review-routing-standard.md +7 -1
- package/references/ssot-governance.md +67 -59
- package/references/taskr-gap-analysis.md +600 -0
- package/references/testing-standard.md +50 -0
- package/references/walkthrough-closeout.md +10 -9
- package/scripts/check-harness.mjs +111 -331
- package/scripts/commands/dashboard-command.mjs +67 -0
- package/scripts/commands/migration-command.mjs +96 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +327 -0
- package/scripts/harness.mjs +106 -20
- package/scripts/lib/capability-registry.mjs +591 -0
- package/scripts/lib/check-module-parallel.mjs +237 -0
- package/scripts/lib/check-profiles.mjs +418 -0
- package/scripts/lib/check-task-contracts.mjs +47 -0
- package/scripts/lib/core-shared.mjs +196 -0
- package/scripts/lib/dashboard-data.mjs +412 -0
- package/scripts/lib/dashboard-workbench.mjs +257 -0
- package/scripts/lib/dashboard-writer.mjs +107 -4
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +514 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +15 -1318
- package/scripts/lib/lesson-maintenance.mjs +152 -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/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +497 -0
- package/scripts/lib/preset-registry.mjs +627 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-dashboard-renderer.mjs +102 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- package/scripts/lib/task-completion-consistency.mjs +16 -0
- package/scripts/lib/task-index.mjs +93 -0
- package/scripts/lib/task-lesson-candidates.mjs +242 -0
- package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
- package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +649 -0
- package/scripts/lib/task-review-model.mjs +469 -0
- package/scripts/lib/task-scanner.mjs +576 -0
- package/scripts/lib/task-tombstone-commands.mjs +140 -0
- package/scripts/postinstall.mjs +14 -0
- package/skills/preset-creator/SKILL.md +179 -0
- package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
- package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
- package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
- package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
- package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
- package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
- package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
- package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
- package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
- package/{templates/planning/visual_roadmap.md → skills/preset-creator/references/complex-task-skeleton/visual_map.md} +24 -2
- package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
- package/templates/AGENTS.md.template +51 -36
- 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 +42 -0
- package/templates/dashboard/assets/app-src/10-router.js +77 -0
- package/templates/dashboard/assets/app-src/20-overview.js +241 -0
- package/templates/dashboard/assets/app-src/30-tasks.js +409 -0
- package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
- package/templates/dashboard/assets/app-src/40-modules.js +58 -0
- package/templates/dashboard/assets/app-src/45-review.js +347 -0
- package/templates/dashboard/assets/app-src/50-migration.js +183 -0
- package/templates/dashboard/assets/app-src/60-shared.js +61 -0
- package/templates/dashboard/assets/app-src/90-bindings.js +524 -0
- package/templates/dashboard/assets/app.css +3107 -300
- package/templates/dashboard/assets/app.css.manifest.json +9 -0
- package/templates/dashboard/assets/app.js +2068 -306
- package/templates/dashboard/assets/app.manifest.json +12 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
- package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
- package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
- package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +531 -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/ledger/Harness-Ledger.md +13 -25
- package/templates/lessons/lesson-arch-process-change.md +1 -1
- package/templates/lessons/lesson-new-doc.md +1 -1
- package/templates/lessons/lesson-ref-change.md +1 -1
- package/templates/planning/brief.md +32 -0
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +70 -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/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +59 -0
- package/templates/planning/task_plan.md +40 -15
- package/templates/planning/visual_map.md +50 -0
- package/templates/reference/docs-library-standard.md +31 -0
- package/templates/reference/execution-workflow-standard.md +5 -2
- package/templates/reference/external-source-intake-standard.md +82 -0
- package/templates/reference/harness-ledger-standard.md +1 -0
- package/templates/reference/pull-request-standard.md +80 -0
- package/templates/reference/repo-governance-standard.md +8 -5
- package/templates/reference/review-routing-standard.md +6 -0
- package/templates/reference/walkthrough-standard.md +3 -1
- package/templates/verifier/verifier-output.md +1 -1
- package/templates/walkthrough/walkthrough-template.md +2 -2
- package/templates-zh-CN/AGENTS.md.template +73 -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/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/brief.md +32 -0
- package/templates-zh-CN/planning/execution_strategy.md +30 -0
- package/templates-zh-CN/planning/lesson_candidates.md +70 -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/review.md +59 -1
- package/templates-zh-CN/planning/task_plan.md +37 -11
- package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/docs-library-standard.md +36 -1
- package/templates-zh-CN/reference/execution-workflow-standard.md +10 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
- package/templates-zh-CN/reference/harness-ledger-standard.md +7 -4
- package/templates-zh-CN/reference/pull-request-standard.md +106 -0
- package/templates-zh-CN/reference/repo-governance-standard.md +4 -1
- package/templates-zh-CN/reference/review-routing-standard.md +8 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +6 -5
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
- package/scripts/smoke-dashboard.mjs +0 -70
- package/scripts/test-harness.mjs +0 -483
- package/templates/ssot/Feature-SSoT.md +0 -43
- package/templates/ssot/Lessons-SSoT.md +0 -44
- 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
- package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
- package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
function overview() {
|
|
2
|
+
return `<div class="dashboard-grid">
|
|
3
|
+
<main class="dashboard-main stack">
|
|
4
|
+
${flowPanel()}
|
|
5
|
+
${activeTaskBriefs()}
|
|
6
|
+
${migrationSummaryPanel()}
|
|
7
|
+
</main>
|
|
8
|
+
<aside class="dashboard-sidebar stack">
|
|
9
|
+
${statusStrip()}
|
|
10
|
+
${ledgerPanel()}
|
|
11
|
+
${healthPanel()}
|
|
12
|
+
${lessonPanel()}
|
|
13
|
+
</aside>
|
|
14
|
+
</div>`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function statusStrip() {
|
|
18
|
+
const status = bundle.status?.checkState?.status || "unknown";
|
|
19
|
+
const failures = bundle.status?.checkState?.failures || 0;
|
|
20
|
+
const warnings = bundle.status?.checkState?.warnings || 0;
|
|
21
|
+
const tasks = bundle.status?.tasks || [];
|
|
22
|
+
const summary = bundle.status?.summary || {};
|
|
23
|
+
const visual = summary.visualMapCoverage || {};
|
|
24
|
+
const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
|
|
25
|
+
return `<section class="status-card-group">
|
|
26
|
+
<div class="status-primary ${status}">
|
|
27
|
+
<span>${t("readiness")}</span>
|
|
28
|
+
<strong>${label(status)}</strong>
|
|
29
|
+
<p>${nextActionText()}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="metrics-grid">
|
|
32
|
+
${metric(t("tasks"), tasks.length)}
|
|
33
|
+
${metric(t("briefCoverage"), `${withBrief}/${tasks.length}`)}
|
|
34
|
+
${metric(t("visualMapCoverage"), `${visual.canonical || 0}/${summary.visualMapRequiredCount || tasks.length}`)}
|
|
35
|
+
${metric(t("fullCutover"), summary.fullCutoverEligible ? t("ready") : t("notReady"))}
|
|
36
|
+
${metric(t("legacyVisualOnly"), summary.legacyVisualOnlyCount || 0)}
|
|
37
|
+
${metric(t("weakBrief"), summary.weakBriefCount || 0)}
|
|
38
|
+
${metric(t("blockers"), failures)}
|
|
39
|
+
${metric(t("advice"), warnings)}
|
|
40
|
+
</div>
|
|
41
|
+
</section>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function metric(labelText, value) {
|
|
45
|
+
return `<div class="metric"><span>${escapeHtml(labelText)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nextActionText() {
|
|
49
|
+
const failures = bundle.status?.checkState?.failures || 0;
|
|
50
|
+
if (failures > 0) return t("resolveBlockers");
|
|
51
|
+
const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
|
|
52
|
+
if (missingBriefs > 0) return `${missingBriefs} ${t("missingBriefs")}`;
|
|
53
|
+
const warnings = bundle.status?.checkState?.warnings || 0;
|
|
54
|
+
if (warnings > 0) return t("reviewAdvice");
|
|
55
|
+
return t("noBlockers");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function flowPanel() {
|
|
59
|
+
const tasks = bundle.status?.tasks || [];
|
|
60
|
+
const total = tasks.length;
|
|
61
|
+
if (total === 0) return "";
|
|
62
|
+
const active = tasks.filter((task) => isActiveTaskState(task.state)).length;
|
|
63
|
+
const done = tasks.filter((task) => !isActiveTaskState(task.state) && (task.state === "done" || task.completion === 100)).length;
|
|
64
|
+
const planned = Math.max(0, total - done - active);
|
|
65
|
+
const pct = (n) => total > 0 ? Math.round((n / total) * 100) : 0;
|
|
66
|
+
return `<section class="flow-panel">
|
|
67
|
+
<div class="section-head">
|
|
68
|
+
<div>
|
|
69
|
+
<p class="eyebrow">${t("firstLook")}</p>
|
|
70
|
+
<h2>${t("projectProgress")}</h2>
|
|
71
|
+
</div>
|
|
72
|
+
<span class="subtle">${done}/${total} ${t("completed")}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="progress-bar-container">
|
|
75
|
+
<div class="progress-bar">
|
|
76
|
+
${done > 0 ? `<div class="progress-segment done" style="width:${pct(done)}%" title="${t("done")}: ${done}"></div>` : ""}
|
|
77
|
+
${active > 0 ? `<div class="progress-segment active" style="width:${pct(active)}%" title="${t("active")}: ${active}"></div>` : ""}
|
|
78
|
+
${planned > 0 ? `<div class="progress-segment planned" style="width:${pct(planned)}%" title="${t("planned")}: ${planned}"></div>` : ""}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="progress-legend">
|
|
81
|
+
<span class="legend-item"><span class="legend-dot done"></span>${t("done")} ${done}</span>
|
|
82
|
+
<span class="legend-item"><span class="legend-dot active"></span>${t("active")} ${active}</span>
|
|
83
|
+
<span class="legend-item"><span class="legend-dot planned"></span>${t("planned")} ${planned}</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
${usesAggregateFlow() ? migrationRunwayBreakdown() : ""}
|
|
87
|
+
</section>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function projectMermaid() {
|
|
91
|
+
if (usesAggregateFlow()) return migrationAggregateMermaid();
|
|
92
|
+
const graph = bundle.graph || { nodes: [], edges: [] };
|
|
93
|
+
const preferredTypes = graph.nodes?.some((node) => node.type === "module") ? ["module", "step"] : ["task", "phase"];
|
|
94
|
+
const nodes = (graph.nodes || [])
|
|
95
|
+
.filter((node) => preferredTypes.includes(node.type))
|
|
96
|
+
.filter((node) => node.type !== "phase" || ["in_progress", "review", "blocked", "done"].includes(node.state))
|
|
97
|
+
.slice(0, 28);
|
|
98
|
+
if (nodes.length < 2) return mermaidFromBriefs();
|
|
99
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
100
|
+
const lines = ["flowchart LR"];
|
|
101
|
+
let edgeCount = 0;
|
|
102
|
+
for (const edge of graph.edges || []) {
|
|
103
|
+
if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) continue;
|
|
104
|
+
lines.push(` ${mermaidId(edge.from)}["${mermaidLabel(edge.from)}"] --> ${mermaidId(edge.to)}["${mermaidLabel(edge.to)}"]`);
|
|
105
|
+
edgeCount += 1;
|
|
106
|
+
if (edgeCount >= 34) break;
|
|
107
|
+
}
|
|
108
|
+
if (edgeCount === 0) {
|
|
109
|
+
for (let index = 1; index < nodes.length; index += 1) {
|
|
110
|
+
lines.push(` ${mermaidId(nodes[index - 1].id)}["${mermaidLabel(nodes[index - 1].id)}"] --> ${mermaidId(nodes[index].id)}["${mermaidLabel(nodes[index].id)}"]`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function usesAggregateFlow() {
|
|
117
|
+
const graph = bundle.graph || { nodes: [], edges: [] };
|
|
118
|
+
const taskCount = (bundle.status?.tasks || []).length;
|
|
119
|
+
const taskNodes = (graph.nodes || []).filter((node) => node.type === "task").length;
|
|
120
|
+
const usefulEdges = (graph.edges || []).filter((edge) => ["depends_on", "current_step"].includes(edge.type)).length;
|
|
121
|
+
return taskCount > 80 || taskNodes > 80 || ((graph.nodes || []).length > 80 && usefulEdges < 6);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function migrationAggregateMermaid() {
|
|
125
|
+
const tasks = bundle.status?.tasks || [];
|
|
126
|
+
const warnings = warningQueue();
|
|
127
|
+
const activeContracts = warnings.filter((warning) => warning.phase === "active-task-contracts").length;
|
|
128
|
+
const moduleCount = new Set(tasks.map(taskModuleKey)).size;
|
|
129
|
+
const reviewWarnings = warnings.filter((warning) => ["review-evidence", "strict-cutover"].includes(warning.phase)).length;
|
|
130
|
+
const lines = [
|
|
131
|
+
"flowchart LR",
|
|
132
|
+
` baseline["${t("runwayBaseline")}\\n${tasks.length} ${t("tasks")}"] --> triage["${t("runwayTriage")}\\n${warnings.length} ${t("warnings")}"]`,
|
|
133
|
+
` triage --> contracts["${t("runwayContracts")}\\n${activeContracts} ${t("items")}"]`,
|
|
134
|
+
` contracts --> modules["${t("runwayModules")}\\n${moduleCount} ${t("groups")}"]`,
|
|
135
|
+
` modules --> cutover["${t("runwayCutover")}\\n${reviewWarnings} ${t("items")}"]`,
|
|
136
|
+
];
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function migrationRunwayBreakdown() {
|
|
141
|
+
const tasks = bundle.status?.tasks || [];
|
|
142
|
+
const warnings = warningQueue();
|
|
143
|
+
const phases = [
|
|
144
|
+
["baseline", t("runwayBaseline"), tasks.length, t("tasks"), "#/tasks"],
|
|
145
|
+
["triage", t("runwayTriage"), warnings.length, t("warnings"), "#/"],
|
|
146
|
+
["active-task-contracts", t("runwayContracts"), warnings.filter((warning) => warning.phase === "active-task-contracts").length, t("items"), "#/"],
|
|
147
|
+
["module-classification", t("runwayModules"), new Set(tasks.map(taskModuleKey)).size, t("groups"), "#/tasks"],
|
|
148
|
+
["strict-cutover", t("runwayCutover"), warnings.filter((warning) => warning.phase === "strict-cutover").length, t("items"), "#/"],
|
|
149
|
+
];
|
|
150
|
+
return `<div class="runway-breakdown">
|
|
151
|
+
${phases.map(([phase, title, count, unit, href]) => `<a href="${href}" data-runway-phase="${escapeAttr(phase)}"><strong>${escapeHtml(title)}</strong><span>${count} ${escapeHtml(unit)}</span></a>`).join("")}
|
|
152
|
+
</div>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function mermaidFromBriefs() {
|
|
156
|
+
const brief = activeTasks().map((task) => taskDocument(task, "brief.md")).find((doc) => doc?.content?.includes("```mermaid"));
|
|
157
|
+
const match = brief?.content.match(/```mermaid\s*([\s\S]*?)```/i);
|
|
158
|
+
return match ? match[1].trim() : "";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function graphSummary() {
|
|
162
|
+
const graph = bundle.graph || { nodes: [], edges: [] };
|
|
163
|
+
if (usesAggregateFlow()) return `${t("aggregateMigrationView")} · ${(bundle.status?.tasks || []).length} ${t("tasks")}`;
|
|
164
|
+
return `${graph.nodes?.length || 0} ${t("nodes")} · ${graph.edges?.length || 0} ${t("edges")}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function activeTaskBriefs() {
|
|
168
|
+
const tasks = activeTasks();
|
|
169
|
+
return `<section class="task-briefs">
|
|
170
|
+
<div class="section-head">
|
|
171
|
+
<div>
|
|
172
|
+
<p class="eyebrow">${t("currentWork")}</p>
|
|
173
|
+
<h2>${t("activeBriefs")}</h2>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="section-actions">
|
|
176
|
+
<span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
|
|
177
|
+
<a href="#/tasks">${t("openTaskIndex")}</a>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="brief-scroll">
|
|
181
|
+
<div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
|
|
182
|
+
</div>
|
|
183
|
+
</section>`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function activeTasks() {
|
|
187
|
+
const tasks = bundle.status?.tasks || [];
|
|
188
|
+
const active = tasks.filter((task) => isActiveTaskState(task.state) || ["planned", "not_started"].includes(task.state));
|
|
189
|
+
if (active.length > 0) return sortTasksByTime(active);
|
|
190
|
+
return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isActiveTaskState(state) {
|
|
194
|
+
return ["active", "in_progress", "review", "blocked", "reopened", "current-evidence"].includes(state);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function taskBriefCard(task, { compact = true } = {}) {
|
|
198
|
+
const doc = taskDocument(task, "brief.md");
|
|
199
|
+
const summaryText = doc ? getBriefSummary(doc.content) : t("missingBriefExplain");
|
|
200
|
+
return `<article class="brief-card ${compact ? "compact" : ""}">
|
|
201
|
+
<div class="card-head">
|
|
202
|
+
<div>
|
|
203
|
+
<a href="#/tasks/${encodeURIComponent(task.id)}">${escapeHtml(task.title)}</a>
|
|
204
|
+
<p>${escapeHtml(task.id)}</p>
|
|
205
|
+
</div>
|
|
206
|
+
${tag(task.state)}
|
|
207
|
+
</div>
|
|
208
|
+
${progressBar(task.completion)}
|
|
209
|
+
<div class="brief-content">
|
|
210
|
+
<p class="brief-teaser">${escapeHtml(summaryText)}</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="card-actions">
|
|
213
|
+
${taskCopyButton(task)}
|
|
214
|
+
<button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
215
|
+
</div>
|
|
216
|
+
</article>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getBriefSummary(content) {
|
|
220
|
+
if (!content) return "";
|
|
221
|
+
let text = content
|
|
222
|
+
.replace(/#+\s+/g, "")
|
|
223
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
224
|
+
.replace(/[*_`]/g, "")
|
|
225
|
+
.replace(/-\s+/g, "")
|
|
226
|
+
.replace(/>\s+/g, "")
|
|
227
|
+
.replaceAll("\n", " ")
|
|
228
|
+
.replace(/\s+/g, " ")
|
|
229
|
+
.trim();
|
|
230
|
+
if (text.length > 140) text = text.slice(0, 137) + "...";
|
|
231
|
+
return text;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function generatedBrief(task) {
|
|
235
|
+
const phaseText = (task.phases || []).slice(0, 6).map((phase) => `<li><strong>${escapeHtml(phase.id)}</strong> ${escapeHtml(phase.output || phase.state)} · ${phase.completion}%</li>`).join("");
|
|
236
|
+
return `<div class="missing-brief">
|
|
237
|
+
<strong>${t("visibilityBriefMissing")}</strong>
|
|
238
|
+
<p>${t("missingBriefExplain")}</p>
|
|
239
|
+
<ul>${phaseText || `<li>${t("noPhaseData")}</li>`}</ul>
|
|
240
|
+
</div>`;
|
|
241
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
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 taskSortLabel() {
|
|
12
|
+
return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function taskDateKey(task) {
|
|
16
|
+
const source = `${task.shortId || ""} ${task.id || ""}`.trim();
|
|
17
|
+
const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const year = Number(match[1]);
|
|
20
|
+
const month = Number(match[2]);
|
|
21
|
+
const day = Number(match[3] || "1");
|
|
22
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
23
|
+
return Date.UTC(year, month - 1, day);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stableTaskLabel(task) {
|
|
27
|
+
return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compareTasksByTime(left, right) {
|
|
31
|
+
const leftDate = taskDateKey(left);
|
|
32
|
+
const rightDate = taskDateKey(right);
|
|
33
|
+
if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
|
|
34
|
+
return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
|
|
35
|
+
}
|
|
36
|
+
if (leftDate !== null && rightDate === null) return -1;
|
|
37
|
+
if (leftDate === null && rightDate !== null) return 1;
|
|
38
|
+
return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortTasksByTime(tasks) {
|
|
42
|
+
return [...tasks].sort(compareTasksByTime);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function taskFolderName(task) {
|
|
46
|
+
const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
|
|
47
|
+
const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
|
|
48
|
+
return task?.shortId || fromPath || fromId || task?.title || "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function taskCopyButton(task, extraClass = "") {
|
|
52
|
+
const folderName = taskFolderName(task);
|
|
53
|
+
return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
|
|
54
|
+
${t("copyTaskNameShort")}
|
|
55
|
+
</button>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function taskGroupTimeKey(group) {
|
|
59
|
+
const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
|
|
60
|
+
if (!match) return null;
|
|
61
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function taskToolbarCard(filteredCount) {
|
|
65
|
+
return `<section class="sidebar-card">
|
|
66
|
+
<h3>${t("filterTitle")}</h3>
|
|
67
|
+
<div class="input-group">
|
|
68
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
69
|
+
</div>
|
|
70
|
+
<div class="select-group">
|
|
71
|
+
<label>${t("stateFilter")}</label>
|
|
72
|
+
<select data-state-filter aria-label="${t("stateFilter")}">
|
|
73
|
+
${["all", "in_progress", "review", "blocked", "planned", "done", "unknown"].map((value) => `<option value="${value}" ${state.taskState === value ? "selected" : ""}>${label(value)}</option>`).join("")}
|
|
74
|
+
</select>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="select-group">
|
|
77
|
+
<label>${t("groupBy")}</label>
|
|
78
|
+
<select data-group-mode aria-label="${t("groupBy")}">
|
|
79
|
+
${["migration", "module", "month", "state"].map((value) => `<option value="${value}" ${state.taskGroupMode === value ? "selected" : ""}>${t(`group_${value}`)}</option>`).join("")}
|
|
80
|
+
</select>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="select-group">
|
|
83
|
+
<label>${t("layout")}</label>
|
|
84
|
+
<div class="layout-toggle-group">
|
|
85
|
+
<button class="layout-btn ${state.taskLayout === "list" ? "active" : ""}" data-layout="list" aria-label="${t("layoutList")}">
|
|
86
|
+
<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>
|
|
87
|
+
${t("layoutList")}
|
|
88
|
+
</button>
|
|
89
|
+
<button class="layout-btn ${state.taskLayout === "grid" ? "active" : ""}" data-layout="grid" aria-label="${t("layoutGrid")}">
|
|
90
|
+
<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>
|
|
91
|
+
${t("layoutGrid")}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="select-group">
|
|
96
|
+
<label>${t("sortByTime")}</label>
|
|
97
|
+
<div class="layout-toggle-group sort-toggle-group">
|
|
98
|
+
<button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
|
|
99
|
+
${t("sortNewest")}
|
|
100
|
+
</button>
|
|
101
|
+
<button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
|
|
102
|
+
${t("sortOldest")}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="search-stats">
|
|
107
|
+
${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
|
|
108
|
+
</div>
|
|
109
|
+
</section>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function taskStatsCard() {
|
|
113
|
+
const allTasks = bundle.status?.tasks || [];
|
|
114
|
+
const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
|
|
115
|
+
return `<section class="sidebar-card">
|
|
116
|
+
<h3>${t("releaseHealth")}</h3>
|
|
117
|
+
<div class="stats-hero-gauge">
|
|
118
|
+
<span class="gauge-percentage">${avgCompletion}%</span>
|
|
119
|
+
<span class="gauge-label">${t("statOverall")}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="stats-breakdown">
|
|
122
|
+
${[
|
|
123
|
+
{ state: "in_progress", label: t("statInProgress"), colorVar: "--accent" },
|
|
124
|
+
{ state: "review", label: t("statReview"), colorVar: "--accent-2" },
|
|
125
|
+
{ state: "blocked", label: t("statBlocked"), colorVar: "--danger" },
|
|
126
|
+
{ state: "done", label: t("statDone"), colorVar: "--ok" }
|
|
127
|
+
].map(({ state, label, colorVar }) => {
|
|
128
|
+
const count = allTasks.filter(t => t.state === state).length;
|
|
129
|
+
return `<div class="stats-breakdown-row">
|
|
130
|
+
<span class="stat-label">
|
|
131
|
+
<span class="state-dot" style="background:var(${colorVar})"></span>
|
|
132
|
+
${label}
|
|
133
|
+
</span>
|
|
134
|
+
<span class="stat-value">${count}</span>
|
|
135
|
+
</div>`;
|
|
136
|
+
}).join("")}
|
|
137
|
+
</div>
|
|
138
|
+
</section>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function taskLegendCard() {
|
|
142
|
+
return `<section class="sidebar-card">
|
|
143
|
+
<h3>${t("legendTitle")}</h3>
|
|
144
|
+
<div class="legend-list">
|
|
145
|
+
<div class="legend-item">
|
|
146
|
+
<span class="badge brief ready" style="margin-top:2px">
|
|
147
|
+
<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>
|
|
148
|
+
${t("badgeBrief")}
|
|
149
|
+
</span>
|
|
150
|
+
<span>${t("legendBriefDesc")}</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="legend-item">
|
|
153
|
+
<span class="badge map ready" style="margin-top:2px">
|
|
154
|
+
<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>
|
|
155
|
+
${t("badgeMap")}
|
|
156
|
+
</span>
|
|
157
|
+
<span>${t("legendMapDesc")}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</section>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function taskStatsBar() {
|
|
164
|
+
const allTasks = bundle.status?.tasks || [];
|
|
165
|
+
const inProgress = allTasks.filter(t => t.state === "in_progress").length;
|
|
166
|
+
const blocked = allTasks.filter(t => t.state === "blocked").length;
|
|
167
|
+
const done = allTasks.filter(t => t.state === "done").length;
|
|
168
|
+
const review = allTasks.filter(t => t.state === "review").length;
|
|
169
|
+
const avgCompletion = allTasks.length ? clampCompletion(allTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / allTasks.length) : 0;
|
|
170
|
+
|
|
171
|
+
return `<section class="task-stats-bar">
|
|
172
|
+
<div class="stat-chip">
|
|
173
|
+
<span class="stat-value">${allTasks.length}</span>
|
|
174
|
+
<span class="stat-label">${t("statTotal")}</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="stat-chip in-progress">
|
|
177
|
+
<span class="stat-value">${inProgress}</span>
|
|
178
|
+
<span class="stat-label">${t("statInProgress")}</span>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="stat-chip review">
|
|
181
|
+
<span class="stat-value">${review}</span>
|
|
182
|
+
<span class="stat-label">${t("statReview")}</span>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="stat-chip blocked">
|
|
185
|
+
<span class="stat-value">${blocked}</span>
|
|
186
|
+
<span class="stat-label">${t("statBlocked")}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="stat-chip done">
|
|
189
|
+
<span class="stat-value">${done}</span>
|
|
190
|
+
<span class="stat-label">${t("statDone")}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="stat-chip completion">
|
|
193
|
+
<div class="stat-bar-track"><div class="stat-bar-fill" style="width:${avgCompletion}%"></div></div>
|
|
194
|
+
<div style="text-align:right">
|
|
195
|
+
<span class="stat-value">${avgCompletion}%</span>
|
|
196
|
+
<span class="stat-label" style="display:block;margin-top:2px">${t("statOverall")}</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</section>`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function taskRow(task) {
|
|
203
|
+
const completion = clampCompletion(task.completion);
|
|
204
|
+
const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
|
|
205
|
+
const mapReady = !!taskDocument(task, "visual_map.md");
|
|
206
|
+
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
207
|
+
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
208
|
+
|
|
209
|
+
return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
210
|
+
<div class="row-accent-bar"></div>
|
|
211
|
+
<div class="row-main">
|
|
212
|
+
<strong>${escapeHtml(task.title)}</strong>
|
|
213
|
+
<span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
|
|
214
|
+
${taskCopyButton(task, "row-copy")}
|
|
215
|
+
</div>
|
|
216
|
+
<div class="row-status">${tag(task.state)}</div>
|
|
217
|
+
<div class="row-progress">
|
|
218
|
+
<div class="mini-progress-track"><div class="mini-progress-fill" style="width:${completion}%"></div></div>
|
|
219
|
+
<span class="row-pct">${completion}%</span>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="row-brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}" aria-label="${escapeAttr(briefLabel)}">
|
|
222
|
+
<span class="badge brief ${briefReady ? "ready" : "missing"}">
|
|
223
|
+
<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>
|
|
224
|
+
${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="row-map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}" aria-label="${escapeAttr(mapLabel)}">
|
|
228
|
+
<span class="badge map ${mapReady ? "ready" : "missing"}">
|
|
229
|
+
<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>
|
|
230
|
+
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
</article>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function taskIndex() {
|
|
237
|
+
const tasks = filteredTasks();
|
|
238
|
+
const groups = taskGroups(tasks);
|
|
239
|
+
const orderedGroups = orderedTaskGroups(groups);
|
|
240
|
+
const groupPageCount = Math.max(1, Math.ceil(orderedGroups.length / taskGroupsPerPage));
|
|
241
|
+
const groupPage = Math.min(Math.max(1, Number(state.taskGroupPage) || 1), groupPageCount);
|
|
242
|
+
const visibleGroups = orderedGroups.slice((groupPage - 1) * taskGroupsPerPage, groupPage * taskGroupsPerPage);
|
|
243
|
+
|
|
244
|
+
return `<div class="tasks-grid">
|
|
245
|
+
<div class="tasks-main stack">
|
|
246
|
+
${taskStatsBar()}
|
|
247
|
+
${visibleGroups.map(([group, groupTasks]) => taskGroup(group, groupTasks)).join("")}
|
|
248
|
+
<section class="group-pager">
|
|
249
|
+
<span>${t("showingGroups")} ${visibleGroups.length ? (groupPage - 1) * taskGroupsPerPage + 1 : 0}-${Math.min(groupPage * taskGroupsPerPage, orderedGroups.length)} / ${orderedGroups.length}</span>
|
|
250
|
+
${pager("task-groups", groupPage, groupPageCount)}
|
|
251
|
+
</section>
|
|
252
|
+
</div>
|
|
253
|
+
<aside class="tasks-sidebar stack">
|
|
254
|
+
${taskToolbarCard(tasks.length)}
|
|
255
|
+
${taskStatsCard()}
|
|
256
|
+
${taskLegendCard()}
|
|
257
|
+
</aside>
|
|
258
|
+
</div>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function orderedTaskGroups(groups) {
|
|
262
|
+
const rank = (group) => {
|
|
263
|
+
if (group.startsWith("module:")) return 2;
|
|
264
|
+
if (group.startsWith("state:")) return 2;
|
|
265
|
+
if (group.startsWith("month:")) return 2;
|
|
266
|
+
if (group === "active") return 0;
|
|
267
|
+
if (group === "brief-ready") return 1;
|
|
268
|
+
if (group.startsWith("legacy:")) return 2;
|
|
269
|
+
if (group === "unknown") return 3;
|
|
270
|
+
return 4;
|
|
271
|
+
};
|
|
272
|
+
return Object.entries(groups).sort(([left], [right]) => {
|
|
273
|
+
const rankDiff = rank(left) - rank(right);
|
|
274
|
+
if (rankDiff !== 0) return rankDiff;
|
|
275
|
+
const leftTime = taskGroupTimeKey(left);
|
|
276
|
+
const rightTime = taskGroupTimeKey(right);
|
|
277
|
+
if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
|
|
278
|
+
return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
|
|
279
|
+
}
|
|
280
|
+
if (leftTime !== null && rightTime === null) return -1;
|
|
281
|
+
if (leftTime === null && rightTime !== null) return 1;
|
|
282
|
+
return left.localeCompare(right);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function taskGroups(tasks) {
|
|
287
|
+
if (state.taskGroupMode === "module") {
|
|
288
|
+
return groupBy(tasks, (task) => `module:${taskModuleKey(task)}`);
|
|
289
|
+
}
|
|
290
|
+
if (state.taskGroupMode === "month") {
|
|
291
|
+
return groupBy(tasks, (task) => {
|
|
292
|
+
const match = task.shortId?.match(/^(\d{4}-\d{2})/);
|
|
293
|
+
return match ? `month:${match[1]}` : "month:unknown";
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (state.taskGroupMode === "state") {
|
|
297
|
+
return groupBy(tasks, (task) => `state:${task.state || "unknown"}`);
|
|
298
|
+
}
|
|
299
|
+
return groupBy(tasks, (task) => {
|
|
300
|
+
if (["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state)) return "active";
|
|
301
|
+
if (task.briefSource === "standalone") return "brief-ready";
|
|
302
|
+
const match = task.shortId?.match(/^(\d{4}-\d{2})/);
|
|
303
|
+
return match ? `legacy:${match[1]}` : task.state || "unknown";
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function taskGroup(group, tasks) {
|
|
308
|
+
const orderedTasks = sortTasksByTime(tasks);
|
|
309
|
+
const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
|
|
310
|
+
const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
|
|
311
|
+
const start = (page - 1) * taskPageSize;
|
|
312
|
+
const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
|
|
313
|
+
const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
|
|
314
|
+
|
|
315
|
+
const isGrid = state.taskLayout === "grid";
|
|
316
|
+
const layoutClass = isGrid ? "task-card-grid" : "task-list";
|
|
317
|
+
const itemRenderer = isGrid ? taskCard : taskRow;
|
|
318
|
+
const listHeader = isGrid ? "" : `<div class="task-list-header">
|
|
319
|
+
<div class="col-main">${t("columnTask")}</div>
|
|
320
|
+
<div class="col-status">${t("columnState")}</div>
|
|
321
|
+
<div class="col-progress">${t("columnCompletion")}</div>
|
|
322
|
+
<div class="col-brief">${t("columnBrief")}</div>
|
|
323
|
+
<div class="col-map">${t("badgeMap")}</div>
|
|
324
|
+
</div>`;
|
|
325
|
+
|
|
326
|
+
return `<section class="task-group">
|
|
327
|
+
<div class="section-head">
|
|
328
|
+
<div>
|
|
329
|
+
<h2>${taskGroupLabel(group)}</h2>
|
|
330
|
+
<p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="group-actions">
|
|
333
|
+
<div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
|
|
334
|
+
<div class="group-progress-track"><div class="group-progress-fill" style="width:${avgCompletion}%"></div></div>
|
|
335
|
+
<span>${avgCompletion}%</span>
|
|
336
|
+
</div>
|
|
337
|
+
${pager("task", page, pageCount, group)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="${layoutClass}">
|
|
341
|
+
${listHeader}
|
|
342
|
+
${visibleTasks.map(itemRenderer).join("")}
|
|
343
|
+
</div>
|
|
344
|
+
</section>`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function taskCard(task) {
|
|
348
|
+
const completion = clampCompletion(task.completion);
|
|
349
|
+
const stateColor = stateToColorVar(task.state);
|
|
350
|
+
const briefReady = task.briefSource === "standalone" || !!taskDocument(task, "brief.md");
|
|
351
|
+
const mapReady = !!taskDocument(task, "visual_map.md");
|
|
352
|
+
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
353
|
+
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
354
|
+
|
|
355
|
+
return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
|
|
356
|
+
<div class="card-header">
|
|
357
|
+
<span class="card-id">${escapeHtml(task.id)}</span>
|
|
358
|
+
<div class="card-header-actions">
|
|
359
|
+
${taskCopyButton(task, "compact")}
|
|
360
|
+
${tag(task.state)}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
364
|
+
<div class="card-meta">
|
|
365
|
+
<span class="meta-module" title="${escapeAttr(taskModuleKey(task))}">
|
|
366
|
+
<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>
|
|
367
|
+
${escapeHtml(taskModuleKey(task))}
|
|
368
|
+
</span>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="card-progress">
|
|
371
|
+
<div class="card-progress-track"><div class="card-progress-fill" style="width:${completion}%"></div></div>
|
|
372
|
+
<span class="progress-pct">${completion}%</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="card-badges">
|
|
375
|
+
<span class="badge brief ${briefReady ? "ready" : "missing"}" title="${escapeAttr(briefLabel)}">
|
|
376
|
+
<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>
|
|
377
|
+
${briefReady ? t("badgeBrief") : t("badgeBriefMissing")}
|
|
378
|
+
</span>
|
|
379
|
+
<span class="badge map ${mapReady ? "ready" : "missing"}" title="${escapeAttr(mapLabel)}">
|
|
380
|
+
<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>
|
|
381
|
+
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
382
|
+
</span>
|
|
383
|
+
</div>
|
|
384
|
+
</article>`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function taskGroupLabel(group) {
|
|
388
|
+
if (group === "active") return t("activeCurrent");
|
|
389
|
+
if (group === "brief-ready") return t("briefReadyGroup");
|
|
390
|
+
if (group.startsWith("legacy:")) return `${t("legacyMonth")} ${group.slice("legacy:".length)}`;
|
|
391
|
+
if (group.startsWith("module:")) return `${t("inferredModule")} · ${group.slice("module:".length)}`;
|
|
392
|
+
if (group.startsWith("month:")) return `${t("legacyMonth")} ${group.slice("month:".length)}`;
|
|
393
|
+
if (group.startsWith("state:")) return `${t("columnState")} · ${label(group.slice("state:".length))}`;
|
|
394
|
+
return label(group);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function filteredTasks() {
|
|
398
|
+
const query = state.query.trim().toLowerCase();
|
|
399
|
+
return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
|
|
400
|
+
const stateMatch = state.taskState === "all" || task.state === state.taskState;
|
|
401
|
+
if (!stateMatch) return false;
|
|
402
|
+
if (!query) return true;
|
|
403
|
+
return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function taskModuleKey(task) {
|
|
408
|
+
return task.module || task.inferredModule || "legacy-unclassified";
|
|
409
|
+
}
|