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
|
@@ -8,6 +8,59 @@ function stateToColorVar(state) {
|
|
|
8
8
|
return map[state] || "--muted";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function taskSortLabel() {
|
|
12
|
+
return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function taskDateKey(task) {
|
|
16
|
+
const source = `${task.shortId || ""} ${task.id || ""}`.trim();
|
|
17
|
+
const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const year = Number(match[1]);
|
|
20
|
+
const month = Number(match[2]);
|
|
21
|
+
const day = Number(match[3] || "1");
|
|
22
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
23
|
+
return Date.UTC(year, month - 1, day);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stableTaskLabel(task) {
|
|
27
|
+
return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compareTasksByTime(left, right) {
|
|
31
|
+
const leftDate = taskDateKey(left);
|
|
32
|
+
const rightDate = taskDateKey(right);
|
|
33
|
+
if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
|
|
34
|
+
return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
|
|
35
|
+
}
|
|
36
|
+
if (leftDate !== null && rightDate === null) return -1;
|
|
37
|
+
if (leftDate === null && rightDate !== null) return 1;
|
|
38
|
+
return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortTasksByTime(tasks) {
|
|
42
|
+
return [...tasks].sort(compareTasksByTime);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function taskFolderName(task) {
|
|
46
|
+
const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
|
|
47
|
+
const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
|
|
48
|
+
return task?.shortId || fromPath || fromId || task?.title || "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function taskCopyButton(task, extraClass = "") {
|
|
52
|
+
const folderName = taskFolderName(task);
|
|
53
|
+
return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
|
|
54
|
+
${t("copyTaskNameShort")}
|
|
55
|
+
</button>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function taskGroupTimeKey(group) {
|
|
59
|
+
const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
|
|
60
|
+
if (!match) return null;
|
|
61
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
11
64
|
function taskToolbarCard(filteredCount) {
|
|
12
65
|
return `<section class="sidebar-card">
|
|
13
66
|
<h3>${t("filterTitle")}</h3>
|
|
@@ -39,6 +92,17 @@ function taskToolbarCard(filteredCount) {
|
|
|
39
92
|
</button>
|
|
40
93
|
</div>
|
|
41
94
|
</div>
|
|
95
|
+
<div class="select-group">
|
|
96
|
+
<label>${t("sortByTime")}</label>
|
|
97
|
+
<div class="layout-toggle-group sort-toggle-group">
|
|
98
|
+
<button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
|
|
99
|
+
${t("sortNewest")}
|
|
100
|
+
</button>
|
|
101
|
+
<button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
|
|
102
|
+
${t("sortOldest")}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
42
106
|
<div class="search-stats">
|
|
43
107
|
${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
|
|
44
108
|
</div>
|
|
@@ -142,11 +206,12 @@ function taskRow(task) {
|
|
|
142
206
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
143
207
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
144
208
|
|
|
145
|
-
return `<
|
|
209
|
+
return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
146
210
|
<div class="row-accent-bar"></div>
|
|
147
211
|
<div class="row-main">
|
|
148
212
|
<strong>${escapeHtml(task.title)}</strong>
|
|
149
213
|
<span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
|
|
214
|
+
${taskCopyButton(task, "row-copy")}
|
|
150
215
|
</div>
|
|
151
216
|
<div class="row-status">${tag(task.state)}</div>
|
|
152
217
|
<div class="row-progress">
|
|
@@ -165,7 +230,7 @@ function taskRow(task) {
|
|
|
165
230
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
166
231
|
</span>
|
|
167
232
|
</div>
|
|
168
|
-
</
|
|
233
|
+
</article>`;
|
|
169
234
|
}
|
|
170
235
|
|
|
171
236
|
function taskIndex() {
|
|
@@ -204,7 +269,18 @@ function orderedTaskGroups(groups) {
|
|
|
204
269
|
if (group === "unknown") return 3;
|
|
205
270
|
return 4;
|
|
206
271
|
};
|
|
207
|
-
return Object.entries(groups).sort(([left], [right]) =>
|
|
272
|
+
return Object.entries(groups).sort(([left], [right]) => {
|
|
273
|
+
const rankDiff = rank(left) - rank(right);
|
|
274
|
+
if (rankDiff !== 0) return rankDiff;
|
|
275
|
+
const leftTime = taskGroupTimeKey(left);
|
|
276
|
+
const rightTime = taskGroupTimeKey(right);
|
|
277
|
+
if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
|
|
278
|
+
return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
|
|
279
|
+
}
|
|
280
|
+
if (leftTime !== null && rightTime === null) return -1;
|
|
281
|
+
if (leftTime === null && rightTime !== null) return 1;
|
|
282
|
+
return left.localeCompare(right);
|
|
283
|
+
});
|
|
208
284
|
}
|
|
209
285
|
|
|
210
286
|
function taskGroups(tasks) {
|
|
@@ -229,11 +305,12 @@ function taskGroups(tasks) {
|
|
|
229
305
|
}
|
|
230
306
|
|
|
231
307
|
function taskGroup(group, tasks) {
|
|
232
|
-
const
|
|
308
|
+
const orderedTasks = sortTasksByTime(tasks);
|
|
309
|
+
const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
|
|
233
310
|
const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
|
|
234
311
|
const start = (page - 1) * taskPageSize;
|
|
235
|
-
const visibleTasks =
|
|
236
|
-
const avgCompletion =
|
|
312
|
+
const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
|
|
313
|
+
const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
|
|
237
314
|
|
|
238
315
|
const isGrid = state.taskLayout === "grid";
|
|
239
316
|
const layoutClass = isGrid ? "task-card-grid" : "task-list";
|
|
@@ -250,7 +327,7 @@ function taskGroup(group, tasks) {
|
|
|
250
327
|
<div class="section-head">
|
|
251
328
|
<div>
|
|
252
329
|
<h2>${taskGroupLabel(group)}</h2>
|
|
253
|
-
<p class="subtle">${t("showing")} ${Math.min(start + 1,
|
|
330
|
+
<p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
|
|
254
331
|
</div>
|
|
255
332
|
<div class="group-actions">
|
|
256
333
|
<div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
|
|
@@ -275,10 +352,13 @@ function taskCard(task) {
|
|
|
275
352
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
276
353
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
277
354
|
|
|
278
|
-
return `<
|
|
355
|
+
return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
|
|
279
356
|
<div class="card-header">
|
|
280
357
|
<span class="card-id">${escapeHtml(task.id)}</span>
|
|
281
|
-
|
|
358
|
+
<div class="card-header-actions">
|
|
359
|
+
${taskCopyButton(task, "compact")}
|
|
360
|
+
${tag(task.state)}
|
|
361
|
+
</div>
|
|
282
362
|
</div>
|
|
283
363
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
284
364
|
<div class="card-meta">
|
|
@@ -301,7 +381,7 @@ function taskCard(task) {
|
|
|
301
381
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
302
382
|
</span>
|
|
303
383
|
</div>
|
|
304
|
-
</
|
|
384
|
+
</article>`;
|
|
305
385
|
}
|
|
306
386
|
|
|
307
387
|
function taskGroupLabel(group) {
|
|
@@ -316,248 +396,14 @@ function taskGroupLabel(group) {
|
|
|
316
396
|
|
|
317
397
|
function filteredTasks() {
|
|
318
398
|
const query = state.query.trim().toLowerCase();
|
|
319
|
-
return (bundle.status?.tasks || []).filter((task) => {
|
|
399
|
+
return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
|
|
320
400
|
const stateMatch = state.taskState === "all" || task.state === state.taskState;
|
|
321
401
|
if (!stateMatch) return false;
|
|
322
402
|
if (!query) return true;
|
|
323
403
|
return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
|
|
324
|
-
});
|
|
404
|
+
}));
|
|
325
405
|
}
|
|
326
406
|
|
|
327
407
|
function taskModuleKey(task) {
|
|
328
408
|
return task.module || task.inferredModule || "legacy-unclassified";
|
|
329
409
|
}
|
|
330
|
-
|
|
331
|
-
function taskDetail(route) {
|
|
332
|
-
const taskId = route.id;
|
|
333
|
-
const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
|
|
334
|
-
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
335
|
-
return `<main class="task-detail">
|
|
336
|
-
<nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
337
|
-
<section class="detail-hero">
|
|
338
|
-
<div>
|
|
339
|
-
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
340
|
-
<h2>${escapeHtml(task.title)}</h2>
|
|
341
|
-
<p>${escapeHtml(task.path)}</p>
|
|
342
|
-
</div>
|
|
343
|
-
<div class="detail-score">${task.completion}%</div>
|
|
344
|
-
</section>
|
|
345
|
-
${taskStateSummary(task)}
|
|
346
|
-
${phaseTimeline(task)}
|
|
347
|
-
<section class="detail-grid">
|
|
348
|
-
<article class="detail-main">
|
|
349
|
-
${taskDocumentLibrary(task, route.doc)}
|
|
350
|
-
</article>
|
|
351
|
-
<aside class="detail-side">
|
|
352
|
-
${reviewActionPanel(task, { mode: "summary" })}
|
|
353
|
-
${migrationSnapshotPanel(task)}
|
|
354
|
-
${openFindings(task)}
|
|
355
|
-
${evidenceList(task)}
|
|
356
|
-
${documentTabs(task)}
|
|
357
|
-
</aside>
|
|
358
|
-
</section>
|
|
359
|
-
</main>`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function taskStateSummary(task) {
|
|
363
|
-
return `<section class="task-state-summary">
|
|
364
|
-
<div>
|
|
365
|
-
<span>${t("legacyState")}</span>
|
|
366
|
-
${tag(task.state)}
|
|
367
|
-
</div>
|
|
368
|
-
<div>
|
|
369
|
-
<span>${t("lifecycleState")}</span>
|
|
370
|
-
${tag(task.lifecycleState || "unknown")}
|
|
371
|
-
</div>
|
|
372
|
-
<div>
|
|
373
|
-
<span>${t("reviewStatus")}</span>
|
|
374
|
-
${tag(task.reviewStatus || "missing")}
|
|
375
|
-
</div>
|
|
376
|
-
<div>
|
|
377
|
-
<span>${t("closeoutStatus")}</span>
|
|
378
|
-
${tag(task.closeoutStatus || "missing")}
|
|
379
|
-
</div>
|
|
380
|
-
</section>`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function phaseTimeline(task) {
|
|
384
|
-
return `<section class="phase-timeline">
|
|
385
|
-
<h2>${t("phaseTimeline")}</h2>
|
|
386
|
-
${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
|
|
387
|
-
<strong>${escapeHtml(phase.id)}</strong>
|
|
388
|
-
<span>${phase.completion}%</span>
|
|
389
|
-
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
390
|
-
${progressBar(phase.completion)}
|
|
391
|
-
</div>`).join("") || emptyState(t("noPhaseData"))}
|
|
392
|
-
</section>`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function taskDocSection(task, fileName, title, required) {
|
|
396
|
-
const doc = taskDocument(task, fileName);
|
|
397
|
-
if (!doc && !required) return "";
|
|
398
|
-
return `<section class="doc-section">
|
|
399
|
-
<div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
|
|
400
|
-
<div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
|
|
401
|
-
</section>`;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function taskDocumentLibrary(task, selectedTab) {
|
|
405
|
-
const docs = orderedTaskDocuments(task);
|
|
406
|
-
if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
|
|
407
|
-
const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
|
|
408
|
-
return `<section class="doc-library">
|
|
409
|
-
<div class="section-head">
|
|
410
|
-
<div>
|
|
411
|
-
<p class="eyebrow">${t("taskDocuments")}</p>
|
|
412
|
-
<h2>${escapeHtml(t("sourceDocuments"))}</h2>
|
|
413
|
-
</div>
|
|
414
|
-
<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
|
|
415
|
-
</div>
|
|
416
|
-
<div class="doc-accordion-list">
|
|
417
|
-
${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
|
|
418
|
-
</div>
|
|
419
|
-
</section>`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function orderedTaskDocuments(task) {
|
|
423
|
-
const docs = taskDocTabs
|
|
424
|
-
.map(([key, file]) => {
|
|
425
|
-
const doc = taskDocument(task, file);
|
|
426
|
-
if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
|
|
427
|
-
if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
|
|
428
|
-
return null;
|
|
429
|
-
})
|
|
430
|
-
.filter(Boolean);
|
|
431
|
-
const priority = taskDocumentPriority(task);
|
|
432
|
-
const rank = new Map(priority.map((key, index) => [key, index]));
|
|
433
|
-
return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function taskDocumentPriority(task) {
|
|
437
|
-
const stateName = task?.state || "";
|
|
438
|
-
const lifecycle = task?.lifecycleState || "";
|
|
439
|
-
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
440
|
-
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
441
|
-
}
|
|
442
|
-
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
443
|
-
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
444
|
-
}
|
|
445
|
-
if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
|
|
446
|
-
return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
|
|
447
|
-
}
|
|
448
|
-
return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function defaultTaskDocumentKey(task, docs) {
|
|
452
|
-
const priority = taskDocumentPriority(task);
|
|
453
|
-
return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function documentAccordion(item, open) {
|
|
457
|
-
return `<details class="doc-accordion" ${open ? "open" : ""}>
|
|
458
|
-
<summary>
|
|
459
|
-
<span>${escapeHtml(item.title)}</span>
|
|
460
|
-
<small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
|
|
461
|
-
</summary>
|
|
462
|
-
<div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
|
|
463
|
-
</details>`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function documentTabs(task) {
|
|
467
|
-
const docs = orderedTaskDocuments(task);
|
|
468
|
-
return `<section class="side-panel">
|
|
469
|
-
<h3>${t("sourceDocuments")}</h3>
|
|
470
|
-
${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
|
|
471
|
-
</section>`;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function selectedSourceDocument(task, tab) {
|
|
475
|
-
if (!tab) return "";
|
|
476
|
-
const match = taskDocTabs.find(([key]) => key === tab);
|
|
477
|
-
if (!match) return "";
|
|
478
|
-
const doc = taskDocument(task, match[1]);
|
|
479
|
-
if (!doc) return "";
|
|
480
|
-
return `<section class="doc-section selected-source">
|
|
481
|
-
<div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
|
|
482
|
-
<div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
|
|
483
|
-
</section>`;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function openFindings(task) {
|
|
487
|
-
const risks = task.risks || [];
|
|
488
|
-
return `<section class="side-panel">
|
|
489
|
-
<h3>${t("openFindings")}</h3>
|
|
490
|
-
${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
|
|
491
|
-
</section>`;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function migrationSnapshotPanel(task) {
|
|
495
|
-
const snapshot = task.migrationSnapshot;
|
|
496
|
-
if (!snapshot) return "";
|
|
497
|
-
return `<section class="side-panel">
|
|
498
|
-
<h3>${t("migrationSnapshot")}</h3>
|
|
499
|
-
<p>${escapeHtml(t("taskPreset"))}: ${tag(task.taskPreset || "none")}</p>
|
|
500
|
-
<p>${escapeHtml(t("targetLevel"))}: ${tag(snapshot.targetLevel || "unknown")}</p>
|
|
501
|
-
<p>${escapeHtml(t("achievedLevel"))}: ${tag(snapshot.achievedLevel || "unknown")}</p>
|
|
502
|
-
<p>${escapeHtml(t("strictDeferred"))}: ${tag(snapshot.strictDeferred ? "yes" : "no")}</p>
|
|
503
|
-
<p>${escapeHtml(t("evidenceBundle"))}: <code>${escapeHtml(snapshot.evidenceBundle || "missing")}</code></p>
|
|
504
|
-
</section>`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
508
|
-
if (!isTaskInReviewStage(task)) return "";
|
|
509
|
-
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
510
|
-
const confirmed = task.reviewStatus === "confirmed";
|
|
511
|
-
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
512
|
-
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
513
|
-
if (mode !== "workspace") {
|
|
514
|
-
return `<section class="side-panel review-actions">
|
|
515
|
-
<h3>${t("reviewActions")}</h3>
|
|
516
|
-
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
517
|
-
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
518
|
-
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
519
|
-
</section>`;
|
|
520
|
-
}
|
|
521
|
-
if (!canUseWorkbenchAction("review-complete")) {
|
|
522
|
-
return `<section class="side-panel review-actions">
|
|
523
|
-
<h3>${t("reviewActions")}</h3>
|
|
524
|
-
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
525
|
-
</section>`;
|
|
526
|
-
}
|
|
527
|
-
if (confirmed) {
|
|
528
|
-
return `<section class="side-panel review-actions">
|
|
529
|
-
<h3>${t("reviewActions")}</h3>
|
|
530
|
-
<p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
|
|
531
|
-
</section>`;
|
|
532
|
-
}
|
|
533
|
-
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
534
|
-
const disabled = blocking || missingWalkthrough || candidateBlocked;
|
|
535
|
-
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : t("reviewWorkbenchReady");
|
|
536
|
-
return `<section class="side-panel review-actions">
|
|
537
|
-
<h3>${t("reviewActions")}</h3>
|
|
538
|
-
<p>${escapeHtml(message)}</p>
|
|
539
|
-
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
540
|
-
<label class="review-check">
|
|
541
|
-
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
542
|
-
<span>${t("reviewConfirmChecklist")}</span>
|
|
543
|
-
</label>
|
|
544
|
-
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
545
|
-
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
546
|
-
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
547
|
-
</section>`;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function isTaskInReviewStage(task) {
|
|
551
|
-
const state = task?.state || "";
|
|
552
|
-
const lifecycle = task?.lifecycleState || "";
|
|
553
|
-
if (["not_started", "planned", "in_progress"].includes(state)) return false;
|
|
554
|
-
return state === "review" || ["in_review", "review-blocked"].includes(lifecycle);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function evidenceList(task) {
|
|
558
|
-
const evidence = task.evidence || [];
|
|
559
|
-
return `<section class="side-panel">
|
|
560
|
-
<h3>${t("evidence")}</h3>
|
|
561
|
-
${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
|
|
562
|
-
</section>`;
|
|
563
|
-
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
function taskDetail(route) {
|
|
2
|
+
const taskId = route.id;
|
|
3
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
|
|
4
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
5
|
+
return `<main class="task-detail">
|
|
6
|
+
<nav class="crumbs"><a href="#/tasks">${t("taskIndex")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
7
|
+
<section class="detail-hero">
|
|
8
|
+
<div>
|
|
9
|
+
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
10
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
11
|
+
<p>${escapeHtml(task.path)}</p>
|
|
12
|
+
${taskCopyButton(task, "detail-copy")}
|
|
13
|
+
</div>
|
|
14
|
+
<div class="detail-score">${task.completion}%</div>
|
|
15
|
+
</section>
|
|
16
|
+
${taskStateSummary(task)}
|
|
17
|
+
${phaseTimeline(task)}
|
|
18
|
+
<section class="detail-grid">
|
|
19
|
+
<article class="detail-main">
|
|
20
|
+
${taskDocumentLibrary(task, route.doc)}
|
|
21
|
+
</article>
|
|
22
|
+
<aside class="detail-side">
|
|
23
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
24
|
+
${lessonCandidatePanel(task, { context: "detail" })}
|
|
25
|
+
${openFindings(task)}
|
|
26
|
+
${evidenceList(task)}
|
|
27
|
+
${documentTabs(task)}
|
|
28
|
+
</aside>
|
|
29
|
+
</section>
|
|
30
|
+
</main>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function taskStateSummary(task) {
|
|
34
|
+
return `<section class="task-state-summary">
|
|
35
|
+
<div>
|
|
36
|
+
<span>${t("legacyState")}</span>
|
|
37
|
+
${tag(task.state)}
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<span>${t("lifecycleState")}</span>
|
|
41
|
+
${tag(task.lifecycleState || "unknown")}
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<span>${t("reviewStatus")}</span>
|
|
45
|
+
${tag(task.reviewStatus || "missing")}
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<span>${t("sedimentationStatus")}</span>
|
|
49
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<span>${t("closeoutStatus")}</span>
|
|
53
|
+
${tag(task.closeoutStatus || "missing")}
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<span>${t("lifecycleQueues")}</span>
|
|
57
|
+
${(task.taskQueues || []).map(tag).join("") || tag("active")}
|
|
58
|
+
</div>
|
|
59
|
+
${taskQueueReasonSummary(task)}
|
|
60
|
+
</section>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function taskQueueReasonSummary(task) {
|
|
64
|
+
const reasons = task.queueReasons || [];
|
|
65
|
+
if (!reasons.length) return "";
|
|
66
|
+
return `<div class="task-queue-reasons">
|
|
67
|
+
<span>${t("queueReasons")}</span>
|
|
68
|
+
<div class="review-reasons">
|
|
69
|
+
${reasons.slice(0, 5).map(reviewReason).join("")}
|
|
70
|
+
</div>
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function phaseTimeline(task) {
|
|
75
|
+
return `<section class="phase-timeline">
|
|
76
|
+
<h2>${t("phaseTimeline")}</h2>
|
|
77
|
+
${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
|
|
78
|
+
<strong>${escapeHtml(phase.id)}</strong>
|
|
79
|
+
<span>${phase.completion}%</span>
|
|
80
|
+
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
81
|
+
${progressBar(phase.completion)}
|
|
82
|
+
</div>`).join("") || emptyState(t("noPhaseData"))}
|
|
83
|
+
</section>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function taskDocSection(task, fileName, title, required) {
|
|
87
|
+
const doc = taskDocument(task, fileName);
|
|
88
|
+
if (!doc && !required) return "";
|
|
89
|
+
return `<section class="doc-section">
|
|
90
|
+
<div class="section-head"><h2>${escapeHtml(title)}</h2>${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}</div>
|
|
91
|
+
<div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : generatedBrief(task)}</div>
|
|
92
|
+
</section>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function taskDocumentLibrary(task, selectedTab) {
|
|
96
|
+
const docs = orderedTaskDocuments(task);
|
|
97
|
+
if (!docs.length) return taskDocSection(task, "brief.md", t("brief"), true);
|
|
98
|
+
const selectedKey = docs.some((doc) => doc.key === selectedTab) ? selectedTab : defaultTaskDocumentKey(task, docs);
|
|
99
|
+
return `<section class="doc-library">
|
|
100
|
+
<div class="section-head">
|
|
101
|
+
<div>
|
|
102
|
+
<p class="eyebrow">${t("taskDocuments")}</p>
|
|
103
|
+
<h2>${escapeHtml(t("sourceDocuments"))}</h2>
|
|
104
|
+
</div>
|
|
105
|
+
<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="doc-accordion-list">
|
|
108
|
+
${docs.map((item) => documentAccordion(item, item.key === selectedKey)).join("")}
|
|
109
|
+
</div>
|
|
110
|
+
</section>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function orderedTaskDocuments(task) {
|
|
114
|
+
const docs = taskDocTabs
|
|
115
|
+
.map(([key, file]) => {
|
|
116
|
+
const doc = taskDocument(task, file);
|
|
117
|
+
if (doc) return { key, file, title: t(key), path: doc.path, content: doc.content };
|
|
118
|
+
if (key === "brief") return { key, file, title: t(key), path: `${task.path}/brief.md`, content: generatedBrief(task), generated: true };
|
|
119
|
+
return null;
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const priority = taskDocumentPriority(task);
|
|
123
|
+
const rank = new Map(priority.map((key, index) => [key, index]));
|
|
124
|
+
return docs.sort((a, b) => (rank.get(a.key) ?? 99) - (rank.get(b.key) ?? 99));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskDocumentPriority(task) {
|
|
128
|
+
const stateName = task?.state || "";
|
|
129
|
+
const lifecycle = task?.lifecycleState || "";
|
|
130
|
+
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
131
|
+
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
132
|
+
}
|
|
133
|
+
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
134
|
+
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
135
|
+
}
|
|
136
|
+
if (stateName === "done" || ["closing", "closed"].includes(lifecycle)) {
|
|
137
|
+
return ["walkthrough", "progress", "review", "findings", "visualMap", "brief", "taskPlan", "strategy", "references", "artifacts", "legacyRoadmap"];
|
|
138
|
+
}
|
|
139
|
+
return ["brief", "taskPlan", "visualMap", "strategy", "progress", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function defaultTaskDocumentKey(task, docs) {
|
|
143
|
+
const priority = taskDocumentPriority(task);
|
|
144
|
+
return priority.find((key) => docs.some((doc) => doc.key === key)) || docs[0]?.key || "brief";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function documentAccordion(item, open) {
|
|
148
|
+
return `<details class="doc-accordion" ${open ? "open" : ""}>
|
|
149
|
+
<summary>
|
|
150
|
+
<span>${escapeHtml(item.title)}</span>
|
|
151
|
+
<small>${escapeHtml(item.generated ? t("generatedFallback") : item.path)}</small>
|
|
152
|
+
</summary>
|
|
153
|
+
<div class="markdown">${window.HarnessMarkdown.render(item.content, state.renderMode)}</div>
|
|
154
|
+
</details>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function documentTabs(task) {
|
|
158
|
+
const docs = orderedTaskDocuments(task);
|
|
159
|
+
return `<section class="side-panel">
|
|
160
|
+
<h3>${t("sourceDocuments")}</h3>
|
|
161
|
+
${docs.map((doc) => `<a href="#/tasks/${encodeURIComponent(task.id)}/docs/${encodeURIComponent(doc.key)}" title="${escapeAttr(doc.path)}">${escapeHtml(doc.title)}</a>`).join("") || `<p>${t("noDocuments")}</p>`}
|
|
162
|
+
</section>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function selectedSourceDocument(task, tab) {
|
|
166
|
+
if (!tab) return "";
|
|
167
|
+
const match = taskDocTabs.find(([key]) => key === tab);
|
|
168
|
+
if (!match) return "";
|
|
169
|
+
const doc = taskDocument(task, match[1]);
|
|
170
|
+
if (!doc) return "";
|
|
171
|
+
return `<section class="doc-section selected-source">
|
|
172
|
+
<div class="section-head"><h2>${t("selectedSource")} · ${t(match[0])}</h2><button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button></div>
|
|
173
|
+
<div class="markdown">${window.HarnessMarkdown.render(doc.content, state.renderMode)}</div>
|
|
174
|
+
</section>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function openFindings(task) {
|
|
178
|
+
const risks = task.risks || [];
|
|
179
|
+
return `<section class="side-panel">
|
|
180
|
+
<h3>${t("openFindings")}</h3>
|
|
181
|
+
${risks.map((risk) => `<div class="finding ${risk.open || risk.blocksRelease ? "open" : ""}"><strong>${escapeHtml(risk.severity)}</strong><span>${escapeHtml(risk.summary)}</span></div>`).join("") || `<p>${t("noOpenFindings")}</p>`}
|
|
182
|
+
</section>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
186
|
+
if (!isTaskInReviewQueue(task)) return "";
|
|
187
|
+
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
188
|
+
const confirmed = task.reviewStatus === "confirmed";
|
|
189
|
+
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
190
|
+
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
191
|
+
if (mode !== "workspace") {
|
|
192
|
+
return `<section class="side-panel review-actions">
|
|
193
|
+
<h3>${t("reviewActions")}</h3>
|
|
194
|
+
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
195
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
196
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
197
|
+
</section>`;
|
|
198
|
+
}
|
|
199
|
+
if (!canUseWorkbenchAction("review-complete")) {
|
|
200
|
+
return `<section class="side-panel review-actions">
|
|
201
|
+
<h3>${t("reviewActions")}</h3>
|
|
202
|
+
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
203
|
+
</section>`;
|
|
204
|
+
}
|
|
205
|
+
if (confirmed) {
|
|
206
|
+
return `<section class="side-panel review-actions">
|
|
207
|
+
<h3>${t("reviewActions")}</h3>
|
|
208
|
+
<p>${escapeHtml(t("reviewAlreadyConfirmed"))}</p>
|
|
209
|
+
</section>`;
|
|
210
|
+
}
|
|
211
|
+
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
212
|
+
const queueBlocked = !taskCanBeHumanConfirmed(task);
|
|
213
|
+
const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
|
|
214
|
+
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
|
|
215
|
+
return `<section class="side-panel review-actions">
|
|
216
|
+
<h3>${t("reviewActions")}</h3>
|
|
217
|
+
<p>${escapeHtml(message)}</p>
|
|
218
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
219
|
+
<label class="review-check">
|
|
220
|
+
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
221
|
+
<span>${t("reviewConfirmChecklist")}</span>
|
|
222
|
+
</label>
|
|
223
|
+
<div class="review-confirm-copy">
|
|
224
|
+
${taskCopyButton(task, "review-copy-task-name")}
|
|
225
|
+
</div>
|
|
226
|
+
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
227
|
+
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
228
|
+
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
229
|
+
</section>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isTaskInReviewQueue(task) {
|
|
233
|
+
return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function taskCanBeHumanConfirmed(task) {
|
|
237
|
+
return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function evidenceList(task) {
|
|
241
|
+
const evidence = task.evidence || [];
|
|
242
|
+
return `<section class="side-panel">
|
|
243
|
+
<h3>${t("evidence")}</h3>
|
|
244
|
+
${evidence.map((item) => `<p><strong>${escapeHtml(item.type || "evidence")}</strong> ${escapeHtml(item.summary || "")}</p>`).join("") || `<p>${t("noEvidence")}</p>`}
|
|
245
|
+
</section>`;
|
|
246
|
+
}
|