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,246 @@
|
|
|
1
|
+
function taskDetail(route) {
|
|
2
|
+
const taskId = route.id;
|
|
3
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
|
|
4
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
5
|
+
return `<main class="task-detail">
|
|
6
|
+
<nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
7
|
+
<section class="detail-hero">
|
|
8
|
+
<div>
|
|
9
|
+
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
10
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
11
|
+
<p>${escapeHtml(task.path)}</p>
|
|
12
|
+
${taskCopyButton(task, "detail-copy")}
|
|
13
|
+
</div>
|
|
14
|
+
<div class="detail-score">${task.completion}%</div>
|
|
15
|
+
</section>
|
|
16
|
+
${taskStateSummary(task)}
|
|
17
|
+
${phaseTimeline(task)}
|
|
18
|
+
<section class="detail-grid">
|
|
19
|
+
<article class="detail-main">
|
|
20
|
+
${taskDocumentLibrary(task, route.doc)}
|
|
21
|
+
</article>
|
|
22
|
+
<aside class="detail-side">
|
|
23
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
24
|
+
${lessonCandidatePanel(task, { context: "detail" })}
|
|
25
|
+
${openFindings(task)}
|
|
26
|
+
${evidenceList(task)}
|
|
27
|
+
${documentTabs(task)}
|
|
28
|
+
</aside>
|
|
29
|
+
</section>
|
|
30
|
+
</main>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function taskStateSummary(task) {
|
|
34
|
+
return `<section class="task-state-summary">
|
|
35
|
+
<div>
|
|
36
|
+
<span>${t("legacyState")}</span>
|
|
37
|
+
${tag(task.state)}
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<span>${t("lifecycleState")}</span>
|
|
41
|
+
${tag(task.lifecycleState || "unknown")}
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<span>${t("reviewStatus")}</span>
|
|
45
|
+
${tag(task.reviewStatus || "missing")}
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<span>${t("sedimentationStatus")}</span>
|
|
49
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<span>${t("closeoutStatus")}</span>
|
|
53
|
+
${tag(task.closeoutStatus || "missing")}
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<span>${t("lifecycleQueues")}</span>
|
|
57
|
+
${(task.taskQueues || []).map(tag).join("") || tag("active")}
|
|
58
|
+
</div>
|
|
59
|
+
${taskQueueReasonSummary(task)}
|
|
60
|
+
</section>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function taskQueueReasonSummary(task) {
|
|
64
|
+
const reasons = task.queueReasons || [];
|
|
65
|
+
if (!reasons.length) return "";
|
|
66
|
+
return `<div class="task-queue-reasons">
|
|
67
|
+
<span>${t("queueReasons")}</span>
|
|
68
|
+
<div class="review-reasons">
|
|
69
|
+
${reasons.slice(0, 5).map(reviewReason).join("")}
|
|
70
|
+
</div>
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function phaseTimeline(task) {
|
|
75
|
+
return `<section class="phase-timeline">
|
|
76
|
+
<h2>${t("phaseTimeline")}</h2>
|
|
77
|
+
${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
|
|
78
|
+
<strong>${escapeHtml(phase.id)}</strong>
|
|
79
|
+
<span>${phase.completion}%</span>
|
|
80
|
+
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
81
|
+
${progressBar(phase.completion)}
|
|
82
|
+
</div>`).join("") || emptyState(t("noPhaseData"))}
|
|
83
|
+
</section>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function taskDocSection(task, fileName, title, required) {
|
|
87
|
+
const doc = taskDocument(task, fileName);
|
|
88
|
+
if (!doc && !required) return "";
|
|
89
|
+
return `<section class="doc-section">
|
|
90
|
+
<div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
|
|
91
|
+
<div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
|
|
92
|
+
</section>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function taskDocumentLibrary(task, selectedTab) {
|
|
96
|
+
const docs = orderedTaskDocuments(task);
|
|
97
|
+
if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
|
|
98
|
+
const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
|
|
99
|
+
return `<section class="doc-library">
|
|
100
|
+
<div class="section-head">
|
|
101
|
+
<div>
|
|
102
|
+
<p class="eyebrow">${t("taskDocuments")}</p>
|
|
103
|
+
<h2>${escapeHtml(t("sourceDocuments"))}</h2>
|
|
104
|
+
</div>
|
|
105
|
+
<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="doc-accordion-list">
|
|
108
|
+
${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
|
|
109
|
+
</div>
|
|
110
|
+
</section>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function orderedTaskDocuments(task) {
|
|
114
|
+
const docs = taskDocTabs
|
|
115
|
+
.map(([key, file]) => {
|
|
116
|
+
const doc = taskDocument(task, file);
|
|
117
|
+
if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
|
|
118
|
+
if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
|
|
119
|
+
return null;
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const priority = taskDocumentPriority(task);
|
|
123
|
+
const rank = new Map(priority.map((key, index) => [key, index]));
|
|
124
|
+
return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskDocumentPriority(task) {
|
|
128
|
+
const stateName = task?.state || "";
|
|
129
|
+
const lifecycle = task?.lifecycleState || "";
|
|
130
|
+
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
131
|
+
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
132
|
+
}
|
|
133
|
+
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
134
|
+
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
135
|
+
}
|
|
136
|
+
if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
|
|
137
|
+
return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
|
|
138
|
+
}
|
|
139
|
+
return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function defaultTaskDocumentKey(task, docs) {
|
|
143
|
+
const priority = taskDocumentPriority(task);
|
|
144
|
+
return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function documentAccordion(item, open) {
|
|
148
|
+
return `<details class="doc-accordion" ${open ? "open" : ""}>
|
|
149
|
+
<summary>
|
|
150
|
+
<span>${escapeHtml(item.title)}</span>
|
|
151
|
+
<small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
|
|
152
|
+
</summary>
|
|
153
|
+
<div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
|
|
154
|
+
</details>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function documentTabs(task) {
|
|
158
|
+
const docs = orderedTaskDocuments(task);
|
|
159
|
+
return `<section class="side-panel">
|
|
160
|
+
<h3>${t("sourceDocuments")}</h3>
|
|
161
|
+
${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>`}
|
|
162
|
+
</section>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function selectedSourceDocument(task, tab) {
|
|
166
|
+
if (!tab) return "";
|
|
167
|
+
const match = taskDocTabs.find(([key]) => key === tab);
|
|
168
|
+
if (!match) return "";
|
|
169
|
+
const doc = taskDocument(task, match[1]);
|
|
170
|
+
if (!doc) return "";
|
|
171
|
+
return `<section class="doc-section selected-source">
|
|
172
|
+
<div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
|
|
173
|
+
<div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
|
|
174
|
+
</section>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function openFindings(task) {
|
|
178
|
+
const risks = task.risks || [];
|
|
179
|
+
return `<section class="side-panel">
|
|
180
|
+
<h3>${t("openFindings")}</h3>
|
|
181
|
+
${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>`}
|
|
182
|
+
</section>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
186
|
+
if (!isTaskInReviewQueue(task)) return "";
|
|
187
|
+
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
188
|
+
const confirmed = task.reviewStatus === "confirmed";
|
|
189
|
+
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
190
|
+
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
191
|
+
if (mode !== "workspace") {
|
|
192
|
+
return `<section class="side-panel review-actions">
|
|
193
|
+
<h3>${t("reviewActions")}</h3>
|
|
194
|
+
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
195
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
196
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
197
|
+
</section>`;
|
|
198
|
+
}
|
|
199
|
+
if (!canUseWorkbenchAction("review-complete")) {
|
|
200
|
+
return `<section class="side-panel review-actions">
|
|
201
|
+
<h3>${t("reviewActions")}</h3>
|
|
202
|
+
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
203
|
+
</section>`;
|
|
204
|
+
}
|
|
205
|
+
if (confirmed) {
|
|
206
|
+
return `<section class="side-panel review-actions">
|
|
207
|
+
<h3>${t("reviewActions")}</h3>
|
|
208
|
+
<p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
|
|
209
|
+
</section>`;
|
|
210
|
+
}
|
|
211
|
+
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
212
|
+
const queueBlocked = !taskCanBeHumanConfirmed(task);
|
|
213
|
+
const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
|
|
214
|
+
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
|
|
215
|
+
return `<section class="side-panel review-actions">
|
|
216
|
+
<h3>${t("reviewActions")}</h3>
|
|
217
|
+
<p>${escapeHtml(message)}</p>
|
|
218
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
219
|
+
<label class="review-check">
|
|
220
|
+
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
221
|
+
<span>${t("reviewConfirmChecklist")}</span>
|
|
222
|
+
</label>
|
|
223
|
+
<div class="review-confirm-copy">
|
|
224
|
+
${taskCopyButton(task, "review-copy-task-name")}
|
|
225
|
+
</div>
|
|
226
|
+
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
227
|
+
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
228
|
+
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
229
|
+
</section>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isTaskInReviewQueue(task) {
|
|
233
|
+
return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function taskCanBeHumanConfirmed(task) {
|
|
237
|
+
return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function evidenceList(task) {
|
|
241
|
+
const evidence = task.evidence || [];
|
|
242
|
+
return `<section class="side-panel">
|
|
243
|
+
<h3>${t("evidence")}</h3>
|
|
244
|
+
${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
|
|
245
|
+
</section>`;
|
|
246
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
function reviewQueue() {
|
|
2
|
+
ensureReviewQueueState();
|
|
3
|
+
const tabs = reviewQueueTabs();
|
|
4
|
+
const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
|
|
5
|
+
const baseTasks = reviewQueueBaseTasks(activeTab);
|
|
6
|
+
const reasonOptions = reviewReasonOptions(baseTasks);
|
|
7
|
+
normalizeReviewReasonFilter(reasonOptions);
|
|
8
|
+
const tasks = reviewFilteredTasks(baseTasks);
|
|
9
|
+
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
10
|
+
const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
|
|
11
|
+
const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
|
|
12
|
+
return `<div class="dashboard-grid review-queue-page">
|
|
13
|
+
<main class="dashboard-main stack">
|
|
14
|
+
<section class="flow-panel">
|
|
15
|
+
<div class="section-head">
|
|
16
|
+
<div>
|
|
17
|
+
<p class="eyebrow">${t("review")}</p>
|
|
18
|
+
<h2>${t("reviewQueue")}</h2>
|
|
19
|
+
<p class="subtle">${t("reviewQueueSubtitle")}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
|
|
24
|
+
${tabs.map((tab) => reviewQueueTab(tab)).join("")}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="review-queue-toolbar">
|
|
27
|
+
<div class="input-group">
|
|
28
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="select-group">
|
|
31
|
+
<label>${t("reasonFilter")}</label>
|
|
32
|
+
<select data-review-reason-filter aria-label="${t("reasonFilter")}">
|
|
33
|
+
<option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
|
|
34
|
+
${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
|
|
35
|
+
</select>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="select-group">
|
|
38
|
+
<label>${t("sortBy")}</label>
|
|
39
|
+
<select data-review-sort aria-label="${t("sortBy")}">
|
|
40
|
+
${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
|
|
41
|
+
</select>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
|
|
45
|
+
<div class="review-queue-list">
|
|
46
|
+
${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="review-queue-pager">
|
|
50
|
+
${pager("review", page, pageCount)}
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
</main>
|
|
54
|
+
<aside class="dashboard-sidebar stack">
|
|
55
|
+
<section class="side-panel review-queue-summary">
|
|
56
|
+
<h3>${t("reviewQueue")}</h3>
|
|
57
|
+
<div class="review-queue-stats">
|
|
58
|
+
${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
<section class="side-panel">
|
|
62
|
+
<h3>${escapeHtml(activeTab.label)}</h3>
|
|
63
|
+
<p>${escapeHtml(activeTab.description)}</p>
|
|
64
|
+
<dl class="review-queue-contract">
|
|
65
|
+
<div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
|
|
66
|
+
<div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
|
|
67
|
+
</dl>
|
|
68
|
+
</section>
|
|
69
|
+
</aside>
|
|
70
|
+
</div>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureReviewQueueState() {
|
|
74
|
+
if (!state.reviewQueueTab) state.reviewQueueTab = "review";
|
|
75
|
+
if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
|
|
76
|
+
if (!state.reviewSort) state.reviewSort = "queue";
|
|
77
|
+
if (!state.reviewQueuePage) state.reviewQueuePage = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reviewQueueTabs() {
|
|
81
|
+
return [
|
|
82
|
+
{ id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
|
|
83
|
+
{ id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
|
|
84
|
+
{ id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
|
|
85
|
+
{ id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
|
|
86
|
+
{ id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
|
|
87
|
+
{ id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reviewQueueTab(tab) {
|
|
92
|
+
const active = tab.id === state.reviewQueueTab;
|
|
93
|
+
const count = reviewQueueBaseTasks(tab).length;
|
|
94
|
+
return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
|
|
95
|
+
<span>${escapeHtml(tab.label)}</span>
|
|
96
|
+
<strong>${count}</strong>
|
|
97
|
+
</button>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reviewSortOptions() {
|
|
101
|
+
return [
|
|
102
|
+
{ id: "queue", label: t("sortQueuePriority") },
|
|
103
|
+
{ id: "newest", label: t("sortNewest") },
|
|
104
|
+
{ id: "oldest", label: t("sortOldest") },
|
|
105
|
+
{ id: "id", label: t("sortTaskId") },
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reviewQueueBaseTasks(tab) {
|
|
110
|
+
return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function taskMatchesReviewTab(task, tab) {
|
|
114
|
+
const queues = reviewTaskQueues(task);
|
|
115
|
+
return (tab.queues || []).some((queue) => queues.includes(queue));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reviewTaskQueues(task) {
|
|
119
|
+
return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function reviewReasonOptions(tasks) {
|
|
123
|
+
return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeReviewReasonFilter(reasonOptions) {
|
|
127
|
+
const current = state.reviewReasonFilter || "all";
|
|
128
|
+
if (current === "all") return;
|
|
129
|
+
if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function reviewFilteredTasks(tasks) {
|
|
133
|
+
const query = state.query.trim().toLowerCase();
|
|
134
|
+
const reasonFilter = state.reviewReasonFilter || "all";
|
|
135
|
+
return [...tasks]
|
|
136
|
+
.filter((task) => {
|
|
137
|
+
if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
|
|
138
|
+
if (!query) return true;
|
|
139
|
+
return [
|
|
140
|
+
task.id,
|
|
141
|
+
task.shortId,
|
|
142
|
+
task.title,
|
|
143
|
+
task.module,
|
|
144
|
+
task.inferredModule,
|
|
145
|
+
task.state,
|
|
146
|
+
task.lifecycleState,
|
|
147
|
+
task.reviewStatus,
|
|
148
|
+
task.closeoutStatus,
|
|
149
|
+
...(task.taskQueues || []),
|
|
150
|
+
...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
|
|
151
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
152
|
+
})
|
|
153
|
+
.sort(reviewTaskSort);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function reviewTaskSort(left, right) {
|
|
157
|
+
if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
|
|
158
|
+
if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
|
|
159
|
+
if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
160
|
+
return reviewPriorityRank(left) - reviewPriorityRank(right)
|
|
161
|
+
|| compareTasksByTimeForOrder(left, right, "desc")
|
|
162
|
+
|| stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compareTasksByTimeForOrder(left, right, order) {
|
|
166
|
+
const previous = state.taskSortOrder;
|
|
167
|
+
state.taskSortOrder = order;
|
|
168
|
+
const result = compareTasksByTime(left, right);
|
|
169
|
+
state.taskSortOrder = previous;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function reviewPriorityRank(task) {
|
|
174
|
+
const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
175
|
+
const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
|
|
176
|
+
const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
|
|
177
|
+
const queues = reviewTaskQueues(task);
|
|
178
|
+
const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
|
|
179
|
+
return Math.min(reasonRank, taskQueueRank);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function reviewTruthyCount(tasks, key) {
|
|
183
|
+
return tasks.filter((task) => task[key] === true).length;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function reviewQueueCard(task, tab) {
|
|
187
|
+
const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
|
|
188
|
+
const reasons = task.queueReasons || [];
|
|
189
|
+
const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
|
|
190
|
+
const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
|
|
191
|
+
const displayId = task.shortId || taskFolderName(task) || task.id;
|
|
192
|
+
return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
193
|
+
<div class="card-header">
|
|
194
|
+
<span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
|
|
195
|
+
${tag(task.reviewStatus || "missing")}
|
|
196
|
+
${reviewTaskQueues(task).map(tag).join("")}
|
|
197
|
+
</div>
|
|
198
|
+
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
199
|
+
<div class="card-meta">
|
|
200
|
+
<span>${tag(task.lifecycleState || "unknown")}</span>
|
|
201
|
+
<span>${tag(task.closeoutStatus || "missing")}</span>
|
|
202
|
+
<span>${openMaterial} ${t("openFindings")}</span>
|
|
203
|
+
<span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
|
|
204
|
+
<span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
|
|
207
|
+
${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
|
|
208
|
+
${lessonActions}
|
|
209
|
+
<div class="review-queue-actions">
|
|
210
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
211
|
+
<a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
|
|
212
|
+
<button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
213
|
+
${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
|
|
214
|
+
</div>
|
|
215
|
+
</article>`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
219
|
+
const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
|
|
220
|
+
if (!candidates.length) return "";
|
|
221
|
+
const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
|
|
222
|
+
const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
|
|
223
|
+
const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
|
|
224
|
+
return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
|
|
225
|
+
<div class="lesson-candidate-panel-head">
|
|
226
|
+
<div>
|
|
227
|
+
<p class="eyebrow">${t("lessonCandidates")}</p>
|
|
228
|
+
<h3>${t("lessonSedimentationActions")}</h3>
|
|
229
|
+
</div>
|
|
230
|
+
<span class="tag">${visibleCandidates.length}/${candidates.length}</span>
|
|
231
|
+
</div>
|
|
232
|
+
${staticNote}
|
|
233
|
+
<div class="lesson-candidate-actions">
|
|
234
|
+
${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
|
|
235
|
+
</div>
|
|
236
|
+
${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
|
|
237
|
+
</section>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function lessonCandidateAction(task, candidate) {
|
|
241
|
+
const followUp = String(candidate.followUpTask || "").trim();
|
|
242
|
+
const hasFollowUp = followUp && !/^pending$/i.test(followUp);
|
|
243
|
+
const prompt = lessonSedimentationPrompt(task, candidate);
|
|
244
|
+
return `<div class="lesson-candidate-action">
|
|
245
|
+
<div class="lesson-candidate-main">
|
|
246
|
+
<strong>${escapeHtml(candidate.id)}</strong>
|
|
247
|
+
<span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
|
|
248
|
+
<small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
|
|
249
|
+
</div>
|
|
250
|
+
<span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
|
|
251
|
+
<div class="lesson-candidate-command-row">
|
|
252
|
+
${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
|
|
253
|
+
<button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
|
|
254
|
+
<button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function lessonSedimentationPrompt(task, candidate) {
|
|
260
|
+
return [
|
|
261
|
+
"You are executing a lesson sedimentation follow-up task.",
|
|
262
|
+
"",
|
|
263
|
+
`Source task: ${task.id}`,
|
|
264
|
+
`Source candidate: ${candidate.id} - ${candidate.title || ""}`,
|
|
265
|
+
`Candidate scope: ${candidate.scope || "unspecified"}`,
|
|
266
|
+
`Candidate module key: ${candidate.moduleKey || "n/a"}`,
|
|
267
|
+
`Detail artifact: ${candidate.detailArtifact || "not provided"}`,
|
|
268
|
+
`Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
|
|
269
|
+
`Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
|
|
270
|
+
`Promotion target: ${candidate.promotionTarget || "unspecified"}`,
|
|
271
|
+
`Conflict check: ${candidate.conflictCheck || "pending"}`,
|
|
272
|
+
`Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
|
|
273
|
+
"",
|
|
274
|
+
"Instructions:",
|
|
275
|
+
"1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
|
|
276
|
+
"2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
|
|
277
|
+
"3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
|
|
278
|
+
"4. Check conflicts against existing lessons and standards.",
|
|
279
|
+
"5. Propose the smallest diff first.",
|
|
280
|
+
"6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
|
|
281
|
+
].join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function reviewReason(reason) {
|
|
285
|
+
return `<div class="review-reason">
|
|
286
|
+
<strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
|
|
287
|
+
<span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
|
|
288
|
+
</div>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function firstUsefulLine(text) {
|
|
292
|
+
return String(text || "")
|
|
293
|
+
.split(/\n+/)
|
|
294
|
+
.map((line) => line.trim())
|
|
295
|
+
.filter(Boolean)[0] || "";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function reviewWorkspace(route) {
|
|
299
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
|
|
300
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
301
|
+
const walkthroughDoc = taskDocument(task, "__walkthrough__");
|
|
302
|
+
const candidateDoc = taskDocument(task, "lesson_candidates.md");
|
|
303
|
+
const reviewDoc = taskDocument(task, "review.md");
|
|
304
|
+
const findingsDoc = taskDocument(task, "findings.md");
|
|
305
|
+
return `<main class="review-workspace">
|
|
306
|
+
<nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
307
|
+
<section class="detail-hero review-hero">
|
|
308
|
+
<div>
|
|
309
|
+
<p class="eyebrow">${t("reviewWorkspace")}</p>
|
|
310
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
311
|
+
<p>${escapeHtml(task.path)}</p>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="review-hero-tags">
|
|
314
|
+
${tag(task.lifecycleState || "unknown")}
|
|
315
|
+
${tag(task.reviewStatus || "missing")}
|
|
316
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
317
|
+
</div>
|
|
318
|
+
</section>
|
|
319
|
+
<section class="review-workspace-grid">
|
|
320
|
+
<article class="review-workspace-main stack">
|
|
321
|
+
${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
|
|
322
|
+
${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
|
|
323
|
+
${reviewDocPanel("review", reviewDoc, task.reviewPath)}
|
|
324
|
+
${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
|
|
325
|
+
</article>
|
|
326
|
+
<aside class="review-workspace-side stack">
|
|
327
|
+
${reviewActionPanel(task, { mode: "workspace" })}
|
|
328
|
+
${taskStateSummary(task)}
|
|
329
|
+
${openFindings(task)}
|
|
330
|
+
${evidenceList(task)}
|
|
331
|
+
</aside>
|
|
332
|
+
</section>
|
|
333
|
+
</main>`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function reviewDocPanel(key, doc, fallbackPath = "") {
|
|
337
|
+
return `<section class="doc-section review-doc-panel">
|
|
338
|
+
<div class="section-head">
|
|
339
|
+
<div>
|
|
340
|
+
<p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
|
|
341
|
+
<h2>${t(key)}</h2>
|
|
342
|
+
</div>
|
|
343
|
+
${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
|
|
344
|
+
</div>
|
|
345
|
+
<div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
|
|
346
|
+
</section>`;
|
|
347
|
+
}
|