coding-agent-harness 1.0.2 → 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 +25 -0
- package/CONTRIBUTING.md +98 -0
- package/README.md +211 -86
- package/README.zh-CN.md +54 -34
- package/SKILL.md +25 -18
- package/docs-release/README.md +9 -5
- package/docs-release/architecture/overview.md +17 -5
- package/docs-release/architecture/overview.zh-CN.md +9 -5
- package/docs-release/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +31 -8
- package/docs-release/guides/agent-installation.md +34 -9
- 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 +3 -2
- package/docs-release/guides/document-audience-and-surfaces.md +3 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
- package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
- package/docs-release/guides/migration-playbook.en-US.md +14 -15
- package/docs-release/guides/migration-playbook.md +14 -15
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
- package/docs-release/guides/parent-control-repository-pattern.md +7 -5
- package/docs-release/guides/preset-development.md +214 -0
- package/docs-release/guides/repository-operating-models.en-US.md +5 -4
- package/docs-release/guides/repository-operating-models.md +5 -4
- 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/en-US.md +1 -1
- package/docs-release/intl/zh-CN.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +8 -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 +2 -2
- package/references/delivery-operating-model-standard.md +3 -3
- package/references/docs-directory-standard.md +6 -7
- package/references/harness-ledger.md +53 -96
- package/references/lessons-governance.md +88 -93
- package/references/module-parallel-standard.md +14 -14
- package/references/planning-loop.md +12 -6
- package/references/pull-request-standard.md +118 -0
- package/references/repo-governance-standard.md +11 -2
- 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/walkthrough-closeout.md +7 -7
- package/scripts/check-harness.mjs +40 -301
- 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 +55 -260
- package/scripts/lib/capability-registry.mjs +66 -8
- package/scripts/lib/check-module-parallel.mjs +237 -0
- package/scripts/lib/check-profiles.mjs +61 -153
- package/scripts/lib/check-task-contracts.mjs +47 -0
- package/scripts/lib/core-shared.mjs +10 -0
- package/scripts/lib/dashboard-data.mjs +29 -6
- package/scripts/lib/dashboard-workbench.mjs +52 -12
- package/scripts/lib/dashboard-writer.mjs +14 -2
- 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 +5 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/migration-support.mjs +1 -1
- 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 +297 -403
- package/scripts/lib/task-review-model.mjs +469 -0
- package/scripts/lib/task-scanner.mjs +130 -236
- 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/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
- package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
- package/templates/AGENTS.md.template +19 -15
- package/templates/dashboard/assets/app-src/00-state.js +1 -0
- package/templates/dashboard/assets/app-src/10-router.js +2 -1
- package/templates/dashboard/assets/app-src/20-overview.js +11 -5
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
- package/templates/dashboard/assets/app-src/45-review.js +241 -22
- package/templates/dashboard/assets/app-src/50-migration.js +24 -10
- package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
- package/templates/dashboard/assets/app.css +698 -156
- package/templates/dashboard/assets/app.css.manifest.json +9 -0
- package/templates/dashboard/assets/app.js +662 -91
- package/templates/dashboard/assets/app.manifest.json +1 -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 +123 -21
- 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/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- 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 +36 -13
- package/templates/reference/execution-workflow-standard.md +4 -3
- package/templates/reference/pull-request-standard.md +80 -0
- package/templates/reference/repo-governance-standard.md +7 -6
- package/templates/reference/review-routing-standard.md +6 -0
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/verifier/verifier-output.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +20 -16
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/execution_strategy.md +30 -0
- package/templates-zh-CN/planning/lesson_candidates.md +18 -6
- package/templates-zh-CN/planning/review.md +59 -1
- package/templates-zh-CN/planning/task_plan.md +30 -10
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/docs-library-standard.md +1 -1
- package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
- package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
- package/templates-zh-CN/reference/pull-request-standard.md +106 -0
- package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
- package/templates-zh-CN/reference/review-routing-standard.md +8 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/scripts/smoke-dashboard.mjs +0 -92
- package/scripts/test-harness.mjs +0 -1395
- package/templates/ssot/Feature-SSoT.md +0 -43
- package/templates/ssot/Lessons-SSoT.md +0 -44
- package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
- package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
|
@@ -15,6 +15,7 @@ const state = {
|
|
|
15
15
|
renderMode: "rendered",
|
|
16
16
|
theme: localStorage.getItem("harness.theme") || "system",
|
|
17
17
|
taskLayout: localStorage.getItem("harness.taskLayout") || "list",
|
|
18
|
+
taskSortOrder: localStorage.getItem("harness.taskSortOrder") === "asc" ? "asc" : "desc",
|
|
18
19
|
runtime: { mode: "static", csrfToken: "", writableActions: [] },
|
|
19
20
|
runtimeLoaded: false,
|
|
20
21
|
runtimePoller: null,
|
|
@@ -30,6 +31,8 @@ const taskDocTabs = [
|
|
|
30
31
|
["strategy", "execution_strategy.md"],
|
|
31
32
|
["visualMap", "visual_map.md"],
|
|
32
33
|
["legacyRoadmap", "visual_roadmap.md"],
|
|
34
|
+
["lessonCandidates", "lesson_candidates.md"],
|
|
35
|
+
["longRunningContract", "long-running-task-contract.md"],
|
|
33
36
|
["progress", "progress.md"],
|
|
34
37
|
["review", "review.md"],
|
|
35
38
|
["findings", "findings.md"],
|
|
@@ -73,15 +76,26 @@ function shell() {
|
|
|
73
76
|
<button data-theme-toggle>${themeLabel()}</button>
|
|
74
77
|
</div>
|
|
75
78
|
</header>
|
|
79
|
+
${runtimeModeBanner()}
|
|
76
80
|
${renderRoute()}
|
|
77
81
|
<div id="drawer-overlay" class="drawer-overlay"></div>
|
|
78
82
|
<div id="task-drawer" class="task-drawer"></div>
|
|
79
83
|
</div>`;
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
function runtimeModeBanner() {
|
|
87
|
+
if (window.__HARNESS_WORKBENCH__ === true) return "";
|
|
88
|
+
return `<section class="runtime-banner">
|
|
89
|
+
<strong>${t("staticReadOnly")}</strong>
|
|
90
|
+
<span>${t("staticReadOnlyDetail")}</span>
|
|
91
|
+
<code>harness dev</code>
|
|
92
|
+
</section>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
function renderRoute() {
|
|
83
96
|
const route = currentRoute();
|
|
84
97
|
if (route.name === "task") return taskDetail(route);
|
|
98
|
+
if (route.name === "reviewTask") return reviewWorkspace(route);
|
|
85
99
|
if (route.name === "review") return reviewQueue();
|
|
86
100
|
if (route.name === "modules") return modulesView(route.id);
|
|
87
101
|
if (route.name === "tasks") return taskIndex();
|
|
@@ -92,6 +106,7 @@ function currentRoute() {
|
|
|
92
106
|
const hash = window.location.hash || "#/";
|
|
93
107
|
const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent);
|
|
94
108
|
if (parts[0] === "tasks" && parts[1]) return { name: "task", id: parts[1], doc: parts[2] === "docs" ? parts[3] || "" : "" };
|
|
109
|
+
if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
|
|
95
110
|
if (parts[0] === "review") return { name: "review" };
|
|
96
111
|
if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
|
|
97
112
|
if (parts[0] === "tasks") return { name: "tasks" };
|
|
@@ -99,7 +114,8 @@ function currentRoute() {
|
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
function routeLink(hash, text, routeName) {
|
|
102
|
-
const
|
|
117
|
+
const current = currentRoute().name;
|
|
118
|
+
const active = current === routeName || (routeName === "review" && current === "reviewTask");
|
|
103
119
|
return `<a class="${active ? "active" : ""}" href="${hash}">${escapeHtml(text)}</a>`;
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -270,24 +286,29 @@ function graphSummary() {
|
|
|
270
286
|
}
|
|
271
287
|
|
|
272
288
|
function activeTaskBriefs() {
|
|
273
|
-
const tasks = activeTasks()
|
|
289
|
+
const tasks = activeTasks();
|
|
274
290
|
return `<section class="task-briefs">
|
|
275
291
|
<div class="section-head">
|
|
276
292
|
<div>
|
|
277
293
|
<p class="eyebrow">${t("currentWork")}</p>
|
|
278
294
|
<h2>${t("activeBriefs")}</h2>
|
|
279
295
|
</div>
|
|
280
|
-
<
|
|
296
|
+
<div class="section-actions">
|
|
297
|
+
<span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
|
|
298
|
+
<a href="#/tasks">${t("openTaskIndex")}</a>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="brief-scroll">
|
|
302
|
+
<div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
|
|
281
303
|
</div>
|
|
282
|
-
<div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
|
|
283
304
|
</section>`;
|
|
284
305
|
}
|
|
285
306
|
|
|
286
307
|
function activeTasks() {
|
|
287
308
|
const tasks = bundle.status?.tasks || [];
|
|
288
309
|
const active = tasks.filter((task) => isActiveTaskState(task.state) || ["planned", "not_started"].includes(task.state));
|
|
289
|
-
if (active.length > 0) return active;
|
|
290
|
-
return tasks.filter((task) => task.briefSource === "standalone")
|
|
310
|
+
if (active.length > 0) return sortTasksByTime(active);
|
|
311
|
+
return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
|
|
291
312
|
}
|
|
292
313
|
|
|
293
314
|
function isActiveTaskState(state) {
|
|
@@ -310,6 +331,7 @@ function taskBriefCard(task, { compact = true } = {}) {
|
|
|
310
331
|
<p class="brief-teaser">${escapeHtml(summaryText)}</p>
|
|
311
332
|
</div>
|
|
312
333
|
<div class="card-actions">
|
|
334
|
+
${taskCopyButton(task)}
|
|
313
335
|
<button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
314
336
|
</div>
|
|
315
337
|
</article>`;
|
|
@@ -349,6 +371,59 @@ function stateToColorVar(state) {
|
|
|
349
371
|
return map[state] || "--muted";
|
|
350
372
|
}
|
|
351
373
|
|
|
374
|
+
function taskSortLabel() {
|
|
375
|
+
return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function taskDateKey(task) {
|
|
379
|
+
const source = `${task.shortId || ""} ${task.id || ""}`.trim();
|
|
380
|
+
const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
|
|
381
|
+
if (!match) return null;
|
|
382
|
+
const year = Number(match[1]);
|
|
383
|
+
const month = Number(match[2]);
|
|
384
|
+
const day = Number(match[3] || "1");
|
|
385
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
386
|
+
return Date.UTC(year, month - 1, day);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function stableTaskLabel(task) {
|
|
390
|
+
return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function compareTasksByTime(left, right) {
|
|
394
|
+
const leftDate = taskDateKey(left);
|
|
395
|
+
const rightDate = taskDateKey(right);
|
|
396
|
+
if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
|
|
397
|
+
return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
|
|
398
|
+
}
|
|
399
|
+
if (leftDate !== null && rightDate === null) return -1;
|
|
400
|
+
if (leftDate === null && rightDate !== null) return 1;
|
|
401
|
+
return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function sortTasksByTime(tasks) {
|
|
405
|
+
return [...tasks].sort(compareTasksByTime);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function taskFolderName(task) {
|
|
409
|
+
const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
|
|
410
|
+
const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
|
|
411
|
+
return task?.shortId || fromPath || fromId || task?.title || "";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function taskCopyButton(task, extraClass = "") {
|
|
415
|
+
const folderName = taskFolderName(task);
|
|
416
|
+
return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
|
|
417
|
+
${t("copyTaskNameShort")}
|
|
418
|
+
</button>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function taskGroupTimeKey(group) {
|
|
422
|
+
const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
|
|
423
|
+
if (!match) return null;
|
|
424
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
|
|
425
|
+
}
|
|
426
|
+
|
|
352
427
|
function taskToolbarCard(filteredCount) {
|
|
353
428
|
return `<section class="sidebar-card">
|
|
354
429
|
<h3>${t("filterTitle")}</h3>
|
|
@@ -380,6 +455,17 @@ function taskToolbarCard(filteredCount) {
|
|
|
380
455
|
</button>
|
|
381
456
|
</div>
|
|
382
457
|
</div>
|
|
458
|
+
<div class="select-group">
|
|
459
|
+
<label>${t("sortByTime")}</label>
|
|
460
|
+
<div class="layout-toggle-group sort-toggle-group">
|
|
461
|
+
<button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
|
|
462
|
+
${t("sortNewest")}
|
|
463
|
+
</button>
|
|
464
|
+
<button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
|
|
465
|
+
${t("sortOldest")}
|
|
466
|
+
</button>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
383
469
|
<div class="search-stats">
|
|
384
470
|
${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
|
|
385
471
|
</div>
|
|
@@ -483,11 +569,12 @@ function taskRow(task) {
|
|
|
483
569
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
484
570
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
485
571
|
|
|
486
|
-
return `<
|
|
572
|
+
return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
487
573
|
<div class="row-accent-bar"></div>
|
|
488
574
|
<div class="row-main">
|
|
489
575
|
<strong>${escapeHtml(task.title)}</strong>
|
|
490
576
|
<span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
|
|
577
|
+
${taskCopyButton(task, "row-copy")}
|
|
491
578
|
</div>
|
|
492
579
|
<div class="row-status">${tag(task.state)}</div>
|
|
493
580
|
<div class="row-progress">
|
|
@@ -506,7 +593,7 @@ function taskRow(task) {
|
|
|
506
593
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
507
594
|
</span>
|
|
508
595
|
</div>
|
|
509
|
-
</
|
|
596
|
+
</article>`;
|
|
510
597
|
}
|
|
511
598
|
|
|
512
599
|
function taskIndex() {
|
|
@@ -545,7 +632,18 @@ function orderedTaskGroups(groups) {
|
|
|
545
632
|
if (group === "unknown") return 3;
|
|
546
633
|
return 4;
|
|
547
634
|
};
|
|
548
|
-
return Object.entries(groups).sort(([left], [right]) =>
|
|
635
|
+
return Object.entries(groups).sort(([left], [right]) => {
|
|
636
|
+
const rankDiff = rank(left) - rank(right);
|
|
637
|
+
if (rankDiff !== 0) return rankDiff;
|
|
638
|
+
const leftTime = taskGroupTimeKey(left);
|
|
639
|
+
const rightTime = taskGroupTimeKey(right);
|
|
640
|
+
if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
|
|
641
|
+
return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
|
|
642
|
+
}
|
|
643
|
+
if (leftTime !== null && rightTime === null) return -1;
|
|
644
|
+
if (leftTime === null && rightTime !== null) return 1;
|
|
645
|
+
return left.localeCompare(right);
|
|
646
|
+
});
|
|
549
647
|
}
|
|
550
648
|
|
|
551
649
|
function taskGroups(tasks) {
|
|
@@ -570,11 +668,12 @@ function taskGroups(tasks) {
|
|
|
570
668
|
}
|
|
571
669
|
|
|
572
670
|
function taskGroup(group, tasks) {
|
|
573
|
-
const
|
|
671
|
+
const orderedTasks = sortTasksByTime(tasks);
|
|
672
|
+
const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
|
|
574
673
|
const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
|
|
575
674
|
const start = (page - 1) * taskPageSize;
|
|
576
|
-
const visibleTasks =
|
|
577
|
-
const avgCompletion =
|
|
675
|
+
const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
|
|
676
|
+
const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
|
|
578
677
|
|
|
579
678
|
const isGrid = state.taskLayout === "grid";
|
|
580
679
|
const layoutClass = isGrid ? "task-card-grid" : "task-list";
|
|
@@ -591,7 +690,7 @@ function taskGroup(group, tasks) {
|
|
|
591
690
|
<div class="section-head">
|
|
592
691
|
<div>
|
|
593
692
|
<h2>${taskGroupLabel(group)}</h2>
|
|
594
|
-
<p class="subtle">${t("showing")} ${Math.min(start + 1,
|
|
693
|
+
<p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
|
|
595
694
|
</div>
|
|
596
695
|
<div class="group-actions">
|
|
597
696
|
<div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
|
|
@@ -616,10 +715,13 @@ function taskCard(task) {
|
|
|
616
715
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
617
716
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
618
717
|
|
|
619
|
-
return `<
|
|
718
|
+
return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
|
|
620
719
|
<div class="card-header">
|
|
621
720
|
<span class="card-id">${escapeHtml(task.id)}</span>
|
|
622
|
-
|
|
721
|
+
<div class="card-header-actions">
|
|
722
|
+
${taskCopyButton(task, "compact")}
|
|
723
|
+
${tag(task.state)}
|
|
724
|
+
</div>
|
|
623
725
|
</div>
|
|
624
726
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
625
727
|
<div class="card-meta">
|
|
@@ -642,7 +744,7 @@ function taskCard(task) {
|
|
|
642
744
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
643
745
|
</span>
|
|
644
746
|
</div>
|
|
645
|
-
</
|
|
747
|
+
</article>`;
|
|
646
748
|
}
|
|
647
749
|
|
|
648
750
|
function taskGroupLabel(group) {
|
|
@@ -657,12 +759,12 @@ function taskGroupLabel(group) {
|
|
|
657
759
|
|
|
658
760
|
function filteredTasks() {
|
|
659
761
|
const query = state.query.trim().toLowerCase();
|
|
660
|
-
return (bundle.status?.tasks || []).filter((task) => {
|
|
762
|
+
return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
|
|
661
763
|
const stateMatch = state.taskState === "all" || task.state === state.taskState;
|
|
662
764
|
if (!stateMatch) return false;
|
|
663
765
|
if (!query) return true;
|
|
664
766
|
return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
|
|
665
|
-
});
|
|
767
|
+
}));
|
|
666
768
|
}
|
|
667
769
|
|
|
668
770
|
function taskModuleKey(task) {
|
|
@@ -680,6 +782,7 @@ function taskDetail(route) {
|
|
|
680
782
|
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
681
783
|
<h2>${escapeHtml(task.title)}</h2>
|
|
682
784
|
<p>${escapeHtml(task.path)}</p>
|
|
785
|
+
${taskCopyButton(task, "detail-copy")}
|
|
683
786
|
</div>
|
|
684
787
|
<div class="detail-score">${task.completion}%</div>
|
|
685
788
|
</section>
|
|
@@ -690,7 +793,8 @@ function taskDetail(route) {
|
|
|
690
793
|
${taskDocumentLibrary(task, route.doc)}
|
|
691
794
|
</article>
|
|
692
795
|
<aside class="detail-side">
|
|
693
|
-
${reviewActionPanel(task)}
|
|
796
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
797
|
+
${lessonCandidatePanel(task, { context: "detail" })}
|
|
694
798
|
${openFindings(task)}
|
|
695
799
|
${evidenceList(task)}
|
|
696
800
|
${documentTabs(task)}
|
|
@@ -713,13 +817,33 @@ function taskStateSummary(task) {
|
|
|
713
817
|
<span>${t("reviewStatus")}</span>
|
|
714
818
|
${tag(task.reviewStatus || "missing")}
|
|
715
819
|
</div>
|
|
820
|
+
<div>
|
|
821
|
+
<span>${t("sedimentationStatus")}</span>
|
|
822
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
823
|
+
</div>
|
|
716
824
|
<div>
|
|
717
825
|
<span>${t("closeoutStatus")}</span>
|
|
718
826
|
${tag(task.closeoutStatus || "missing")}
|
|
719
827
|
</div>
|
|
828
|
+
<div>
|
|
829
|
+
<span>${t("lifecycleQueues")}</span>
|
|
830
|
+
${(task.taskQueues || []).map(tag).join("") || tag("active")}
|
|
831
|
+
</div>
|
|
832
|
+
${taskQueueReasonSummary(task)}
|
|
720
833
|
</section>`;
|
|
721
834
|
}
|
|
722
835
|
|
|
836
|
+
function taskQueueReasonSummary(task) {
|
|
837
|
+
const reasons = task.queueReasons || [];
|
|
838
|
+
if (!reasons.length) return "";
|
|
839
|
+
return `<div class="task-queue-reasons">
|
|
840
|
+
<span>${t("queueReasons")}</span>
|
|
841
|
+
<div class="review-reasons">
|
|
842
|
+
${reasons.slice(0, 5).map(reviewReason).join("")}
|
|
843
|
+
</div>
|
|
844
|
+
</div>`;
|
|
845
|
+
}
|
|
846
|
+
|
|
723
847
|
function phaseTimeline(task) {
|
|
724
848
|
return `<section class="phase-timeline">
|
|
725
849
|
<h2>${t("phaseTimeline")}</h2>
|
|
@@ -777,7 +901,7 @@ function taskDocumentPriority(task) {
|
|
|
777
901
|
const stateName = task?.state || "";
|
|
778
902
|
const lifecycle = task?.lifecycleState || "";
|
|
779
903
|
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
780
|
-
return ["walkthrough", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "legacyRoadmap", "references", "artifacts"];
|
|
904
|
+
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
781
905
|
}
|
|
782
906
|
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
783
907
|
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
@@ -831,11 +955,26 @@ function openFindings(task) {
|
|
|
831
955
|
</section>`;
|
|
832
956
|
}
|
|
833
957
|
|
|
834
|
-
function reviewActionPanel(task) {
|
|
835
|
-
if (!
|
|
836
|
-
if (!isTaskInReviewStage(task)) return "";
|
|
958
|
+
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
959
|
+
if (!isTaskInReviewQueue(task)) return "";
|
|
837
960
|
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
838
961
|
const confirmed = task.reviewStatus === "confirmed";
|
|
962
|
+
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
963
|
+
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
964
|
+
if (mode !== "workspace") {
|
|
965
|
+
return `<section class="side-panel review-actions">
|
|
966
|
+
<h3>${t("reviewActions")}</h3>
|
|
967
|
+
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
968
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
969
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
970
|
+
</section>`;
|
|
971
|
+
}
|
|
972
|
+
if (!canUseWorkbenchAction("review-complete")) {
|
|
973
|
+
return `<section class="side-panel review-actions">
|
|
974
|
+
<h3>${t("reviewActions")}</h3>
|
|
975
|
+
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
976
|
+
</section>`;
|
|
977
|
+
}
|
|
839
978
|
if (confirmed) {
|
|
840
979
|
return `<section class="side-panel review-actions">
|
|
841
980
|
<h3>${t("reviewActions")}</h3>
|
|
@@ -843,26 +982,32 @@ function reviewActionPanel(task) {
|
|
|
843
982
|
</section>`;
|
|
844
983
|
}
|
|
845
984
|
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
846
|
-
const
|
|
847
|
-
const
|
|
985
|
+
const queueBlocked = !taskCanBeHumanConfirmed(task);
|
|
986
|
+
const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
|
|
987
|
+
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
|
|
848
988
|
return `<section class="side-panel review-actions">
|
|
849
989
|
<h3>${t("reviewActions")}</h3>
|
|
850
990
|
<p>${escapeHtml(message)}</p>
|
|
991
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
851
992
|
<label class="review-check">
|
|
852
993
|
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
853
994
|
<span>${t("reviewConfirmChecklist")}</span>
|
|
854
995
|
</label>
|
|
996
|
+
<div class="review-confirm-copy">
|
|
997
|
+
${taskCopyButton(task, "review-copy-task-name")}
|
|
998
|
+
</div>
|
|
855
999
|
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
856
1000
|
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
857
1001
|
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
858
1002
|
</section>`;
|
|
859
1003
|
}
|
|
860
1004
|
|
|
861
|
-
function
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1005
|
+
function isTaskInReviewQueue(task) {
|
|
1006
|
+
return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function taskCanBeHumanConfirmed(task) {
|
|
1010
|
+
return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
|
|
866
1011
|
}
|
|
867
1012
|
|
|
868
1013
|
function evidenceList(task) {
|
|
@@ -933,10 +1078,16 @@ function moduleCard(module) {
|
|
|
933
1078
|
}
|
|
934
1079
|
|
|
935
1080
|
function reviewQueue() {
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
const
|
|
1081
|
+
ensureReviewQueueState();
|
|
1082
|
+
const tabs = reviewQueueTabs();
|
|
1083
|
+
const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
|
|
1084
|
+
const baseTasks = reviewQueueBaseTasks(activeTab);
|
|
1085
|
+
const reasonOptions = reviewReasonOptions(baseTasks);
|
|
1086
|
+
normalizeReviewReasonFilter(reasonOptions);
|
|
1087
|
+
const tasks = reviewFilteredTasks(baseTasks);
|
|
1088
|
+
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
1089
|
+
const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
|
|
1090
|
+
const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
|
|
940
1091
|
return `<div class="dashboard-grid review-queue-page">
|
|
941
1092
|
<main class="dashboard-main stack">
|
|
942
1093
|
<section class="flow-panel">
|
|
@@ -946,10 +1097,36 @@ function reviewQueue() {
|
|
|
946
1097
|
<h2>${t("reviewQueue")}</h2>
|
|
947
1098
|
<p class="subtle">${t("reviewQueueSubtitle")}</p>
|
|
948
1099
|
</div>
|
|
949
|
-
<span class="subtle">${
|
|
1100
|
+
<span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
|
|
950
1101
|
</div>
|
|
951
|
-
<div class="
|
|
952
|
-
${
|
|
1102
|
+
<div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
|
|
1103
|
+
${tabs.map((tab) => reviewQueueTab(tab)).join("")}
|
|
1104
|
+
</div>
|
|
1105
|
+
<div class="review-queue-toolbar">
|
|
1106
|
+
<div class="input-group">
|
|
1107
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="select-group">
|
|
1110
|
+
<label>${t("reasonFilter")}</label>
|
|
1111
|
+
<select data-review-reason-filter aria-label="${t("reasonFilter")}">
|
|
1112
|
+
<option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
|
|
1113
|
+
${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
|
|
1114
|
+
</select>
|
|
1115
|
+
</div>
|
|
1116
|
+
<div class="select-group">
|
|
1117
|
+
<label>${t("sortBy")}</label>
|
|
1118
|
+
<select data-review-sort aria-label="${t("sortBy")}">
|
|
1119
|
+
${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
|
|
1120
|
+
</select>
|
|
1121
|
+
</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
|
|
1124
|
+
<div class="review-queue-list">
|
|
1125
|
+
${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div class="review-queue-pager">
|
|
1129
|
+
${pager("review", page, pageCount)}
|
|
953
1130
|
</div>
|
|
954
1131
|
</section>
|
|
955
1132
|
</main>
|
|
@@ -957,52 +1134,239 @@ function reviewQueue() {
|
|
|
957
1134
|
<section class="side-panel review-queue-summary">
|
|
958
1135
|
<h3>${t("reviewQueue")}</h3>
|
|
959
1136
|
<div class="review-queue-stats">
|
|
960
|
-
${metric(
|
|
961
|
-
${metric(t("reviewBlockedQueue"), blocked)}
|
|
962
|
-
${metric(t("reviewConfirmedQueue"), confirmed)}
|
|
1137
|
+
${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
|
|
963
1138
|
</div>
|
|
964
1139
|
</section>
|
|
965
1140
|
<section class="side-panel">
|
|
966
|
-
<h3>${
|
|
967
|
-
<p>${escapeHtml(
|
|
1141
|
+
<h3>${escapeHtml(activeTab.label)}</h3>
|
|
1142
|
+
<p>${escapeHtml(activeTab.description)}</p>
|
|
1143
|
+
<dl class="review-queue-contract">
|
|
1144
|
+
<div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
|
|
1145
|
+
<div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
|
|
1146
|
+
</dl>
|
|
968
1147
|
</section>
|
|
969
1148
|
</aside>
|
|
970
1149
|
</div>`;
|
|
971
1150
|
}
|
|
972
1151
|
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1152
|
+
function ensureReviewQueueState() {
|
|
1153
|
+
if (!state.reviewQueueTab) state.reviewQueueTab = "review";
|
|
1154
|
+
if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
|
|
1155
|
+
if (!state.reviewSort) state.reviewSort = "queue";
|
|
1156
|
+
if (!state.reviewQueuePage) state.reviewQueuePage = 1;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function reviewQueueTabs() {
|
|
1160
|
+
return [
|
|
1161
|
+
{ id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
|
|
1162
|
+
{ id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
|
|
1163
|
+
{ id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
|
|
1164
|
+
{ id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
|
|
1165
|
+
{ id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
|
|
1166
|
+
{ id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
|
|
1167
|
+
];
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function reviewQueueTab(tab) {
|
|
1171
|
+
const active = tab.id === state.reviewQueueTab;
|
|
1172
|
+
const count = reviewQueueBaseTasks(tab).length;
|
|
1173
|
+
return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
|
|
1174
|
+
<span>${escapeHtml(tab.label)}</span>
|
|
1175
|
+
<strong>${count}</strong>
|
|
1176
|
+
</button>`;
|
|
977
1177
|
}
|
|
978
1178
|
|
|
979
|
-
function
|
|
980
|
-
|
|
981
|
-
|
|
1179
|
+
function reviewSortOptions() {
|
|
1180
|
+
return [
|
|
1181
|
+
{ id: "queue", label: t("sortQueuePriority") },
|
|
1182
|
+
{ id: "newest", label: t("sortNewest") },
|
|
1183
|
+
{ id: "oldest", label: t("sortOldest") },
|
|
1184
|
+
{ id: "id", label: t("sortTaskId") },
|
|
1185
|
+
];
|
|
982
1186
|
}
|
|
983
1187
|
|
|
984
|
-
function
|
|
1188
|
+
function reviewQueueBaseTasks(tab) {
|
|
1189
|
+
return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function taskMatchesReviewTab(task, tab) {
|
|
1193
|
+
const queues = reviewTaskQueues(task);
|
|
1194
|
+
return (tab.queues || []).some((queue) => queues.includes(queue));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function reviewTaskQueues(task) {
|
|
1198
|
+
return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function reviewReasonOptions(tasks) {
|
|
1202
|
+
return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function normalizeReviewReasonFilter(reasonOptions) {
|
|
1206
|
+
const current = state.reviewReasonFilter || "all";
|
|
1207
|
+
if (current === "all") return;
|
|
1208
|
+
if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function reviewFilteredTasks(tasks) {
|
|
1212
|
+
const query = state.query.trim().toLowerCase();
|
|
1213
|
+
const reasonFilter = state.reviewReasonFilter || "all";
|
|
1214
|
+
return [...tasks]
|
|
1215
|
+
.filter((task) => {
|
|
1216
|
+
if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
|
|
1217
|
+
if (!query) return true;
|
|
1218
|
+
return [
|
|
1219
|
+
task.id,
|
|
1220
|
+
task.shortId,
|
|
1221
|
+
task.title,
|
|
1222
|
+
task.module,
|
|
1223
|
+
task.inferredModule,
|
|
1224
|
+
task.state,
|
|
1225
|
+
task.lifecycleState,
|
|
1226
|
+
task.reviewStatus,
|
|
1227
|
+
task.closeoutStatus,
|
|
1228
|
+
...(task.taskQueues || []),
|
|
1229
|
+
...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
|
|
1230
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
1231
|
+
})
|
|
1232
|
+
.sort(reviewTaskSort);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function reviewTaskSort(left, right) {
|
|
1236
|
+
if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
|
|
1237
|
+
if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
|
|
1238
|
+
if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
1239
|
+
return reviewPriorityRank(left) - reviewPriorityRank(right)
|
|
1240
|
+
|| compareTasksByTimeForOrder(left, right, "desc")
|
|
1241
|
+
|| stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function compareTasksByTimeForOrder(left, right, order) {
|
|
1245
|
+
const previous = state.taskSortOrder;
|
|
1246
|
+
state.taskSortOrder = order;
|
|
1247
|
+
const result = compareTasksByTime(left, right);
|
|
1248
|
+
state.taskSortOrder = previous;
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function reviewPriorityRank(task) {
|
|
1253
|
+
const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
1254
|
+
const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
|
|
1255
|
+
const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
|
|
1256
|
+
const queues = reviewTaskQueues(task);
|
|
1257
|
+
const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
|
|
1258
|
+
return Math.min(reasonRank, taskQueueRank);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function reviewTruthyCount(tasks, key) {
|
|
1262
|
+
return tasks.filter((task) => task[key] === true).length;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function reviewQueueCard(task, tab) {
|
|
985
1266
|
const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
|
|
1267
|
+
const reasons = task.queueReasons || [];
|
|
1268
|
+
const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
|
|
1269
|
+
const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
|
|
1270
|
+
const displayId = task.shortId || taskFolderName(task) || task.id;
|
|
986
1271
|
return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
987
1272
|
<div class="card-header">
|
|
988
|
-
<span class="card-id"
|
|
1273
|
+
<span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
|
|
989
1274
|
${tag(task.reviewStatus || "missing")}
|
|
1275
|
+
${reviewTaskQueues(task).map(tag).join("")}
|
|
990
1276
|
</div>
|
|
991
1277
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
992
1278
|
<div class="card-meta">
|
|
993
1279
|
<span>${tag(task.lifecycleState || "unknown")}</span>
|
|
994
1280
|
<span>${tag(task.closeoutStatus || "missing")}</span>
|
|
995
1281
|
<span>${openMaterial} ${t("openFindings")}</span>
|
|
1282
|
+
<span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
|
|
1283
|
+
<span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
|
|
996
1284
|
</div>
|
|
997
1285
|
<p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
|
|
1286
|
+
${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
|
|
1287
|
+
${lessonActions}
|
|
998
1288
|
<div class="review-queue-actions">
|
|
1289
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
999
1290
|
<a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
|
|
1000
1291
|
<button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
1292
|
+
${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
|
|
1001
1293
|
</div>
|
|
1002
|
-
${reviewActionPanel(task)}
|
|
1003
1294
|
</article>`;
|
|
1004
1295
|
}
|
|
1005
1296
|
|
|
1297
|
+
function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
1298
|
+
const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
|
|
1299
|
+
if (!candidates.length) return "";
|
|
1300
|
+
const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
|
|
1301
|
+
const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
|
|
1302
|
+
const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
|
|
1303
|
+
return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
|
|
1304
|
+
<div class="lesson-candidate-panel-head">
|
|
1305
|
+
<div>
|
|
1306
|
+
<p class="eyebrow">${t("lessonCandidates")}</p>
|
|
1307
|
+
<h3>${t("lessonSedimentationActions")}</h3>
|
|
1308
|
+
</div>
|
|
1309
|
+
<span class="tag">${visibleCandidates.length}/${candidates.length}</span>
|
|
1310
|
+
</div>
|
|
1311
|
+
${staticNote}
|
|
1312
|
+
<div class="lesson-candidate-actions">
|
|
1313
|
+
${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
|
|
1314
|
+
</div>
|
|
1315
|
+
${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
|
|
1316
|
+
</section>`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function lessonCandidateAction(task, candidate) {
|
|
1320
|
+
const followUp = String(candidate.followUpTask || "").trim();
|
|
1321
|
+
const hasFollowUp = followUp && !/^pending$/i.test(followUp);
|
|
1322
|
+
const prompt = lessonSedimentationPrompt(task, candidate);
|
|
1323
|
+
return `<div class="lesson-candidate-action">
|
|
1324
|
+
<div class="lesson-candidate-main">
|
|
1325
|
+
<strong>${escapeHtml(candidate.id)}</strong>
|
|
1326
|
+
<span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
|
|
1327
|
+
<small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
|
|
1328
|
+
</div>
|
|
1329
|
+
<span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
|
|
1330
|
+
<div class="lesson-candidate-command-row">
|
|
1331
|
+
${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
|
|
1332
|
+
<button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
|
|
1333
|
+
<button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
|
|
1334
|
+
</div>
|
|
1335
|
+
</div>`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function lessonSedimentationPrompt(task, candidate) {
|
|
1339
|
+
return [
|
|
1340
|
+
"You are executing a lesson sedimentation follow-up task.",
|
|
1341
|
+
"",
|
|
1342
|
+
`Source task: ${task.id}`,
|
|
1343
|
+
`Source candidate: ${candidate.id} - ${candidate.title || ""}`,
|
|
1344
|
+
`Candidate scope: ${candidate.scope || "unspecified"}`,
|
|
1345
|
+
`Candidate module key: ${candidate.moduleKey || "n/a"}`,
|
|
1346
|
+
`Detail artifact: ${candidate.detailArtifact || "not provided"}`,
|
|
1347
|
+
`Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
|
|
1348
|
+
`Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
|
|
1349
|
+
`Promotion target: ${candidate.promotionTarget || "unspecified"}`,
|
|
1350
|
+
`Conflict check: ${candidate.conflictCheck || "pending"}`,
|
|
1351
|
+
`Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
|
|
1352
|
+
"",
|
|
1353
|
+
"Instructions:",
|
|
1354
|
+
"1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
|
|
1355
|
+
"2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
|
|
1356
|
+
"3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
|
|
1357
|
+
"4. Check conflicts against existing lessons and standards.",
|
|
1358
|
+
"5. Propose the smallest diff first.",
|
|
1359
|
+
"6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
|
|
1360
|
+
].join("\n");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function reviewReason(reason) {
|
|
1364
|
+
return `<div class="review-reason">
|
|
1365
|
+
<strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
|
|
1366
|
+
<span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
|
|
1367
|
+
</div>`;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1006
1370
|
function firstUsefulLine(text) {
|
|
1007
1371
|
return String(text || "")
|
|
1008
1372
|
.split(/\n+/)
|
|
@@ -1010,6 +1374,57 @@ function firstUsefulLine(text) {
|
|
|
1010
1374
|
.filter(Boolean)[0] || "";
|
|
1011
1375
|
}
|
|
1012
1376
|
|
|
1377
|
+
function reviewWorkspace(route) {
|
|
1378
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
|
|
1379
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
1380
|
+
const walkthroughDoc = taskDocument(task, "__walkthrough__");
|
|
1381
|
+
const candidateDoc = taskDocument(task, "lesson_candidates.md");
|
|
1382
|
+
const reviewDoc = taskDocument(task, "review.md");
|
|
1383
|
+
const findingsDoc = taskDocument(task, "findings.md");
|
|
1384
|
+
return `<main class="review-workspace">
|
|
1385
|
+
<nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
1386
|
+
<section class="detail-hero review-hero">
|
|
1387
|
+
<div>
|
|
1388
|
+
<p class="eyebrow">${t("reviewWorkspace")}</p>
|
|
1389
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
1390
|
+
<p>${escapeHtml(task.path)}</p>
|
|
1391
|
+
</div>
|
|
1392
|
+
<div class="review-hero-tags">
|
|
1393
|
+
${tag(task.lifecycleState || "unknown")}
|
|
1394
|
+
${tag(task.reviewStatus || "missing")}
|
|
1395
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
1396
|
+
</div>
|
|
1397
|
+
</section>
|
|
1398
|
+
<section class="review-workspace-grid">
|
|
1399
|
+
<article class="review-workspace-main stack">
|
|
1400
|
+
${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
|
|
1401
|
+
${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
|
|
1402
|
+
${reviewDocPanel("review", reviewDoc, task.reviewPath)}
|
|
1403
|
+
${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
|
|
1404
|
+
</article>
|
|
1405
|
+
<aside class="review-workspace-side stack">
|
|
1406
|
+
${reviewActionPanel(task, { mode: "workspace" })}
|
|
1407
|
+
${taskStateSummary(task)}
|
|
1408
|
+
${openFindings(task)}
|
|
1409
|
+
${evidenceList(task)}
|
|
1410
|
+
</aside>
|
|
1411
|
+
</section>
|
|
1412
|
+
</main>`;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function reviewDocPanel(key, doc, fallbackPath = "") {
|
|
1416
|
+
return `<section class="doc-section review-doc-panel">
|
|
1417
|
+
<div class="section-head">
|
|
1418
|
+
<div>
|
|
1419
|
+
<p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
|
|
1420
|
+
<h2>${t(key)}</h2>
|
|
1421
|
+
</div>
|
|
1422
|
+
${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
|
|
1423
|
+
</div>
|
|
1424
|
+
<div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
|
|
1425
|
+
</section>`;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1013
1428
|
function migrationPanel() {
|
|
1014
1429
|
const advice = warningQueue();
|
|
1015
1430
|
const missingBriefs = advice.filter((warning) => warning.type === "missing-brief").length;
|
|
@@ -1150,25 +1565,39 @@ function pager(kind, page, pageCount, group = "") {
|
|
|
1150
1565
|
}
|
|
1151
1566
|
|
|
1152
1567
|
function lessonPanel() {
|
|
1153
|
-
const lessons = (
|
|
1154
|
-
.filter((table) => table.kind === "lessons-ssot")
|
|
1155
|
-
.flatMap((table) => table.rows);
|
|
1568
|
+
const lessons = lessonDocuments();
|
|
1156
1569
|
return `<section class="lesson-panel">
|
|
1157
1570
|
<div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
|
|
1158
1571
|
<div class="lesson-list" style="padding-top: 10px;">
|
|
1159
|
-
${lessons.map((
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
|
|
1164
|
-
<strong>${escapeHtml(lessonId)}</strong>
|
|
1165
|
-
<p>${escapeHtml(summary)}</p>
|
|
1572
|
+
${lessons.map((lesson) => {
|
|
1573
|
+
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
|
|
1574
|
+
<strong>${escapeHtml(lesson.id)}</strong>
|
|
1575
|
+
<p>${escapeHtml(lesson.title || lesson.path)}</p>
|
|
1166
1576
|
</div>`;
|
|
1167
1577
|
}).join("") || emptyState(t("noLessons"))}
|
|
1168
1578
|
</div>
|
|
1169
1579
|
</section>`;
|
|
1170
1580
|
}
|
|
1171
1581
|
|
|
1582
|
+
function lessonDocuments() {
|
|
1583
|
+
return (bundle.documents?.documents || [])
|
|
1584
|
+
.filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
|
|
1585
|
+
.map((doc) => {
|
|
1586
|
+
const id = lessonIdFromDocument(doc);
|
|
1587
|
+
return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
|
|
1588
|
+
})
|
|
1589
|
+
.filter((lesson) => lesson.id)
|
|
1590
|
+
.sort((left, right) => String(right.id).localeCompare(String(left.id)));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function lessonIdFromDocument(doc) {
|
|
1594
|
+
const content = doc?.content || "";
|
|
1595
|
+
const path = doc?.path || "";
|
|
1596
|
+
return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
1597
|
+
|| path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
1598
|
+
|| "";
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1172
1601
|
function healthPanel() {
|
|
1173
1602
|
const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
|
|
1174
1603
|
return `<section class="health-panel">
|
|
@@ -1272,6 +1701,13 @@ function bind() {
|
|
|
1272
1701
|
localStorage.setItem("harness.taskLayout", state.taskLayout);
|
|
1273
1702
|
app();
|
|
1274
1703
|
}));
|
|
1704
|
+
document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
|
|
1705
|
+
state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
|
|
1706
|
+
localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
|
|
1707
|
+
state.taskPageByGroup = {};
|
|
1708
|
+
state.taskGroupPage = 1;
|
|
1709
|
+
app();
|
|
1710
|
+
}));
|
|
1275
1711
|
document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
1276
1712
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
1277
1713
|
app();
|
|
@@ -1286,11 +1722,27 @@ function bind() {
|
|
|
1286
1722
|
state.warningPage = 1;
|
|
1287
1723
|
app();
|
|
1288
1724
|
}));
|
|
1725
|
+
document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
|
|
1726
|
+
state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
|
|
1727
|
+
state.reviewQueuePage = 1;
|
|
1728
|
+
app();
|
|
1729
|
+
}));
|
|
1730
|
+
document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
|
|
1731
|
+
state.reviewReasonFilter = select.value || "all";
|
|
1732
|
+
state.reviewQueuePage = 1;
|
|
1733
|
+
app();
|
|
1734
|
+
}));
|
|
1735
|
+
document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
|
|
1736
|
+
state.reviewSort = select.value || "queue";
|
|
1737
|
+
state.reviewQueuePage = 1;
|
|
1738
|
+
app();
|
|
1739
|
+
}));
|
|
1289
1740
|
document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
|
|
1290
1741
|
const page = Math.max(1, Number(button.dataset.page) || 1);
|
|
1291
1742
|
if (button.dataset.pageKind === "warning") state.warningPage = page;
|
|
1292
1743
|
if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
|
|
1293
1744
|
if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
|
|
1745
|
+
if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
|
|
1294
1746
|
app();
|
|
1295
1747
|
}));
|
|
1296
1748
|
document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
|
|
@@ -1315,6 +1767,9 @@ function bind() {
|
|
|
1315
1767
|
const taskId = el.dataset.openDrawer;
|
|
1316
1768
|
openDrawer(taskId);
|
|
1317
1769
|
}));
|
|
1770
|
+
bindCopyTaskNameButtons(document);
|
|
1771
|
+
bindRepairPromptButtons(document);
|
|
1772
|
+
bindLessonSedimentationButtons(document);
|
|
1318
1773
|
document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
|
|
1319
1774
|
e.preventDefault();
|
|
1320
1775
|
const lessonId = el.dataset.openLessonDrawer;
|
|
@@ -1404,6 +1859,7 @@ function renderDrawerContent(taskId) {
|
|
|
1404
1859
|
<div>
|
|
1405
1860
|
<h2>${escapeHtml(task.title)}</h2>
|
|
1406
1861
|
<p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
|
|
1862
|
+
${taskCopyButton(task, "detail-copy")}
|
|
1407
1863
|
</div>
|
|
1408
1864
|
<button class="btn-close" data-close-drawer>×</button>
|
|
1409
1865
|
</div>
|
|
@@ -1416,12 +1872,16 @@ function renderDrawerContent(taskId) {
|
|
|
1416
1872
|
|
|
1417
1873
|
const body = `
|
|
1418
1874
|
<div class="task-drawer-body stack">
|
|
1419
|
-
<div
|
|
1420
|
-
<div
|
|
1421
|
-
|
|
1875
|
+
<div class="drawer-task-summary">
|
|
1876
|
+
<div>
|
|
1877
|
+
<span>${t("statOverall")}</span>
|
|
1878
|
+
<strong>${task.completion}%</strong>
|
|
1879
|
+
</div>
|
|
1880
|
+
<a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
|
|
1422
1881
|
</div>
|
|
1423
1882
|
${taskStateSummary(task)}
|
|
1424
|
-
${reviewActionPanel(task)}
|
|
1883
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
1884
|
+
${lessonCandidatePanel(task, { context: "drawer" })}
|
|
1425
1885
|
${timeline}
|
|
1426
1886
|
${documents}
|
|
1427
1887
|
${findings}
|
|
@@ -1445,18 +1905,145 @@ function openDrawer(taskId) {
|
|
|
1445
1905
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
1446
1906
|
openDrawer(taskId);
|
|
1447
1907
|
}));
|
|
1908
|
+
bindCopyTaskNameButtons(drawer);
|
|
1909
|
+
bindRepairPromptButtons(drawer);
|
|
1910
|
+
bindLessonSedimentationButtons(drawer);
|
|
1448
1911
|
drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
|
|
1449
1912
|
}
|
|
1450
1913
|
|
|
1914
|
+
function bindCopyTaskNameButtons(root) {
|
|
1915
|
+
root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
1916
|
+
event.preventDefault();
|
|
1917
|
+
event.stopPropagation();
|
|
1918
|
+
const taskName = button.dataset.copyTaskName || "";
|
|
1919
|
+
const defaultText = t("copyTaskNameShort");
|
|
1920
|
+
try {
|
|
1921
|
+
await copyText(taskName);
|
|
1922
|
+
button.textContent = t("copyTaskNameSuccess");
|
|
1923
|
+
} catch {
|
|
1924
|
+
button.textContent = t("copyTaskNameFailed");
|
|
1925
|
+
}
|
|
1926
|
+
window.setTimeout(() => {
|
|
1927
|
+
button.textContent = defaultText;
|
|
1928
|
+
}, 1400);
|
|
1929
|
+
}));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function bindRepairPromptButtons(root) {
|
|
1933
|
+
root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
1934
|
+
event.preventDefault();
|
|
1935
|
+
event.stopPropagation();
|
|
1936
|
+
const prompt = button.dataset.repairPrompt || "";
|
|
1937
|
+
const defaultText = t("copyRepairPrompt");
|
|
1938
|
+
try {
|
|
1939
|
+
await copyText(prompt);
|
|
1940
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
1941
|
+
} catch {
|
|
1942
|
+
button.textContent = t("copyTaskNameFailed");
|
|
1943
|
+
}
|
|
1944
|
+
window.setTimeout(() => {
|
|
1945
|
+
button.textContent = defaultText;
|
|
1946
|
+
}, 1400);
|
|
1947
|
+
}));
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function bindLessonSedimentationButtons(root) {
|
|
1951
|
+
root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
1952
|
+
event.preventDefault();
|
|
1953
|
+
event.stopPropagation();
|
|
1954
|
+
const prompt = button.dataset.lessonPrompt || "";
|
|
1955
|
+
const defaultText = t("copyLessonPrompt");
|
|
1956
|
+
try {
|
|
1957
|
+
await copyText(prompt);
|
|
1958
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
1959
|
+
} catch {
|
|
1960
|
+
button.textContent = t("copyTaskNameFailed");
|
|
1961
|
+
}
|
|
1962
|
+
window.setTimeout(() => {
|
|
1963
|
+
button.textContent = defaultText;
|
|
1964
|
+
}, 1400);
|
|
1965
|
+
}));
|
|
1966
|
+
root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
1967
|
+
event.preventDefault();
|
|
1968
|
+
event.stopPropagation();
|
|
1969
|
+
await createLessonSedimentationFromDashboard(button);
|
|
1970
|
+
}));
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
async function createLessonSedimentationFromDashboard(button) {
|
|
1974
|
+
const taskId = button.dataset.createLessonSedimentation || "";
|
|
1975
|
+
const candidateId = button.dataset.candidateId || "";
|
|
1976
|
+
const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
|
|
1977
|
+
if (result) result.textContent = t("lessonTaskCreating");
|
|
1978
|
+
button.disabled = true;
|
|
1979
|
+
try {
|
|
1980
|
+
const response = await fetch("/api/tasks/lesson-sedimentation", {
|
|
1981
|
+
method: "POST",
|
|
1982
|
+
headers: {
|
|
1983
|
+
"content-type": "application/json",
|
|
1984
|
+
"x-harness-csrf": state.runtime?.csrfToken || "",
|
|
1985
|
+
},
|
|
1986
|
+
body: JSON.stringify({ taskId, candidateId }),
|
|
1987
|
+
});
|
|
1988
|
+
const payload = await response.json();
|
|
1989
|
+
if (!response.ok) throw payload;
|
|
1990
|
+
if (result) {
|
|
1991
|
+
result.innerHTML = lessonSedimentationSuccess(payload);
|
|
1992
|
+
bindLessonSedimentationButtons(result);
|
|
1993
|
+
result.scrollIntoView({ block: "center", inline: "nearest" });
|
|
1994
|
+
}
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
button.disabled = false;
|
|
1997
|
+
if (result) result.innerHTML = lessonSedimentationFailure(error);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function lessonSedimentationSuccess(payload) {
|
|
2002
|
+
const followUp = payload?.followUpTask || {};
|
|
2003
|
+
const prompt = payload?.prompt || "";
|
|
2004
|
+
const taskId = followUp.id || "";
|
|
2005
|
+
const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
|
|
2006
|
+
return `<div class="workbench-action-result success">
|
|
2007
|
+
<strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
|
|
2008
|
+
${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
2009
|
+
${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
|
|
2010
|
+
</div>`;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function lessonSedimentationFailure(error) {
|
|
2014
|
+
const message = error?.error || error?.message || t("lessonTaskCreateFailed");
|
|
2015
|
+
const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
|
|
2016
|
+
const details = error?.details || {};
|
|
2017
|
+
const existingTask = details.followUpTask || details.existingTask || "";
|
|
2018
|
+
return `<div class="workbench-action-result failed">
|
|
2019
|
+
<strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
|
|
2020
|
+
<span>${escapeHtml(message)}</span>
|
|
2021
|
+
${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
2022
|
+
${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
|
|
2023
|
+
</div>`;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
async function copyText(text) {
|
|
2027
|
+
if (navigator.clipboard?.writeText) {
|
|
2028
|
+
await navigator.clipboard.writeText(text);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
const textarea = document.createElement("textarea");
|
|
2032
|
+
textarea.value = text;
|
|
2033
|
+
textarea.setAttribute("readonly", "");
|
|
2034
|
+
textarea.style.position = "fixed";
|
|
2035
|
+
textarea.style.left = "-9999px";
|
|
2036
|
+
document.body.appendChild(textarea);
|
|
2037
|
+
textarea.select();
|
|
2038
|
+
const copied = document.execCommand("copy");
|
|
2039
|
+
textarea.remove();
|
|
2040
|
+
if (!copied) throw new Error("copy failed");
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1451
2043
|
function renderLessonDrawerContent(lessonId) {
|
|
1452
|
-
const
|
|
1453
|
-
const row = (lessonTable?.rows || []).find((r) => {
|
|
1454
|
-
const cells = r.cells || {};
|
|
1455
|
-
const id = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
|
|
1456
|
-
return id === lessonId;
|
|
1457
|
-
});
|
|
2044
|
+
const lesson = lessonDocuments().find((item) => item.id === lessonId);
|
|
1458
2045
|
|
|
1459
|
-
if (!
|
|
2046
|
+
if (!lesson) {
|
|
1460
2047
|
return `<div class="task-drawer-header">
|
|
1461
2048
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
1462
2049
|
<button class="btn-close" data-close-drawer>×</button>
|
|
@@ -1466,23 +2053,13 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
1466
2053
|
</div>`;
|
|
1467
2054
|
}
|
|
1468
2055
|
|
|
1469
|
-
const
|
|
1470
|
-
const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
|
|
1471
|
-
const docPath = cells["\u8be6\u60c5\u6587\u6863"] || cells.Document || cells.document || "";
|
|
1472
|
-
|
|
1473
|
-
let doc = null;
|
|
1474
|
-
if (docPath) {
|
|
1475
|
-
doc = findDocument(docPath);
|
|
1476
|
-
}
|
|
1477
|
-
if (!doc) {
|
|
1478
|
-
doc = (bundle.documents?.documents || []).find((d) => d.path.includes(lessonId) || d.path.endsWith(`${lessonId}.md`));
|
|
1479
|
-
}
|
|
2056
|
+
const doc = lesson.doc || findDocument(lesson.path);
|
|
1480
2057
|
|
|
1481
2058
|
const header = `
|
|
1482
2059
|
<div class="task-drawer-header">
|
|
1483
2060
|
<div>
|
|
1484
2061
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
1485
|
-
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(
|
|
2062
|
+
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
|
|
1486
2063
|
</div>
|
|
1487
2064
|
<button class="btn-close" data-close-drawer>×</button>
|
|
1488
2065
|
</div>
|
|
@@ -1492,16 +2069,10 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
1492
2069
|
if (doc && doc.content) {
|
|
1493
2070
|
markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
|
|
1494
2071
|
} else {
|
|
1495
|
-
const rowsHtml = Object.entries(cells)
|
|
1496
|
-
.map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
|
|
1497
|
-
.join("");
|
|
1498
2072
|
markdownBody = `
|
|
1499
2073
|
<div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
|
|
1500
2074
|
<p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
|
|
1501
2075
|
</div>
|
|
1502
|
-
<table class="rendered-table" style="width: 100%;">
|
|
1503
|
-
<tbody>${rowsHtml}</tbody>
|
|
1504
|
-
</table>
|
|
1505
2076
|
`;
|
|
1506
2077
|
}
|
|
1507
2078
|
|