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
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
function reviewQueue() {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
2
|
+
ensureReviewQueueState();
|
|
3
|
+
const tabs = reviewQueueTabs();
|
|
4
|
+
const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
|
|
5
|
+
const baseTasks = reviewQueueBaseTasks(activeTab);
|
|
6
|
+
const reasonOptions = reviewReasonOptions(baseTasks);
|
|
7
|
+
normalizeReviewReasonFilter(reasonOptions);
|
|
8
|
+
const tasks = reviewFilteredTasks(baseTasks);
|
|
9
|
+
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
10
|
+
const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
|
|
11
|
+
const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
|
|
6
12
|
return `<div class="dashboard-grid review-queue-page">
|
|
7
13
|
<main class="dashboard-main stack">
|
|
8
14
|
<section class="flow-panel">
|
|
@@ -12,10 +18,36 @@ function reviewQueue() {
|
|
|
12
18
|
<h2>${t("reviewQueue")}</h2>
|
|
13
19
|
<p class="subtle">${t("reviewQueueSubtitle")}</p>
|
|
14
20
|
</div>
|
|
15
|
-
<span class="subtle">${
|
|
21
|
+
<span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
|
|
16
22
|
</div>
|
|
17
|
-
<div class="
|
|
18
|
-
${
|
|
23
|
+
<div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
|
|
24
|
+
${tabs.map((tab) => reviewQueueTab(tab)).join("")}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="review-queue-toolbar">
|
|
27
|
+
<div class="input-group">
|
|
28
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="select-group">
|
|
31
|
+
<label>${t("reasonFilter")}</label>
|
|
32
|
+
<select data-review-reason-filter aria-label="${t("reasonFilter")}">
|
|
33
|
+
<option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
|
|
34
|
+
${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
|
|
35
|
+
</select>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="select-group">
|
|
38
|
+
<label>${t("sortBy")}</label>
|
|
39
|
+
<select data-review-sort aria-label="${t("sortBy")}">
|
|
40
|
+
${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
|
|
41
|
+
</select>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
|
|
45
|
+
<div class="review-queue-list">
|
|
46
|
+
${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="review-queue-pager">
|
|
50
|
+
${pager("review", page, pageCount)}
|
|
19
51
|
</div>
|
|
20
52
|
</section>
|
|
21
53
|
</main>
|
|
@@ -23,52 +55,239 @@ function reviewQueue() {
|
|
|
23
55
|
<section class="side-panel review-queue-summary">
|
|
24
56
|
<h3>${t("reviewQueue")}</h3>
|
|
25
57
|
<div class="review-queue-stats">
|
|
26
|
-
${metric(
|
|
27
|
-
${metric(t("reviewBlockedQueue"), blocked)}
|
|
28
|
-
${metric(t("reviewConfirmedQueue"), confirmed)}
|
|
58
|
+
${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
|
|
29
59
|
</div>
|
|
30
60
|
</section>
|
|
31
61
|
<section class="side-panel">
|
|
32
|
-
<h3>${
|
|
33
|
-
<p>${escapeHtml(
|
|
62
|
+
<h3>${escapeHtml(activeTab.label)}</h3>
|
|
63
|
+
<p>${escapeHtml(activeTab.description)}</p>
|
|
64
|
+
<dl class="review-queue-contract">
|
|
65
|
+
<div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
|
|
66
|
+
<div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
|
|
67
|
+
</dl>
|
|
34
68
|
</section>
|
|
35
69
|
</aside>
|
|
36
70
|
</div>`;
|
|
37
71
|
}
|
|
38
72
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
function ensureReviewQueueState() {
|
|
74
|
+
if (!state.reviewQueueTab) state.reviewQueueTab = "review";
|
|
75
|
+
if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
|
|
76
|
+
if (!state.reviewSort) state.reviewSort = "queue";
|
|
77
|
+
if (!state.reviewQueuePage) state.reviewQueuePage = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function reviewQueueTabs() {
|
|
81
|
+
return [
|
|
82
|
+
{ id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
|
|
83
|
+
{ id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
|
|
84
|
+
{ id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
|
|
85
|
+
{ id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
|
|
86
|
+
{ id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
|
|
87
|
+
{ id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reviewQueueTab(tab) {
|
|
92
|
+
const active = tab.id === state.reviewQueueTab;
|
|
93
|
+
const count = reviewQueueBaseTasks(tab).length;
|
|
94
|
+
return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
|
|
95
|
+
<span>${escapeHtml(tab.label)}</span>
|
|
96
|
+
<strong>${count}</strong>
|
|
97
|
+
</button>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reviewSortOptions() {
|
|
101
|
+
return [
|
|
102
|
+
{ id: "queue", label: t("sortQueuePriority") },
|
|
103
|
+
{ id: "newest", label: t("sortNewest") },
|
|
104
|
+
{ id: "oldest", label: t("sortOldest") },
|
|
105
|
+
{ id: "id", label: t("sortTaskId") },
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reviewQueueBaseTasks(tab) {
|
|
110
|
+
return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function taskMatchesReviewTab(task, tab) {
|
|
114
|
+
const queues = reviewTaskQueues(task);
|
|
115
|
+
return (tab.queues || []).some((queue) => queues.includes(queue));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reviewTaskQueues(task) {
|
|
119
|
+
return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function reviewReasonOptions(tasks) {
|
|
123
|
+
return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeReviewReasonFilter(reasonOptions) {
|
|
127
|
+
const current = state.reviewReasonFilter || "all";
|
|
128
|
+
if (current === "all") return;
|
|
129
|
+
if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function reviewFilteredTasks(tasks) {
|
|
133
|
+
const query = state.query.trim().toLowerCase();
|
|
134
|
+
const reasonFilter = state.reviewReasonFilter || "all";
|
|
135
|
+
return [...tasks]
|
|
136
|
+
.filter((task) => {
|
|
137
|
+
if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
|
|
138
|
+
if (!query) return true;
|
|
139
|
+
return [
|
|
140
|
+
task.id,
|
|
141
|
+
task.shortId,
|
|
142
|
+
task.title,
|
|
143
|
+
task.module,
|
|
144
|
+
task.inferredModule,
|
|
145
|
+
task.state,
|
|
146
|
+
task.lifecycleState,
|
|
147
|
+
task.reviewStatus,
|
|
148
|
+
task.closeoutStatus,
|
|
149
|
+
...(task.taskQueues || []),
|
|
150
|
+
...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
|
|
151
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
152
|
+
})
|
|
153
|
+
.sort(reviewTaskSort);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function reviewTaskSort(left, right) {
|
|
157
|
+
if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
|
|
158
|
+
if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
|
|
159
|
+
if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
160
|
+
return reviewPriorityRank(left) - reviewPriorityRank(right)
|
|
161
|
+
|| compareTasksByTimeForOrder(left, right, "desc")
|
|
162
|
+
|| stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compareTasksByTimeForOrder(left, right, order) {
|
|
166
|
+
const previous = state.taskSortOrder;
|
|
167
|
+
state.taskSortOrder = order;
|
|
168
|
+
const result = compareTasksByTime(left, right);
|
|
169
|
+
state.taskSortOrder = previous;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function reviewPriorityRank(task) {
|
|
174
|
+
const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
175
|
+
const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
|
|
176
|
+
const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
|
|
177
|
+
const queues = reviewTaskQueues(task);
|
|
178
|
+
const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
|
|
179
|
+
return Math.min(reasonRank, taskQueueRank);
|
|
43
180
|
}
|
|
44
181
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
return `${rank}:${task.id}`;
|
|
182
|
+
function reviewTruthyCount(tasks, key) {
|
|
183
|
+
return tasks.filter((task) => task[key] === true).length;
|
|
48
184
|
}
|
|
49
185
|
|
|
50
|
-
function reviewQueueCard(task) {
|
|
186
|
+
function reviewQueueCard(task, tab) {
|
|
51
187
|
const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
|
|
188
|
+
const reasons = task.queueReasons || [];
|
|
189
|
+
const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
|
|
190
|
+
const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
|
|
191
|
+
const displayId = task.shortId || taskFolderName(task) || task.id;
|
|
52
192
|
return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
53
193
|
<div class="card-header">
|
|
54
|
-
<span class="card-id"
|
|
194
|
+
<span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
|
|
55
195
|
${tag(task.reviewStatus || "missing")}
|
|
196
|
+
${reviewTaskQueues(task).map(tag).join("")}
|
|
56
197
|
</div>
|
|
57
198
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
58
199
|
<div class="card-meta">
|
|
59
200
|
<span>${tag(task.lifecycleState || "unknown")}</span>
|
|
60
201
|
<span>${tag(task.closeoutStatus || "missing")}</span>
|
|
61
202
|
<span>${openMaterial} ${t("openFindings")}</span>
|
|
203
|
+
<span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
|
|
204
|
+
<span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
|
|
62
205
|
</div>
|
|
63
206
|
<p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
|
|
207
|
+
${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
|
|
208
|
+
${lessonActions}
|
|
64
209
|
<div class="review-queue-actions">
|
|
65
210
|
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
66
211
|
<a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
|
|
67
212
|
<button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
213
|
+
${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
|
|
68
214
|
</div>
|
|
69
215
|
</article>`;
|
|
70
216
|
}
|
|
71
217
|
|
|
218
|
+
function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
219
|
+
const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
|
|
220
|
+
if (!candidates.length) return "";
|
|
221
|
+
const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
|
|
222
|
+
const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
|
|
223
|
+
const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
|
|
224
|
+
return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
|
|
225
|
+
<div class="lesson-candidate-panel-head">
|
|
226
|
+
<div>
|
|
227
|
+
<p class="eyebrow">${t("lessonCandidates")}</p>
|
|
228
|
+
<h3>${t("lessonSedimentationActions")}</h3>
|
|
229
|
+
</div>
|
|
230
|
+
<span class="tag">${visibleCandidates.length}/${candidates.length}</span>
|
|
231
|
+
</div>
|
|
232
|
+
${staticNote}
|
|
233
|
+
<div class="lesson-candidate-actions">
|
|
234
|
+
${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
|
|
235
|
+
</div>
|
|
236
|
+
${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
|
|
237
|
+
</section>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function lessonCandidateAction(task, candidate) {
|
|
241
|
+
const followUp = String(candidate.followUpTask || "").trim();
|
|
242
|
+
const hasFollowUp = followUp && !/^pending$/i.test(followUp);
|
|
243
|
+
const prompt = lessonSedimentationPrompt(task, candidate);
|
|
244
|
+
return `<div class="lesson-candidate-action">
|
|
245
|
+
<div class="lesson-candidate-main">
|
|
246
|
+
<strong>${escapeHtml(candidate.id)}</strong>
|
|
247
|
+
<span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
|
|
248
|
+
<small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
|
|
249
|
+
</div>
|
|
250
|
+
<span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
|
|
251
|
+
<div class="lesson-candidate-command-row">
|
|
252
|
+
${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
|
|
253
|
+
<button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
|
|
254
|
+
<button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
|
|
255
|
+
</div>
|
|
256
|
+
</div>`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function lessonSedimentationPrompt(task, candidate) {
|
|
260
|
+
return [
|
|
261
|
+
"You are executing a lesson sedimentation follow-up task.",
|
|
262
|
+
"",
|
|
263
|
+
`Source task: ${task.id}`,
|
|
264
|
+
`Source candidate: ${candidate.id} - ${candidate.title || ""}`,
|
|
265
|
+
`Candidate scope: ${candidate.scope || "unspecified"}`,
|
|
266
|
+
`Candidate module key: ${candidate.moduleKey || "n/a"}`,
|
|
267
|
+
`Detail artifact: ${candidate.detailArtifact || "not provided"}`,
|
|
268
|
+
`Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
|
|
269
|
+
`Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
|
|
270
|
+
`Promotion target: ${candidate.promotionTarget || "unspecified"}`,
|
|
271
|
+
`Conflict check: ${candidate.conflictCheck || "pending"}`,
|
|
272
|
+
`Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
|
|
273
|
+
"",
|
|
274
|
+
"Instructions:",
|
|
275
|
+
"1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
|
|
276
|
+
"2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
|
|
277
|
+
"3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
|
|
278
|
+
"4. Check conflicts against existing lessons and standards.",
|
|
279
|
+
"5. Propose the smallest diff first.",
|
|
280
|
+
"6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
|
|
281
|
+
].join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function reviewReason(reason) {
|
|
285
|
+
return `<div class="review-reason">
|
|
286
|
+
<strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
|
|
287
|
+
<span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
|
|
288
|
+
</div>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
72
291
|
function firstUsefulLine(text) {
|
|
73
292
|
return String(text || "")
|
|
74
293
|
.split(/\n+/)
|
|
@@ -123,6 +342,6 @@ function reviewDocPanel(key, doc, fallbackPath = "") {
|
|
|
123
342
|
</div>
|
|
124
343
|
${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
|
|
125
344
|
</div>
|
|
126
|
-
<div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div>
|
|
345
|
+
<div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
|
|
127
346
|
</section>`;
|
|
128
347
|
}
|
|
@@ -138,25 +138,39 @@ function pager(kind, page, pageCount, group = "") {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function lessonPanel() {
|
|
141
|
-
const lessons = (
|
|
142
|
-
.filter((table) => table.kind === "lessons-ssot")
|
|
143
|
-
.flatMap((table) => table.rows);
|
|
141
|
+
const lessons = lessonDocuments();
|
|
144
142
|
return `<section class="lesson-panel">
|
|
145
143
|
<div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
|
|
146
144
|
<div class="lesson-list" style="padding-top: 10px;">
|
|
147
|
-
${lessons.map((
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
|
|
152
|
-
<strong>${escapeHtml(lessonId)}</strong>
|
|
153
|
-
<p>${escapeHtml(summary)}</p>
|
|
145
|
+
${lessons.map((lesson) => {
|
|
146
|
+
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
|
|
147
|
+
<strong>${escapeHtml(lesson.id)}</strong>
|
|
148
|
+
<p>${escapeHtml(lesson.title || lesson.path)}</p>
|
|
154
149
|
</div>`;
|
|
155
150
|
}).join("") || emptyState(t("noLessons"))}
|
|
156
151
|
</div>
|
|
157
152
|
</section>`;
|
|
158
153
|
}
|
|
159
154
|
|
|
155
|
+
function lessonDocuments() {
|
|
156
|
+
return (bundle.documents?.documents || [])
|
|
157
|
+
.filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
|
|
158
|
+
.map((doc) => {
|
|
159
|
+
const id = lessonIdFromDocument(doc);
|
|
160
|
+
return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
|
|
161
|
+
})
|
|
162
|
+
.filter((lesson) => lesson.id)
|
|
163
|
+
.sort((left, right) => String(right.id).localeCompare(String(left.id)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function lessonIdFromDocument(doc) {
|
|
167
|
+
const content = doc?.content || "";
|
|
168
|
+
const path = doc?.path || "";
|
|
169
|
+
return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
170
|
+
|| path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
171
|
+
|| "";
|
|
172
|
+
}
|
|
173
|
+
|
|
160
174
|
function healthPanel() {
|
|
161
175
|
const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
|
|
162
176
|
return `<section class="health-panel">
|
|
@@ -28,6 +28,13 @@ function bind() {
|
|
|
28
28
|
localStorage.setItem("harness.taskLayout", state.taskLayout);
|
|
29
29
|
app();
|
|
30
30
|
}));
|
|
31
|
+
document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
|
|
32
|
+
state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
|
|
33
|
+
localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
|
|
34
|
+
state.taskPageByGroup = {};
|
|
35
|
+
state.taskGroupPage = 1;
|
|
36
|
+
app();
|
|
37
|
+
}));
|
|
31
38
|
document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
32
39
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
33
40
|
app();
|
|
@@ -42,11 +49,27 @@ function bind() {
|
|
|
42
49
|
state.warningPage = 1;
|
|
43
50
|
app();
|
|
44
51
|
}));
|
|
52
|
+
document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
|
|
53
|
+
state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
|
|
54
|
+
state.reviewQueuePage = 1;
|
|
55
|
+
app();
|
|
56
|
+
}));
|
|
57
|
+
document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
|
|
58
|
+
state.reviewReasonFilter = select.value || "all";
|
|
59
|
+
state.reviewQueuePage = 1;
|
|
60
|
+
app();
|
|
61
|
+
}));
|
|
62
|
+
document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
|
|
63
|
+
state.reviewSort = select.value || "queue";
|
|
64
|
+
state.reviewQueuePage = 1;
|
|
65
|
+
app();
|
|
66
|
+
}));
|
|
45
67
|
document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
|
|
46
68
|
const page = Math.max(1, Number(button.dataset.page) || 1);
|
|
47
69
|
if (button.dataset.pageKind === "warning") state.warningPage = page;
|
|
48
70
|
if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
|
|
49
71
|
if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
|
|
72
|
+
if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
|
|
50
73
|
app();
|
|
51
74
|
}));
|
|
52
75
|
document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
|
|
@@ -71,6 +94,9 @@ function bind() {
|
|
|
71
94
|
const taskId = el.dataset.openDrawer;
|
|
72
95
|
openDrawer(taskId);
|
|
73
96
|
}));
|
|
97
|
+
bindCopyTaskNameButtons(document);
|
|
98
|
+
bindRepairPromptButtons(document);
|
|
99
|
+
bindLessonSedimentationButtons(document);
|
|
74
100
|
document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
|
|
75
101
|
e.preventDefault();
|
|
76
102
|
const lessonId = el.dataset.openLessonDrawer;
|
|
@@ -160,6 +186,7 @@ function renderDrawerContent(taskId) {
|
|
|
160
186
|
<div>
|
|
161
187
|
<h2>${escapeHtml(task.title)}</h2>
|
|
162
188
|
<p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
|
|
189
|
+
${taskCopyButton(task, "detail-copy")}
|
|
163
190
|
</div>
|
|
164
191
|
<button class="btn-close" data-close-drawer>×</button>
|
|
165
192
|
</div>
|
|
@@ -172,12 +199,16 @@ function renderDrawerContent(taskId) {
|
|
|
172
199
|
|
|
173
200
|
const body = `
|
|
174
201
|
<div class="task-drawer-body stack">
|
|
175
|
-
<div
|
|
176
|
-
<div
|
|
177
|
-
|
|
202
|
+
<div class="drawer-task-summary">
|
|
203
|
+
<div>
|
|
204
|
+
<span>${t("statOverall")}</span>
|
|
205
|
+
<strong>${task.completion}%</strong>
|
|
206
|
+
</div>
|
|
207
|
+
<a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
|
|
178
208
|
</div>
|
|
179
209
|
${taskStateSummary(task)}
|
|
180
210
|
${reviewActionPanel(task, { mode: "summary" })}
|
|
211
|
+
${lessonCandidatePanel(task, { context: "drawer" })}
|
|
181
212
|
${timeline}
|
|
182
213
|
${documents}
|
|
183
214
|
${findings}
|
|
@@ -201,18 +232,145 @@ function openDrawer(taskId) {
|
|
|
201
232
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
202
233
|
openDrawer(taskId);
|
|
203
234
|
}));
|
|
235
|
+
bindCopyTaskNameButtons(drawer);
|
|
236
|
+
bindRepairPromptButtons(drawer);
|
|
237
|
+
bindLessonSedimentationButtons(drawer);
|
|
204
238
|
drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
|
|
205
239
|
}
|
|
206
240
|
|
|
241
|
+
function bindCopyTaskNameButtons(root) {
|
|
242
|
+
root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
event.stopPropagation();
|
|
245
|
+
const taskName = button.dataset.copyTaskName || "";
|
|
246
|
+
const defaultText = t("copyTaskNameShort");
|
|
247
|
+
try {
|
|
248
|
+
await copyText(taskName);
|
|
249
|
+
button.textContent = t("copyTaskNameSuccess");
|
|
250
|
+
} catch {
|
|
251
|
+
button.textContent = t("copyTaskNameFailed");
|
|
252
|
+
}
|
|
253
|
+
window.setTimeout(() => {
|
|
254
|
+
button.textContent = defaultText;
|
|
255
|
+
}, 1400);
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function bindRepairPromptButtons(root) {
|
|
260
|
+
root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
event.stopPropagation();
|
|
263
|
+
const prompt = button.dataset.repairPrompt || "";
|
|
264
|
+
const defaultText = t("copyRepairPrompt");
|
|
265
|
+
try {
|
|
266
|
+
await copyText(prompt);
|
|
267
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
268
|
+
} catch {
|
|
269
|
+
button.textContent = t("copyTaskNameFailed");
|
|
270
|
+
}
|
|
271
|
+
window.setTimeout(() => {
|
|
272
|
+
button.textContent = defaultText;
|
|
273
|
+
}, 1400);
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function bindLessonSedimentationButtons(root) {
|
|
278
|
+
root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
event.stopPropagation();
|
|
281
|
+
const prompt = button.dataset.lessonPrompt || "";
|
|
282
|
+
const defaultText = t("copyLessonPrompt");
|
|
283
|
+
try {
|
|
284
|
+
await copyText(prompt);
|
|
285
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
286
|
+
} catch {
|
|
287
|
+
button.textContent = t("copyTaskNameFailed");
|
|
288
|
+
}
|
|
289
|
+
window.setTimeout(() => {
|
|
290
|
+
button.textContent = defaultText;
|
|
291
|
+
}, 1400);
|
|
292
|
+
}));
|
|
293
|
+
root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
await createLessonSedimentationFromDashboard(button);
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function createLessonSedimentationFromDashboard(button) {
|
|
301
|
+
const taskId = button.dataset.createLessonSedimentation || "";
|
|
302
|
+
const candidateId = button.dataset.candidateId || "";
|
|
303
|
+
const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
|
|
304
|
+
if (result) result.textContent = t("lessonTaskCreating");
|
|
305
|
+
button.disabled = true;
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch("/api/tasks/lesson-sedimentation", {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
"content-type": "application/json",
|
|
311
|
+
"x-harness-csrf": state.runtime?.csrfToken || "",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify({ taskId, candidateId }),
|
|
314
|
+
});
|
|
315
|
+
const payload = await response.json();
|
|
316
|
+
if (!response.ok) throw payload;
|
|
317
|
+
if (result) {
|
|
318
|
+
result.innerHTML = lessonSedimentationSuccess(payload);
|
|
319
|
+
bindLessonSedimentationButtons(result);
|
|
320
|
+
result.scrollIntoView({ block: "center", inline: "nearest" });
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
button.disabled = false;
|
|
324
|
+
if (result) result.innerHTML = lessonSedimentationFailure(error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function lessonSedimentationSuccess(payload) {
|
|
329
|
+
const followUp = payload?.followUpTask || {};
|
|
330
|
+
const prompt = payload?.prompt || "";
|
|
331
|
+
const taskId = followUp.id || "";
|
|
332
|
+
const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
|
|
333
|
+
return `<div class="workbench-action-result success">
|
|
334
|
+
<strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
|
|
335
|
+
${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
336
|
+
${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
|
|
337
|
+
</div>`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function lessonSedimentationFailure(error) {
|
|
341
|
+
const message = error?.error || error?.message || t("lessonTaskCreateFailed");
|
|
342
|
+
const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
|
|
343
|
+
const details = error?.details || {};
|
|
344
|
+
const existingTask = details.followUpTask || details.existingTask || "";
|
|
345
|
+
return `<div class="workbench-action-result failed">
|
|
346
|
+
<strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
|
|
347
|
+
<span>${escapeHtml(message)}</span>
|
|
348
|
+
${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
349
|
+
${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
|
|
350
|
+
</div>`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function copyText(text) {
|
|
354
|
+
if (navigator.clipboard?.writeText) {
|
|
355
|
+
await navigator.clipboard.writeText(text);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const textarea = document.createElement("textarea");
|
|
359
|
+
textarea.value = text;
|
|
360
|
+
textarea.setAttribute("readonly", "");
|
|
361
|
+
textarea.style.position = "fixed";
|
|
362
|
+
textarea.style.left = "-9999px";
|
|
363
|
+
document.body.appendChild(textarea);
|
|
364
|
+
textarea.select();
|
|
365
|
+
const copied = document.execCommand("copy");
|
|
366
|
+
textarea.remove();
|
|
367
|
+
if (!copied) throw new Error("copy failed");
|
|
368
|
+
}
|
|
369
|
+
|
|
207
370
|
function renderLessonDrawerContent(lessonId) {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const id = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
|
|
212
|
-
return id === lessonId;
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!row) {
|
|
371
|
+
const lesson = lessonDocuments().find((item) => item.id === lessonId);
|
|
372
|
+
|
|
373
|
+
if (!lesson) {
|
|
216
374
|
return `<div class="task-drawer-header">
|
|
217
375
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
218
376
|
<button class="btn-close" data-close-drawer>×</button>
|
|
@@ -222,23 +380,13 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
222
380
|
</div>`;
|
|
223
381
|
}
|
|
224
382
|
|
|
225
|
-
const
|
|
226
|
-
const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
|
|
227
|
-
const docPath = cells["\u8be6\u60c5\u6587\u6863"] || cells.Document || cells.document || "";
|
|
228
|
-
|
|
229
|
-
let doc = null;
|
|
230
|
-
if (docPath) {
|
|
231
|
-
doc = findDocument(docPath);
|
|
232
|
-
}
|
|
233
|
-
if (!doc) {
|
|
234
|
-
doc = (bundle.documents?.documents || []).find((d) => d.path.includes(lessonId) || d.path.endsWith(`${lessonId}.md`));
|
|
235
|
-
}
|
|
383
|
+
const doc = lesson.doc || findDocument(lesson.path);
|
|
236
384
|
|
|
237
385
|
const header = `
|
|
238
386
|
<div class="task-drawer-header">
|
|
239
387
|
<div>
|
|
240
388
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
241
|
-
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(
|
|
389
|
+
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
|
|
242
390
|
</div>
|
|
243
391
|
<button class="btn-close" data-close-drawer>×</button>
|
|
244
392
|
</div>
|
|
@@ -248,16 +396,10 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
248
396
|
if (doc && doc.content) {
|
|
249
397
|
markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
|
|
250
398
|
} else {
|
|
251
|
-
const rowsHtml = Object.entries(cells)
|
|
252
|
-
.map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
|
|
253
|
-
.join("");
|
|
254
399
|
markdownBody = `
|
|
255
400
|
<div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
|
|
256
401
|
<p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
|
|
257
402
|
</div>
|
|
258
|
-
<table class="rendered-table" style="width: 100%;">
|
|
259
|
-
<tbody>${rowsHtml}</tbody>
|
|
260
|
-
</table>
|
|
261
403
|
`;
|
|
262
404
|
}
|
|
263
405
|
|