coding-agent-harness 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -5
- package/README.md +12 -2
- package/README.zh-CN.md +10 -2
- package/SKILL.md +14 -3
- package/dist/build-dist.mjs +32 -6
- package/dist/check-dist-observation.mjs +73 -28
- package/dist/check-harness.mjs +0 -1
- package/dist/check-import-graph.mjs +44 -27
- package/dist/check-lite-forbidden-surfaces.mjs +121 -0
- package/dist/check-no-ts-nocheck.mjs +88 -0
- package/dist/check-runtime-emit.mjs +10 -3
- package/dist/check-type-boundaries.mjs +67 -8
- package/dist/commands/dashboard-command.mjs +52 -14
- package/dist/commands/migration-command.mjs +18 -8
- package/dist/commands/module-command.mjs +142 -0
- package/dist/commands/preset-command.mjs +65 -4
- package/dist/commands/registry.mjs +483 -0
- package/dist/commands/task-command.mjs +111 -53
- package/dist/harness.mjs +6 -303
- package/dist/lib/capability-registry.mjs +229 -53
- package/dist/lib/check-module-parallel.mjs +1 -6
- package/dist/lib/check-profiles.mjs +39 -46
- package/dist/lib/check-task-contracts.mjs +6 -4
- package/dist/lib/command-registry.mjs +248 -0
- package/dist/lib/core-shared.mjs +78 -3
- package/dist/lib/dashboard-data.mjs +203 -22
- package/dist/lib/dashboard-workbench.mjs +245 -21
- package/dist/lib/dashboard-writer.mjs +4 -1
- package/dist/lib/git-status-summary.mjs +0 -1
- package/dist/lib/governance-index-generator.mjs +7 -5
- package/dist/lib/governance-sync.mjs +46 -121
- package/dist/lib/governance-table-boundary.mjs +1 -14
- package/dist/lib/harness-core.mjs +5 -1
- package/dist/lib/harness-paths.mjs +115 -1
- package/dist/lib/impact-classifier.mjs +420 -0
- package/dist/lib/lesson-maintenance.mjs +1 -2
- package/dist/lib/markdown-utils.mjs +50 -1
- package/dist/lib/migration-planner.mjs +31 -16
- package/dist/lib/migration-support.mjs +5 -4
- package/dist/lib/module-registry.mjs +296 -0
- package/dist/lib/preset-audit-contracts.mjs +24 -1
- package/dist/lib/preset-engine.mjs +68 -29
- package/dist/lib/preset-registry.mjs +374 -72
- package/dist/lib/preset-runner.mjs +560 -0
- package/dist/lib/review-confirm-git-gate.mjs +73 -19
- package/dist/lib/status-builder.mjs +23 -8
- package/dist/lib/structure-migration.mjs +6 -4
- package/dist/lib/subagent-authorization-audit.mjs +8 -2
- package/dist/lib/task-archive-eligibility.mjs +65 -0
- package/dist/lib/task-audit-metadata.mjs +25 -11
- package/dist/lib/task-audit-migration.mjs +21 -14
- package/dist/lib/task-discovery-contract.mjs +32 -0
- package/dist/lib/task-index.mjs +4 -2
- package/dist/lib/task-lesson-candidates.mjs +1 -2
- package/dist/lib/task-lesson-sedimentation.mjs +310 -9
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
- package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
- package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
- package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
- package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
- package/dist/lib/task-lifecycle/template-files.mjs +2 -5
- package/dist/lib/task-lifecycle.mjs +117 -159
- package/dist/lib/task-metadata.mjs +10 -5
- package/dist/lib/task-preset-contract-drift.mjs +45 -0
- package/dist/lib/task-repository.mjs +192 -0
- package/dist/lib/task-review-model.mjs +38 -17
- package/dist/lib/task-scanner.mjs +75 -23
- package/dist/lib/task-template-materials.mjs +131 -0
- package/dist/lib/task-tombstone-commands.mjs +187 -18
- package/dist/lib/types/check-profiles.js +1 -0
- package/dist/lib/types/impact.js +1 -0
- package/dist/lib/types/preset.js +1 -0
- package/dist/lib/types/task-lifecycle.js +1 -0
- package/dist/lib/types/task-scanner.js +1 -0
- package/dist/postinstall.mjs +2 -2
- package/dist/run-built-tests.mjs +10 -3
- package/docs-release/README.md +2 -1
- package/docs-release/architecture/document-contract-kernel/README.md +150 -0
- package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
- package/docs-release/architecture/overview.md +2 -2
- package/docs-release/architecture/overview.zh-CN.md +2 -2
- package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
- package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
- package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/README.md +1 -1
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
- package/docs-release/guides/agent-installation.en-US.md +4 -6
- package/docs-release/guides/agent-installation.md +11 -8
- package/docs-release/guides/contributing.md +10 -3
- package/docs-release/guides/contributing.zh-CN.md +10 -3
- package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
- package/docs-release/guides/migration-playbook.en-US.md +9 -6
- package/docs-release/guides/migration-playbook.md +9 -6
- package/docs-release/guides/preset-development.md +68 -2
- package/docs-release/guides/task-state-machine.en-US.md +8 -8
- package/docs-release/guides/task-state-machine.md +7 -7
- package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
- package/package.json +19 -11
- package/postinstall.mjs +37 -0
- package/presets/legacy-migration/preset.yaml +5 -5
- package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
- package/presets/lesson-sedimentation/preset.yaml +3 -3
- package/presets/module/preset.yaml +2 -2
- package/presets/module/templates/execution_strategy.append.md +1 -1
- package/presets/module/templates/task_plan.append.md +3 -3
- package/presets/release-closeout/checks/check-release-package.mjs +29 -0
- package/presets/release-closeout/preset.yaml +100 -0
- package/presets/release-closeout/scripts/generate-release-package.mjs +572 -0
- package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
- package/presets/release-closeout/templates/findings.seed.md +5 -0
- package/presets/release-closeout/templates/review.seed.md +3 -0
- package/presets/release-closeout/templates/task_plan.append.md +24 -0
- package/presets/standard-task/preset.yaml +2 -2
- package/references/agents-md-pattern.md +23 -17
- package/references/lessons-governance.md +2 -2
- package/references/module-parallel-standard.md +3 -6
- package/references/pull-request-standard.md +2 -2
- package/references/ssot-governance.md +2 -2
- package/references/taskr-gap-analysis.md +3 -3
- package/run-dist.mjs +34 -0
- package/skills/preset-creator/SKILL.md +40 -8
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
- package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
- package/skills/preset-creator/references/structure-aware-paths.md +112 -0
- package/templates/AGENTS.md.template +28 -26
- package/templates/architecture/README.md +2 -2
- package/templates/architecture/service-catalog.md +2 -2
- package/templates/architecture/services/service-template.md +1 -1
- package/templates/dashboard/assets/app-src/00-state.js +5 -1
- package/templates/dashboard/assets/app-src/10-router.js +7 -0
- package/templates/dashboard/assets/app-src/20-overview.js +8 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
- package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
- package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
- package/templates/dashboard/assets/app-src/40-modules.js +257 -41
- package/templates/dashboard/assets/app-src/45-review.js +127 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
- package/templates/dashboard/assets/app.css +928 -53
- package/templates/dashboard/assets/app.css.manifest.json +2 -0
- package/templates/dashboard/assets/app.js +1071 -98
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
- package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
- package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
- package/templates/dashboard/assets/css-src/31-archive.css +94 -0
- package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
- package/templates/dashboard/assets/i18n.js +166 -2
- package/templates/development/README.md +9 -9
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +1 -1
- package/templates/development/external-source-packs/README.md +2 -2
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +1 -1
- package/templates/integrations/event-contract.md +1 -1
- package/templates/integrations/third-party/vendor-template.md +1 -1
- package/templates/integrations/webhook-contract.md +1 -1
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/modules/module_brief.md +50 -0
- package/templates/modules/module_plan.md +49 -0
- package/templates/modules/registry_view.md +9 -0
- package/templates/modules/session_prompt_pack.md +55 -0
- package/templates/planning/brief.md +32 -8
- package/templates/planning/module_brief.md +28 -3
- package/templates/planning/module_plan.md +26 -11
- package/templates/planning/module_session_prompt.md +11 -2
- package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
- package/templates/planning/review.md +1 -1
- package/templates/planning/visual_map.md +1 -1
- package/templates/reference/docs-library-standard.md +7 -7
- package/templates/reference/execution-workflow-standard.md +13 -0
- package/templates/reference/external-source-intake-standard.md +10 -10
- package/templates/reference/pull-request-standard.md +2 -2
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/reference/review-routing-standard.md +4 -0
- package/templates/ssot/Module-Registry.md +4 -38
- package/templates/walkthrough/walkthrough-template.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +27 -25
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +2 -2
- package/templates-zh-CN/architecture/service-catalog.md +2 -2
- package/templates-zh-CN/architecture/services/service-template.md +1 -1
- package/templates-zh-CN/development/README.md +9 -9
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +1 -1
- package/templates-zh-CN/development/external-source-packs/README.md +2 -2
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +1 -1
- package/templates-zh-CN/integrations/event-contract.md +1 -1
- package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
- package/templates-zh-CN/integrations/webhook-contract.md +1 -1
- package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
- package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
- package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
- package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
- package/templates-zh-CN/modules/module_brief.md +47 -0
- package/templates-zh-CN/modules/module_plan.md +48 -0
- package/templates-zh-CN/modules/registry_view.md +9 -0
- package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
- package/templates-zh-CN/planning/INDEX.md +1 -0
- package/templates-zh-CN/planning/brief.md +26 -7
- package/templates-zh-CN/planning/module_brief.md +24 -2
- package/templates-zh-CN/planning/module_plan.md +35 -29
- package/templates-zh-CN/planning/module_session_prompt.md +15 -11
- package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
- package/templates-zh-CN/planning/review.md +1 -1
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +27 -27
- package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
- package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
- package/templates-zh-CN/reference/pull-request-standard.md +1 -1
- package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
- package/templates-zh-CN/reference/review-routing-standard.md +3 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
- package/templates-zh-CN/reference/worktree-standard.md +1 -1
- package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
- package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
- package/templates-zh-CN/ssot/Module-Registry.md +5 -44
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
|
@@ -1,58 +1,274 @@
|
|
|
1
|
+
function dashboardModules() {
|
|
2
|
+
const structured = Array.isArray(bundle.modules) ? bundle.modules : [];
|
|
3
|
+
if (structured.length > 0) return structured;
|
|
4
|
+
const graphModules = (bundle.graph?.nodes || []).filter((node) => node.type === "module").map((node) => ({
|
|
5
|
+
key: String(node.id || "").replace(/^module:/, ""),
|
|
6
|
+
title: node.label,
|
|
7
|
+
status: node.state,
|
|
8
|
+
currentStep: node.currentStep,
|
|
9
|
+
source: "graph",
|
|
10
|
+
briefPath: node.briefPath,
|
|
11
|
+
modulePlanPath: node.modulePlanPath,
|
|
12
|
+
})).filter((module) => module.key);
|
|
13
|
+
return graphModules;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function moduleDefinition(key) {
|
|
17
|
+
return dashboardModules().find((module) => module.key === key) || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function taskModuleLabel(task) {
|
|
21
|
+
const key = taskModuleKey(task);
|
|
22
|
+
if (key === "base" || key === "legacy-unclassified") return taskModuleDisplayLabel(key);
|
|
23
|
+
return moduleDefinition(key)?.title || key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function taskGroupContext(group, tasks) {
|
|
27
|
+
if (group.startsWith("module:")) {
|
|
28
|
+
const key = group.slice("module:".length);
|
|
29
|
+
const counts = moduleCountsForTasks(tasks);
|
|
30
|
+
if (key === "base") {
|
|
31
|
+
return {
|
|
32
|
+
eyebrow: t("baseModuleEyebrow"),
|
|
33
|
+
title: t("baseModule"),
|
|
34
|
+
summary: `${tasks.length} ${t("tasks")} · ${counts.active} ${t("active")} · ${counts.review} ${t("statReview")} · ${counts.blocked} ${t("statBlocked")}`,
|
|
35
|
+
chips: [`${tasks.length} ${t("tasks")}`, `${counts.risk} ${t("moduleRisks")}`],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (key === "legacy-unclassified") {
|
|
39
|
+
return {
|
|
40
|
+
eyebrow: t("unclassifiedWarning"),
|
|
41
|
+
title: t("unclassifiedModule"),
|
|
42
|
+
summary: t("unclassifiedSummary").replace("{count}", String(tasks.length)),
|
|
43
|
+
chips: [`${tasks.length} ${t("tasks")}`, `${counts.risk} ${t("moduleRisks")}`],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const module = moduleDefinition(key) || { key, title: key, source: "inferred" };
|
|
47
|
+
const chips = [
|
|
48
|
+
module.status ? `${t("columnState")}: ${label(module.status)}` : "",
|
|
49
|
+
module.owner ? `${t("moduleOwner")}: ${module.owner}` : "",
|
|
50
|
+
module.currentStep ? `${t("moduleCurrentStep")}: ${module.currentStep}` : "",
|
|
51
|
+
module.dependsOn?.length ? `${t("moduleDependsOn")}: ${module.dependsOn.join(", ")}` : "",
|
|
52
|
+
module.scope?.length ? `${t("moduleScope")}: ${module.scope.join(", ")}` : "",
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
return {
|
|
55
|
+
eyebrow: module.source === "registry" ? t("registeredModule") : t("inferredModule"),
|
|
56
|
+
title: module.title || key,
|
|
57
|
+
summary: `${tasks.length} ${t("tasks")} · ${counts.active} ${t("active")} · ${counts.review} ${t("statReview")} · ${counts.blocked} ${t("statBlocked")}`,
|
|
58
|
+
chips,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
eyebrow: t("groupBy"),
|
|
63
|
+
title: taskGroupLabel(group),
|
|
64
|
+
summary: `${tasks.length} ${t("tasks")}`,
|
|
65
|
+
chips: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function moduleCountsForTasks(tasks) {
|
|
70
|
+
return {
|
|
71
|
+
active: tasks.filter((task) => ["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state)).length,
|
|
72
|
+
review: tasks.filter((task) => task.state === "review").length,
|
|
73
|
+
blocked: tasks.filter((task) => task.state === "blocked").length,
|
|
74
|
+
risk: tasks.filter(uiDashboardTaskHasRisk).length,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
1
78
|
function modulesView(moduleId = "") {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
79
|
+
const modules = modulesWithTaskFallback();
|
|
80
|
+
const selectedKey = moduleId || state.selectedModuleKey || modules[0]?.key || "";
|
|
81
|
+
state.selectedModuleKey = selectedKey;
|
|
82
|
+
const selected = modules.find((module) => module.key === selectedKey) || modules[0] || null;
|
|
83
|
+
const unclassified = normalCycleTasks().filter((task) => taskModuleKey(task) === "legacy-unclassified");
|
|
84
|
+
return `<main class="stack module-console">
|
|
85
|
+
${moduleRunStrip(modules, unclassified)}
|
|
86
|
+
<section class="module-console-grid">
|
|
87
|
+
<nav class="module-list-panel" aria-label="${escapeAttr(t("moduleView"))}">
|
|
88
|
+
${modules.map((module) => moduleListItem(module, selected?.key === module.key)).join("") || emptyState(t("noModules"))}
|
|
89
|
+
</nav>
|
|
90
|
+
<article class="module-detail-panel">
|
|
91
|
+
${selected ? moduleDetail(selected) : emptyState(t("noModules"))}
|
|
92
|
+
</article>
|
|
93
|
+
</section>
|
|
94
|
+
${unclassified.length ? moduleUnclassifiedPanel(unclassified) : ""}
|
|
95
|
+
</main>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function modulesWithTaskFallback() {
|
|
99
|
+
const moduleMap = new Map(dashboardModules().map((module) => [module.key, {
|
|
100
|
+
...module,
|
|
101
|
+
counts: emptyUiModuleCounts(),
|
|
102
|
+
tasks: [],
|
|
103
|
+
}]));
|
|
104
|
+
for (const task of normalCycleTasks()) {
|
|
6
105
|
const key = taskModuleKey(task);
|
|
7
|
-
if (
|
|
106
|
+
if (key === "legacy-unclassified") continue;
|
|
107
|
+
if (!moduleMap.has(key)) {
|
|
108
|
+
moduleMap.set(key, {
|
|
109
|
+
key,
|
|
110
|
+
title: taskModuleDisplayLabel(key),
|
|
111
|
+
source: key === "base" ? "structure" : "inferred",
|
|
112
|
+
status: task.classificationSource || "inferred",
|
|
113
|
+
counts: emptyUiModuleCounts(),
|
|
114
|
+
tasks: [],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const module = moduleMap.get(key);
|
|
118
|
+
accumulateUiModuleTask(module, task);
|
|
119
|
+
}
|
|
120
|
+
return [...moduleMap.values()].sort((left, right) => {
|
|
121
|
+
const leftActive = Number(left.counts?.active || 0);
|
|
122
|
+
const rightActive = Number(right.counts?.active || 0);
|
|
123
|
+
if (leftActive !== rightActive) return rightActive - leftActive;
|
|
124
|
+
return left.key.localeCompare(right.key);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function emptyUiModuleCounts() {
|
|
129
|
+
return { total: 0, active: 0, review: 0, blocked: 0, risk: 0, missingDocs: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function accumulateUiModuleTask(module, task) {
|
|
133
|
+
if (!module || !task) return;
|
|
134
|
+
const stateValue = String(task.state || "unknown");
|
|
135
|
+
if (!module.tasks.some((item) => item.id === task.id)) module.tasks.push(task);
|
|
136
|
+
module.counts.total = (module.counts.total || 0) + 1;
|
|
137
|
+
if (["in_progress", "review", "blocked", "planned", "not_started"].includes(stateValue)) {
|
|
138
|
+
module.counts.active = (module.counts.active || 0) + 1;
|
|
139
|
+
}
|
|
140
|
+
if (stateValue !== "active") module.counts[stateValue] = (module.counts[stateValue] || 0) + 1;
|
|
141
|
+
if (uiDashboardTaskHasRisk(task)) {
|
|
142
|
+
module.counts.risk = (module.counts.risk || 0) + 1;
|
|
143
|
+
}
|
|
144
|
+
if (task.briefSource && task.briefSource !== "standalone") {
|
|
145
|
+
module.counts.missingDocs = (module.counts.missingDocs || 0) + 1;
|
|
8
146
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function uiDashboardTaskHasRisk(task) {
|
|
150
|
+
if (task.state === "blocked") return true;
|
|
151
|
+
if (String(task.reviewStatus || "").includes("blocked")) return true;
|
|
152
|
+
if (Array.isArray(task.materialIssues) && task.materialIssues.length > 0) return true;
|
|
153
|
+
if (Array.isArray(task.queueReasons) && task.queueReasons.length > 0) return true;
|
|
154
|
+
if (String(task.visualMapStatus || "") === "missing") return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function moduleRunStrip(modules, unclassified) {
|
|
159
|
+
const active = modules.filter((module) => Number(module.counts?.active || 0) > 0).length;
|
|
160
|
+
const risk = modules.reduce((sum, module) => sum + Number(module.counts?.risk || 0), 0);
|
|
161
|
+
const registered = modules.filter((module) => module.source === "registry").length;
|
|
162
|
+
return `<section class="module-run-strip">
|
|
163
|
+
${metric(t("moduleRegistered"), registered)}
|
|
164
|
+
${metric(t("moduleActive"), active)}
|
|
165
|
+
${metric(t("moduleRisks"), risk)}
|
|
166
|
+
${metric(t("moduleUnclassified"), unclassified.length)}
|
|
167
|
+
</section>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function moduleListItem(module, active) {
|
|
171
|
+
const counts = module.counts || emptyUiModuleCounts();
|
|
172
|
+
return `<a class="module-list-item ${active ? "active" : ""}" href="#/modules/${encodeURIComponent(module.key)}" data-module-select="${escapeAttr(module.key)}">
|
|
173
|
+
<span>
|
|
174
|
+
<strong>${escapeHtml(module.key === "base" ? t("baseModule") : module.title || module.key)}</strong>
|
|
175
|
+
<small>${escapeHtml(module.key)} · ${escapeHtml(module.source || "registry")}</small>
|
|
176
|
+
</span>
|
|
177
|
+
<span class="module-list-counts">
|
|
178
|
+
<b>${Number(counts.active || 0)}</b>
|
|
179
|
+
${tag(module.status || "planned")}
|
|
180
|
+
</span>
|
|
181
|
+
</a>`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function moduleDetail(module) {
|
|
185
|
+
const tasks = normalCycleTasks().filter((task) => taskModuleKey(task) === module.key);
|
|
186
|
+
const activeTasks = tasks.filter((task) => ["in_progress", "review", "blocked", "planned", "not_started"].includes(task.state));
|
|
187
|
+
const riskTasks = tasks.filter((task) => task.state === "blocked" || String(task.reviewStatus || "").includes("blocked") || String(task.visualMapStatus || "") === "missing");
|
|
188
|
+
const brief = findDocument(module.briefPath || `TARGET:coding-agent-harness/planning/modules/${module.key}/brief.md`);
|
|
189
|
+
const plan = findDocument(module.modulePlanPath || "");
|
|
190
|
+
return `<div class="module-detail-stack">
|
|
191
|
+
<header class="module-detail-header">
|
|
192
|
+
<div>
|
|
193
|
+
<p class="eyebrow">${escapeHtml(module.key === "base" ? t("baseModuleEyebrow") : module.source === "registry" ? t("registeredModule") : t("inferredModule"))}</p>
|
|
194
|
+
<h2>${escapeHtml(module.key === "base" ? t("baseModule") : module.title || module.key)}</h2>
|
|
195
|
+
<p class="subtle">${escapeHtml(module.key)}${module.currentStep ? ` · ${escapeHtml(module.currentStep)}` : ""}</p>
|
|
196
|
+
</div>
|
|
197
|
+
${tag(module.status || "planned")}
|
|
198
|
+
</header>
|
|
199
|
+
<div class="module-chip-row">
|
|
200
|
+
${module.owner ? `<span class="module-chip">${t("moduleOwner")}: ${escapeHtml(module.owner)}</span>` : ""}
|
|
201
|
+
${module.branch ? `<span class="module-chip">${t("moduleBranch")}: ${escapeHtml(module.branch)}</span>` : ""}
|
|
202
|
+
${module.dependsOn?.length ? `<span class="module-chip">${t("moduleDependsOn")}: ${escapeHtml(module.dependsOn.join(", "))}</span>` : ""}
|
|
203
|
+
</div>
|
|
204
|
+
<section class="module-boundary-grid">
|
|
205
|
+
${moduleBoundaryBlock(t("moduleScope"), module.scope)}
|
|
206
|
+
${moduleBoundaryBlock(t("moduleShared"), module.shared)}
|
|
207
|
+
${moduleBoundaryBlock(t("moduleDependsOn"), module.dependsOn)}
|
|
13
208
|
</section>
|
|
14
|
-
|
|
209
|
+
<section class="module-work-panel">
|
|
210
|
+
<div class="section-head">
|
|
211
|
+
<div>
|
|
212
|
+
<h3>${t("moduleCurrentWork")}</h3>
|
|
213
|
+
<p class="subtle">${activeTasks.length} ${t("active")} · ${riskTasks.length} ${t("moduleRisks")}</p>
|
|
214
|
+
</div>
|
|
215
|
+
<a href="#/tasks">${t("openTaskIndex")}</a>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="module-task-list">
|
|
218
|
+
${activeTasks.slice(0, 10).map(moduleTaskRow).join("") || `<p>${t("noModuleTasks")}</p>`}
|
|
219
|
+
</div>
|
|
220
|
+
</section>
|
|
221
|
+
${riskTasks.length ? `<section class="module-risk-panel">
|
|
222
|
+
<h3>${t("moduleRiskPanel")}</h3>
|
|
223
|
+
<div class="module-task-list">${riskTasks.slice(0, 8).map(moduleTaskRow).join("")}</div>
|
|
224
|
+
</section>` : ""}
|
|
225
|
+
<section class="module-doc-panel">
|
|
226
|
+
<h3>${t("sourceDocuments")}</h3>
|
|
227
|
+
<div class="module-doc-links">
|
|
228
|
+
${moduleDocLink(t("brief"), module.briefPath, brief)}
|
|
229
|
+
${moduleDocLink(t("taskPlan"), module.modulePlanPath, plan)}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="markdown module-doc-preview">${brief ? window.HarnessMarkdown.render(brief.content, "rendered") : `<p>${t("moduleBriefMissing")}</p>`}</div>
|
|
232
|
+
</section>
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function moduleBoundaryBlock(title, values) {
|
|
237
|
+
const items = Array.isArray(values) && values.length ? values : [t("none")];
|
|
238
|
+
return `<div class="module-boundary-block">
|
|
239
|
+
<strong>${escapeHtml(title)}</strong>
|
|
240
|
+
${items.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}
|
|
241
|
+
</div>`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function moduleDocLink(labelText, pathValue, document) {
|
|
245
|
+
if (!pathValue && !document) return `<span class="module-doc-link missing">${escapeHtml(labelText)} · ${t("documentMissing")}</span>`;
|
|
246
|
+
return `<span class="module-doc-link">${escapeHtml(labelText)} · ${escapeHtml(pathValue || document.path || t("ready"))}</span>`;
|
|
15
247
|
}
|
|
16
248
|
|
|
17
249
|
function moduleTaskRow(task) {
|
|
18
250
|
const dotClass = /fail|blocked|open/i.test(task.state) ? "state-fail" : /warn|advice|planned|missing|unknown/i.test(task.state) ? "state-warn" : "state-pass";
|
|
251
|
+
const lifecycle = [task.lifecycleState, task.reviewStatus, task.closeoutStatus].filter(Boolean).map((item) => label(item)).join(" · ");
|
|
19
252
|
return `<a class="module-task-row" href="#/tasks/${encodeURIComponent(task.id)}" data-open-drawer="${escapeAttr(task.id)}">
|
|
20
253
|
<div class="module-task-left">
|
|
21
254
|
<i class="module-task-dot ${dotClass}" title="${escapeAttr(task.state)}"></i>
|
|
22
|
-
<span class="module-task-title">${escapeHtml(task.title)}</span>
|
|
255
|
+
<span class="module-task-title">${escapeHtml(task.title || task.id)}</span>
|
|
256
|
+
${lifecycle ? `<small>${escapeHtml(lifecycle)}</small>` : ""}
|
|
23
257
|
</div>
|
|
24
|
-
<span class="module-task-pct">${task.completion}%</span>
|
|
258
|
+
<span class="module-task-pct">${clampCompletion(task.completion)}%</span>
|
|
25
259
|
</a>`;
|
|
26
260
|
}
|
|
27
261
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const brief = findDocument(module.briefPath || `TARGET:coding-agent-harness/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>`}
|
|
262
|
+
function moduleUnclassifiedPanel(tasks) {
|
|
263
|
+
return `<section class="module-unclassified-panel">
|
|
264
|
+
<div class="section-head">
|
|
265
|
+
<div>
|
|
266
|
+
<p class="eyebrow">${t("unclassifiedWarning")}</p>
|
|
267
|
+
<h2>${t("unclassifiedModule")}</h2>
|
|
268
|
+
<p class="subtle">${t("unclassifiedSummary").replace("{count}", String(tasks.length))}</p>
|
|
269
|
+
</div>
|
|
270
|
+
<a href="#/tasks">${t("openTaskIndex")}</a>
|
|
55
271
|
</div>
|
|
56
|
-
|
|
57
|
-
</
|
|
272
|
+
<div class="module-task-list">${tasks.slice(0, 12).map(moduleTaskRow).join("")}</div>
|
|
273
|
+
</section>`;
|
|
58
274
|
}
|
|
@@ -6,6 +6,10 @@ function reviewQueue() {
|
|
|
6
6
|
const reasonOptions = reviewReasonOptions(baseTasks);
|
|
7
7
|
normalizeReviewReasonFilter(reasonOptions);
|
|
8
8
|
const tasks = reviewFilteredTasks(baseTasks);
|
|
9
|
+
const confirmableTasks = activeTab.id === "review" ? tasks.filter(taskCanBeHumanConfirmed) : [];
|
|
10
|
+
syncReviewBulkSelection(confirmableTasks);
|
|
11
|
+
if (activeTab.id === "lessons") syncLessonBulkSelection(lessonBulkActionableSelections());
|
|
12
|
+
else syncLessonBulkSelection([]);
|
|
9
13
|
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
10
14
|
const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
|
|
11
15
|
const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
|
|
@@ -41,6 +45,8 @@ function reviewQueue() {
|
|
|
41
45
|
</select>
|
|
42
46
|
</div>
|
|
43
47
|
</div>
|
|
48
|
+
${activeTab.id === "review" ? reviewBulkBar(confirmableTasks) : ""}
|
|
49
|
+
${activeTab.id === "lessons" ? lessonBulkBar() : ""}
|
|
44
50
|
<div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
|
|
45
51
|
<div class="review-queue-list">
|
|
46
52
|
${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
|
|
@@ -107,7 +113,7 @@ function reviewSortOptions() {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
function reviewQueueBaseTasks(tab) {
|
|
110
|
-
return (
|
|
116
|
+
return normalCycleTasks().filter((task) => taskMatchesReviewTab(task, tab));
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
function taskMatchesReviewTab(task, tab) {
|
|
@@ -183,17 +189,57 @@ function reviewTruthyCount(tasks, key) {
|
|
|
183
189
|
return tasks.filter((task) => task[key] === true).length;
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
function reviewBulkSelectedIds() {
|
|
193
|
+
return Object.entries(state.reviewBulkSelection || {})
|
|
194
|
+
.filter(([, selected]) => selected === true)
|
|
195
|
+
.map(([taskId]) => taskId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function syncReviewBulkSelection(confirmableTasks) {
|
|
199
|
+
const allowed = new Set(confirmableTasks.map((task) => task.id));
|
|
200
|
+
for (const taskId of Object.keys(state.reviewBulkSelection || {})) {
|
|
201
|
+
if (!allowed.has(taskId)) delete state.reviewBulkSelection[taskId];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function reviewBulkBar(confirmableTasks) {
|
|
206
|
+
const selectedCount = reviewBulkSelectedIds().length;
|
|
207
|
+
const allSelected = confirmableTasks.length > 0 && confirmableTasks.every((task) => state.reviewBulkSelection?.[task.id] === true);
|
|
208
|
+
const disabled = selectedCount === 0 || !canUseWorkbenchAction("review-complete-bulk");
|
|
209
|
+
const result = state.reviewBulkResult ? `<span class="bulk-action-result ${state.reviewBulkResult.ok ? "success" : "failed"}">${escapeHtml(state.reviewBulkResult.message)}</span>` : "";
|
|
210
|
+
return `<div class="bulk-action-bar review-bulk-bar">
|
|
211
|
+
<label class="bulk-select-all">
|
|
212
|
+
<input type="checkbox" data-review-bulk-select-all ${allSelected ? "checked" : ""} ${confirmableTasks.length ? "" : "disabled"} aria-label="${escapeAttr(t("selectAllReviewTasks"))}">
|
|
213
|
+
<span>${t("selectAllReviewTasks")}</span>
|
|
214
|
+
</label>
|
|
215
|
+
<span class="bulk-selected-count">${formatMessage("reviewBulkSelected", { count: selectedCount })}</span>
|
|
216
|
+
<button type="button" data-review-bulk-confirm ${disabled ? "disabled" : ""}>${t("reviewBulkConfirm")}</button>
|
|
217
|
+
<button type="button" data-review-bulk-clear ${selectedCount ? "" : "disabled"}>${t("clearSelection")}</button>
|
|
218
|
+
${result}
|
|
219
|
+
</div>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
186
222
|
function reviewQueueCard(task, tab) {
|
|
187
223
|
const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
|
|
188
224
|
const reasons = task.queueReasons || [];
|
|
189
225
|
const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
|
|
190
226
|
const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
|
|
227
|
+
const closeoutAction = taskReadyForCloseout(task)
|
|
228
|
+
? `<button data-task-complete="${escapeAttr(task.id)}" ${canUseWorkbenchAction("task-complete") ? "" : "disabled"}>${t("completeTaskCloseout")}</button><span class="inline-result" data-task-complete-result="${escapeAttr(task.id)}"></span>`
|
|
229
|
+
: "";
|
|
191
230
|
const displayId = task.shortId || taskFolderName(task) || task.id;
|
|
231
|
+
const canBulkConfirm = tab?.id === "review" && taskCanBeHumanConfirmed(task);
|
|
232
|
+
const bulkSelected = state.reviewBulkSelection?.[task.id] === true;
|
|
233
|
+
const bulkControl = tab?.id === "review" ? `<label class="bulk-card-check">
|
|
234
|
+
<input type="checkbox" data-review-bulk-select="${escapeAttr(task.id)}" ${canBulkConfirm ? "" : "disabled"} ${bulkSelected ? "checked" : ""} aria-label="${escapeAttr(t("selectReviewTask"))}">
|
|
235
|
+
<span>${t("select")}</span>
|
|
236
|
+
</label>` : "";
|
|
192
237
|
return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
193
238
|
<div class="card-header">
|
|
194
239
|
<span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
|
|
195
240
|
${tag(task.reviewStatus || "missing")}
|
|
196
241
|
${reviewTaskQueues(task).map(tag).join("")}
|
|
242
|
+
${bulkControl}
|
|
197
243
|
</div>
|
|
198
244
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
199
245
|
<div class="card-meta">
|
|
@@ -204,23 +250,39 @@ function reviewQueueCard(task, tab) {
|
|
|
204
250
|
<span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
|
|
205
251
|
</div>
|
|
206
252
|
<p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
|
|
253
|
+
${tombstoneSummary(task)}
|
|
207
254
|
${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
|
|
208
255
|
${lessonActions}
|
|
209
256
|
<div class="review-queue-actions">
|
|
210
257
|
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
211
258
|
<a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
|
|
212
259
|
<button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
260
|
+
${closeoutAction}
|
|
213
261
|
${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
|
|
214
262
|
</div>
|
|
215
263
|
</article>`;
|
|
216
264
|
}
|
|
217
265
|
|
|
266
|
+
function tombstoneSummary(task) {
|
|
267
|
+
const deletionState = String(task?.deletionState || "active");
|
|
268
|
+
if (deletionState === "active") return "";
|
|
269
|
+
const reason = String(task?.deleteReason || "").trim();
|
|
270
|
+
const supersededBy = String(task?.supersededBy || "").trim();
|
|
271
|
+
return `<div class="review-tombstone-summary">
|
|
272
|
+
<span>${tag(deletionState)}</span>
|
|
273
|
+
${reason ? `<span>${t("reason")}: ${escapeHtml(reason)}</span>` : ""}
|
|
274
|
+
${supersededBy ? `<a href="#/tasks/${encodeURIComponent(supersededBy)}">${escapeHtml(supersededBy)}</a>` : ""}
|
|
275
|
+
</div>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
218
278
|
function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
219
279
|
const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
|
|
220
280
|
if (!candidates.length) return "";
|
|
221
281
|
const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
|
|
222
282
|
const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
|
|
223
283
|
const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
|
|
284
|
+
syncLessonBulkSelection(lessonBulkActionableSelections());
|
|
285
|
+
const bulkBar = context === "card" ? "" : lessonBulkBar();
|
|
224
286
|
return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
|
|
225
287
|
<div class="lesson-candidate-panel-head">
|
|
226
288
|
<div>
|
|
@@ -230,6 +292,7 @@ function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
|
230
292
|
<span class="tag">${visibleCandidates.length}/${candidates.length}</span>
|
|
231
293
|
</div>
|
|
232
294
|
${staticNote}
|
|
295
|
+
${bulkBar}
|
|
233
296
|
<div class="lesson-candidate-actions">
|
|
234
297
|
${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
|
|
235
298
|
</div>
|
|
@@ -241,6 +304,9 @@ function lessonCandidateAction(task, candidate) {
|
|
|
241
304
|
const followUp = String(candidate.followUpTask || "").trim();
|
|
242
305
|
const hasFollowUp = followUp && !/^pending$/i.test(followUp);
|
|
243
306
|
const prompt = lessonSedimentationPrompt(task, candidate);
|
|
307
|
+
const selectionKey = lessonBulkSelectionKey(task.id, candidate.id);
|
|
308
|
+
const canBulkCreate = canUseWorkbenchAction("lesson-sedimentation-bulk") && !hasFollowUp;
|
|
309
|
+
const selected = state.lessonBulkSelection?.[selectionKey] === true;
|
|
244
310
|
return `<div class="lesson-candidate-action">
|
|
245
311
|
<div class="lesson-candidate-main">
|
|
246
312
|
<strong>${escapeHtml(candidate.id)}</strong>
|
|
@@ -249,6 +315,10 @@ function lessonCandidateAction(task, candidate) {
|
|
|
249
315
|
</div>
|
|
250
316
|
<span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
|
|
251
317
|
<div class="lesson-candidate-command-row">
|
|
318
|
+
<label class="bulk-card-check lesson-bulk-check">
|
|
319
|
+
<input type="checkbox" data-lesson-bulk-select="${escapeAttr(selectionKey)}" ${canBulkCreate ? "" : "disabled"} ${selected ? "checked" : ""} aria-label="${escapeAttr(t("selectLessonCandidate"))}">
|
|
320
|
+
<span>${t("select")}</span>
|
|
321
|
+
</label>
|
|
252
322
|
${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
|
|
253
323
|
<button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
|
|
254
324
|
<button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
|
|
@@ -256,6 +326,62 @@ function lessonCandidateAction(task, candidate) {
|
|
|
256
326
|
</div>`;
|
|
257
327
|
}
|
|
258
328
|
|
|
329
|
+
function lessonBulkSelectionKey(taskId, candidateId) {
|
|
330
|
+
return `${taskId}::${candidateId}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseLessonBulkSelectionKey(key) {
|
|
334
|
+
const separator = String(key || "").lastIndexOf("::");
|
|
335
|
+
if (separator < 0) return null;
|
|
336
|
+
const taskId = key.slice(0, separator);
|
|
337
|
+
const candidateId = key.slice(separator + 2);
|
|
338
|
+
if (!taskId || !candidateId) return null;
|
|
339
|
+
return { taskId, candidateId };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function lessonBulkSelectedSelections() {
|
|
343
|
+
return Object.entries(state.lessonBulkSelection || {})
|
|
344
|
+
.filter(([, selected]) => selected === true)
|
|
345
|
+
.map(([key]) => parseLessonBulkSelectionKey(key))
|
|
346
|
+
.filter(Boolean);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function lessonBulkActionableSelections() {
|
|
350
|
+
return (bundle.status?.tasks || []).flatMap((task) => (task.lessonCandidateRows || [])
|
|
351
|
+
.filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status))
|
|
352
|
+
.filter((candidate) => {
|
|
353
|
+
const followUp = String(candidate.followUpTask || "").trim();
|
|
354
|
+
return !followUp || /^pending$/i.test(followUp);
|
|
355
|
+
})
|
|
356
|
+
.map((candidate) => ({ taskId: task.id, candidateId: candidate.id })));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function syncLessonBulkSelection(actionableSelections) {
|
|
360
|
+
const allowed = new Set(actionableSelections.map((selection) => lessonBulkSelectionKey(selection.taskId, selection.candidateId)));
|
|
361
|
+
for (const key of Object.keys(state.lessonBulkSelection || {})) {
|
|
362
|
+
if (!allowed.has(key)) delete state.lessonBulkSelection[key];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function lessonBulkBar() {
|
|
367
|
+
const actionableSelections = lessonBulkActionableSelections();
|
|
368
|
+
syncLessonBulkSelection(actionableSelections);
|
|
369
|
+
const selectedCount = lessonBulkSelectedSelections().length;
|
|
370
|
+
const allSelected = actionableSelections.length > 0 && actionableSelections.every((selection) => state.lessonBulkSelection?.[lessonBulkSelectionKey(selection.taskId, selection.candidateId)] === true);
|
|
371
|
+
const disabled = selectedCount === 0 || !canUseWorkbenchAction("lesson-sedimentation-bulk");
|
|
372
|
+
const result = state.lessonBulkResult ? `<span class="bulk-action-result ${state.lessonBulkResult.ok ? "success" : "failed"}">${escapeHtml(state.lessonBulkResult.message)}</span>` : "";
|
|
373
|
+
return `<div class="bulk-action-bar lesson-bulk-bar">
|
|
374
|
+
<label class="bulk-select-all">
|
|
375
|
+
<input type="checkbox" data-lesson-bulk-select-all ${allSelected ? "checked" : ""} ${actionableSelections.length ? "" : "disabled"} aria-label="${escapeAttr(t("selectAllLessonCandidates"))}">
|
|
376
|
+
<span>${t("selectAllLessonCandidates")}</span>
|
|
377
|
+
</label>
|
|
378
|
+
<span class="bulk-selected-count">${formatMessage("lessonBulkSelected", { count: selectedCount })}</span>
|
|
379
|
+
<button type="button" data-lesson-bulk-create ${disabled ? "disabled" : ""}>${t("lessonBulkCreate")}</button>
|
|
380
|
+
<button type="button" data-lesson-bulk-clear ${selectedCount ? "" : "disabled"}>${t("clearSelection")}</button>
|
|
381
|
+
${result}
|
|
382
|
+
</div>`;
|
|
383
|
+
}
|
|
384
|
+
|
|
259
385
|
function lessonSedimentationPrompt(task, candidate) {
|
|
260
386
|
return [
|
|
261
387
|
"You are executing a lesson sedimentation follow-up task.",
|