coding-agent-harness 1.0.4 → 1.0.5

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +661 -21
  3. package/LICENSE-EXCEPTION.md +37 -0
  4. package/README.md +33 -1
  5. package/README.zh-CN.md +23 -1
  6. package/SKILL.md +9 -8
  7. package/docs-release/architecture/overview.md +1 -1
  8. package/docs-release/architecture/overview.zh-CN.md +1 -1
  9. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  10. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  11. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  12. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  13. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  14. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  15. package/docs-release/architecture/system-explainer/README.md +67 -0
  16. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  17. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  18. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  19. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  20. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  21. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  22. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  23. package/docs-release/guides/agent-installation.en-US.md +8 -7
  24. package/docs-release/guides/agent-installation.md +9 -7
  25. package/docs-release/guides/preset-development.md +26 -2
  26. package/docs-release/guides/task-state-machine.en-US.md +30 -13
  27. package/docs-release/guides/task-state-machine.md +30 -13
  28. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  29. package/package.json +3 -2
  30. package/references/harness-ledger.md +1 -1
  31. package/scripts/commands/migration-command.mjs +30 -0
  32. package/scripts/commands/task-command.mjs +26 -25
  33. package/scripts/harness.mjs +7 -3
  34. package/scripts/lib/capability-registry.mjs +17 -21
  35. package/scripts/lib/check-module-parallel.mjs +9 -16
  36. package/scripts/lib/check-profiles.mjs +35 -81
  37. package/scripts/lib/check-task-contracts.mjs +13 -5
  38. package/scripts/lib/core-shared.mjs +55 -2
  39. package/scripts/lib/dashboard-data.mjs +126 -18
  40. package/scripts/lib/dashboard-workbench.mjs +80 -1
  41. package/scripts/lib/dashboard-writer.mjs +6 -2
  42. package/scripts/lib/git-status-summary.mjs +1 -1
  43. package/scripts/lib/governance-sync.mjs +180 -83
  44. package/scripts/lib/harness-core.mjs +1 -0
  45. package/scripts/lib/markdown-utils.mjs +33 -0
  46. package/scripts/lib/migration-planner.mjs +4 -6
  47. package/scripts/lib/phase-kind.mjs +50 -0
  48. package/scripts/lib/preset-engine.mjs +5 -8
  49. package/scripts/lib/preset-registry.mjs +188 -39
  50. package/scripts/lib/review-confirm-git-gate.mjs +1 -1
  51. package/scripts/lib/status-builder.mjs +88 -0
  52. package/scripts/lib/status-dashboard-renderer.mjs +7 -4
  53. package/scripts/lib/task-audit-metadata.mjs +385 -0
  54. package/scripts/lib/task-audit-migration.mjs +350 -0
  55. package/scripts/lib/task-completion-consistency.mjs +11 -1
  56. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  57. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  58. package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
  59. package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
  60. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  61. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  62. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  63. package/scripts/lib/task-lifecycle.mjs +114 -147
  64. package/scripts/lib/task-metadata.mjs +118 -0
  65. package/scripts/lib/task-review-model.mjs +54 -68
  66. package/scripts/lib/task-scanner.mjs +70 -143
  67. package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
  68. package/templates/AGENTS.md.template +7 -5
  69. package/templates/dashboard/assets/app-src/00-state.js +12 -0
  70. package/templates/dashboard/assets/app-src/10-router.js +3 -0
  71. package/templates/dashboard/assets/app-src/20-overview.js +7 -3
  72. package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
  73. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  74. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  75. package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
  76. package/templates/dashboard/assets/app.css +583 -0
  77. package/templates/dashboard/assets/app.css.manifest.json +1 -0
  78. package/templates/dashboard/assets/app.js +578 -10
  79. package/templates/dashboard/assets/app.manifest.json +1 -0
  80. package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
  81. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
  82. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  83. package/templates/dashboard/assets/i18n.js +140 -2
  84. package/templates/planning/INDEX.md +87 -0
  85. package/templates/planning/brief.md +1 -1
  86. package/templates/planning/module_session_prompt.md +1 -0
  87. package/templates/planning/review.md +0 -18
  88. package/templates/planning/task_plan.md +4 -43
  89. package/templates/planning/visual_map.md +13 -9
  90. package/templates/planning/visual_map.simple.md +52 -0
  91. package/templates/reference/execution-workflow-standard.md +29 -2
  92. package/templates-zh-CN/AGENTS.md.template +7 -5
  93. package/templates-zh-CN/planning/INDEX.md +87 -0
  94. package/templates-zh-CN/planning/brief.md +1 -1
  95. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  96. package/templates-zh-CN/planning/review.md +0 -18
  97. package/templates-zh-CN/planning/task_plan.md +3 -63
  98. package/templates-zh-CN/planning/visual_map.md +14 -7
  99. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  100. package/templates-zh-CN/reference/execution-workflow-standard.md +31 -6
@@ -12,6 +12,18 @@ const state = {
12
12
  taskGroupPage: 1,
13
13
  warningFilter: "all",
14
14
  warningPage: 1,
15
+ presetQuery: "",
16
+ presetSourceFilter: "all",
17
+ selectedPresetKey: "",
18
+ selectedPresetId: "",
19
+ presetActionResult: null,
20
+ presetInstallSource: "",
21
+ presetInstallScope: "project",
22
+ presetInstallForce: false,
23
+ presetSeedScope: "project",
24
+ presetSeedForce: false,
25
+ presetUninstallScope: "project",
26
+ presetUninstallConfirm: "",
15
27
  renderMode: "rendered",
16
28
  theme: localStorage.getItem("harness.theme") || "system",
17
29
  taskLayout: localStorage.getItem("harness.taskLayout") || "list",
@@ -72,6 +84,7 @@ function shell() {
72
84
  ${routeLink("#/tasks", t("taskIndex"), "tasks")}
73
85
  ${routeLink("#/review", t("reviewQueue"), "review")}
74
86
  ${routeLink("#/modules", t("moduleView"), "modules")}
87
+ ${routeLink("#/presets", t("presetCatalog"), "presets")}
75
88
  <button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>
76
89
  <button data-theme-toggle>${themeLabel()}</button>
77
90
  </div>
@@ -98,6 +111,7 @@ function renderRoute() {
98
111
  if (route.name === "reviewTask") return reviewWorkspace(route);
99
112
  if (route.name === "review") return reviewQueue();
100
113
  if (route.name === "modules") return modulesView(route.id);
114
+ if (route.name === "presets") return presetsView();
101
115
  if (route.name === "tasks") return taskIndex();
102
116
  return overview();
103
117
  }
@@ -109,6 +123,7 @@ function currentRoute() {
109
123
  if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
110
124
  if (parts[0] === "review") return { name: "review" };
111
125
  if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
126
+ if (parts[0] === "presets") return { name: "presets" };
112
127
  if (parts[0] === "tasks") return { name: "tasks" };
113
128
  return { name: "overview" };
114
129
  }
@@ -137,6 +152,9 @@ function overview() {
137
152
 
138
153
  function statusStrip() {
139
154
  const status = bundle.status?.checkState?.status || "unknown";
155
+ const validationMode = bundle.status?.checkState?.validationMode || "validated";
156
+ const dataOnly = validationMode === "data-only";
157
+ const displayState = dataOnly ? "snapshot" : status;
140
158
  const failures = bundle.status?.checkState?.failures || 0;
141
159
  const warnings = bundle.status?.checkState?.warnings || 0;
142
160
  const tasks = bundle.status?.tasks || [];
@@ -144,9 +162,9 @@ function statusStrip() {
144
162
  const visual = summary.visualMapCoverage || {};
145
163
  const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
146
164
  return `<section class="status-card-group">
147
- <div class="status-primary ${status}">
148
- <span>${t("readiness")}</span>
149
- <strong>${label(status)}</strong>
165
+ <div class="status-primary ${displayState}">
166
+ <span>${dataOnly ? t("snapshotStatus") : t("readiness")}</span>
167
+ <strong>${dataOnly ? t("snapshot") : label(status)}</strong>
150
168
  <p>${nextActionText()}</p>
151
169
  </div>
152
170
  <div class="metrics-grid">
@@ -167,6 +185,7 @@ function metric(labelText, value) {
167
185
  }
168
186
 
169
187
  function nextActionText() {
188
+ if ((bundle.status?.checkState?.validationMode || "validated") === "data-only") return t("snapshotNotValidated");
170
189
  const failures = bundle.status?.checkState?.failures || 0;
171
190
  if (failures > 0) return t("resolveBlockers");
172
191
  const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
@@ -845,17 +864,57 @@ function taskQueueReasonSummary(task) {
845
864
  }
846
865
 
847
866
  function phaseTimeline(task) {
867
+ const knownKinds = new Set(["init", "execution", "gate"]);
868
+ const groups = [
869
+ ["init", "Init"],
870
+ ["execution", "Execution"],
871
+ ["gate", "Gate"],
872
+ ["other", "Other / Invalid"],
873
+ ];
874
+ const phases = task.phases || [];
875
+ const grouped = groups
876
+ .map(([kind, label]) => {
877
+ const items = kind === "other"
878
+ ? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
879
+ : phases.filter((phase) => (phase.kind || "execution") === kind);
880
+ if (!items.length) return "";
881
+ return `<div class="phase-kind-group ${escapeAttr(kind)}">
882
+ <h3>${escapeHtml(label)}</h3>
883
+ ${items.map(phaseStep).join("")}
884
+ </div>`;
885
+ })
886
+ .join("");
848
887
  return `<section class="phase-timeline">
849
888
  <h2>${t("phaseTimeline")}</h2>
850
- ${(task.phases || []).map((phase) => `<div class="phase-step ${phase.state}">
851
- <strong>${escapeHtml(phase.id)}</strong>
852
- <span>${phase.completion}%</span>
853
- <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
854
- ${progressBar(phase.completion)}
855
- </div>`).join("") || emptyState(t("noPhaseData"))}
889
+ ${grouped || emptyState(t("noPhaseData"))}
856
890
  </section>`;
857
891
  }
858
892
 
893
+ function phaseStep(phase) {
894
+ const kind = phase.kind || "execution";
895
+ const actor = phase.actor || "agent";
896
+ const knownKind = ["init", "execution", "gate"].includes(kind);
897
+ const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
898
+ const phaseKindClass = knownKind ? kind : "other";
899
+ return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
900
+ <div class="phase-step-head">
901
+ <strong>${escapeHtml(phase.id)}</strong>
902
+ <span>${kindLabel} · ${phase.completion}%</span>
903
+ </div>
904
+ <p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
905
+ ${progressBar(phase.completion)}
906
+ <div class="phase-meta">
907
+ ${phaseMetaTag(actor)}
908
+ ${tag(phase.evidenceStatus || "missing")}
909
+ </div>
910
+ ${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
911
+ </div>`;
912
+ }
913
+
914
+ function phaseMetaTag(value) {
915
+ return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
916
+ }
917
+
859
918
  function taskDocSection(task, fileName, title, required) {
860
919
  const doc = taskDocument(task, fileName);
861
920
  if (!doc && !required) return "";
@@ -1609,6 +1668,382 @@ function healthPanel() {
1609
1668
  </section>`;
1610
1669
  }
1611
1670
 
1671
+ function presetsView() {
1672
+ ensurePresetState();
1673
+ const catalog = bundle.presetCatalog || { summary: {}, roots: [], presets: [] };
1674
+ let presets = filteredPresets();
1675
+ syncVisiblePresetSelection(presets);
1676
+ presets = filteredPresets();
1677
+ const selected = selectedPreset(presets);
1678
+ syncPresetUninstallScope(selected);
1679
+ return `<div class="presets-page stack">
1680
+ <section class="flow-panel preset-command-center">
1681
+ <div class="section-head">
1682
+ <div>
1683
+ <p class="eyebrow">${t("presetCatalog")}</p>
1684
+ <h2>${t("presetCatalog")}</h2>
1685
+ <p class="subtle">${t("presetCatalogSubtitle")}</p>
1686
+ </div>
1687
+ <span class="preset-count-pill">${presets.length}/${catalog.summary?.total || 0}</span>
1688
+ </div>
1689
+ <div class="preset-priority-strip" aria-label="${escapeAttr(t("presetPriorityTitle"))}">
1690
+ ${presetPriorityStep("project", 1)}
1691
+ ${presetPriorityStep("user", 2)}
1692
+ ${presetPriorityStep("builtin", 3)}
1693
+ </div>
1694
+ <div class="preset-toolbar">
1695
+ <div class="input-group">
1696
+ <input data-preset-search value="${escapeAttr(state.presetQuery)}" placeholder="${escapeAttr(t("presetSearchPlaceholder"))}" aria-label="${escapeAttr(t("presetSearch"))}">
1697
+ </div>
1698
+ <div class="preset-source-tabs" role="tablist" aria-label="${escapeAttr(t("presetSourceFilter"))}">
1699
+ ${presetSourceOptions().map((source) => presetSourceButton(source)).join("")}
1700
+ </div>
1701
+ </div>
1702
+ </section>
1703
+ <section class="preset-workspace">
1704
+ <div class="flow-panel preset-collection-panel">
1705
+ <div class="preset-panel-heading">
1706
+ <div>
1707
+ <h3>${t("presetCollection")}</h3>
1708
+ <p>${t("presetCollectionHint")}</p>
1709
+ </div>
1710
+ </div>
1711
+ <div class="preset-catalog-list">
1712
+ ${presets.map((preset) => presetCard(preset, selected ? presetKey(selected) : "")).join("") || emptyState(t("noPresets"))}
1713
+ </div>
1714
+ </div>
1715
+ <div class="preset-detail-workspace stack">
1716
+ ${presetDetailPanel(selected)}
1717
+ ${presetLayerStackPanel(selected)}
1718
+ </div>
1719
+ <aside class="preset-context-actions stack">
1720
+ ${presetActionPanel(selected)}
1721
+ ${presetImportPanel()}
1722
+ ${presetRestorePanel()}
1723
+ ${presetSummaryPanel(catalog)}
1724
+ </aside>
1725
+ </section>
1726
+ </div>`;
1727
+ }
1728
+
1729
+ function ensurePresetState() {
1730
+ const presets = bundle.presetCatalog?.presets || [];
1731
+ if (!state.selectedPresetKey && state.selectedPresetId) {
1732
+ const legacySelection = presets.find((preset) => preset.id === state.selectedPresetId);
1733
+ if (legacySelection) state.selectedPresetKey = presetKey(legacySelection);
1734
+ }
1735
+ if (!state.selectedPresetKey && presets[0]) {
1736
+ state.selectedPresetKey = presetKey(presets[0]);
1737
+ state.presetUninstallConfirm = "";
1738
+ }
1739
+ if (state.selectedPresetKey && !presets.some((preset) => presetKey(preset) === state.selectedPresetKey) && presets[0]) {
1740
+ state.selectedPresetKey = presetKey(presets[0]);
1741
+ state.presetUninstallConfirm = "";
1742
+ }
1743
+ }
1744
+
1745
+ function presetSourceOptions() {
1746
+ return [
1747
+ ["all", t("allPresets")],
1748
+ ["project", t("presetSourceProject")],
1749
+ ["user", t("presetSourceUser")],
1750
+ ["builtin", t("presetSourceBuiltin")],
1751
+ ];
1752
+ }
1753
+
1754
+ function presetSourceButton([source, labelText]) {
1755
+ const active = state.presetSourceFilter === source;
1756
+ const count = source === "all" ? (bundle.presetCatalog?.summary?.total || 0) : (bundle.presetCatalog?.summary?.[source] || 0);
1757
+ return `<button type="button" class="${active ? "active" : ""}" data-preset-source-filter="${escapeAttr(source)}" role="tab" aria-selected="${active ? "true" : "false"}">
1758
+ <span>${escapeHtml(labelText)}</span>
1759
+ <strong>${count}</strong>
1760
+ </button>`;
1761
+ }
1762
+
1763
+ function filteredPresets() {
1764
+ const query = String(state.presetQuery || "").trim().toLowerCase();
1765
+ return (bundle.presetCatalog?.presets || []).filter((preset) => {
1766
+ if (state.presetSourceFilter !== "all" && preset.source !== state.presetSourceFilter) return false;
1767
+ return presetMatchesQuery(preset, query);
1768
+ });
1769
+ }
1770
+
1771
+ function presetMatchesQuery(preset, query = state.presetQuery) {
1772
+ const normalizedQuery = String(query || "").trim().toLowerCase();
1773
+ if (!normalizedQuery) return true;
1774
+ return [
1775
+ preset.id,
1776
+ preset.source,
1777
+ preset.purpose,
1778
+ preset.taskKind,
1779
+ preset.manifestPath,
1780
+ preset.version,
1781
+ ...(preset.compatibleBudgets || []),
1782
+ ].some((value) => String(value || "").toLowerCase().includes(normalizedQuery));
1783
+ }
1784
+
1785
+ function syncVisiblePresetSelection(visiblePresets) {
1786
+ if (!visiblePresets.length) {
1787
+ state.selectedPresetKey = "";
1788
+ state.presetUninstallConfirm = "";
1789
+ return;
1790
+ }
1791
+ if (!visiblePresets.some((preset) => presetKey(preset) === state.selectedPresetKey)) {
1792
+ state.selectedPresetKey = presetKey(visiblePresets[0]);
1793
+ state.presetUninstallConfirm = "";
1794
+ }
1795
+ }
1796
+
1797
+ function selectedPreset(visiblePresets = filteredPresets()) {
1798
+ return visiblePresets.find((preset) => presetKey(preset) === state.selectedPresetKey) || visiblePresets[0] || null;
1799
+ }
1800
+
1801
+ function presetCard(preset, selectedId) {
1802
+ const key = presetKey(preset);
1803
+ const selected = key === selectedId;
1804
+ return `<article class="preset-card ${selected ? "active" : ""} ${preset.effective ? "effective" : "shadowed"}">
1805
+ <div class="preset-card-topline">
1806
+ <button type="button" class="preset-card-select" data-preset-select="${escapeAttr(key)}" aria-pressed="${selected ? "true" : "false"}">
1807
+ <span class="card-id">${escapeHtml(preset.id)}</span>
1808
+ </button>
1809
+ <div class="preset-card-tools">
1810
+ ${presetSourceBadge(preset.source)}
1811
+ ${presetStatusBadge(preset)}
1812
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}" title="${escapeAttr(t("copyPresetId"))}">${t("copyIdShort")}</button>
1813
+ </div>
1814
+ </div>
1815
+ <button type="button" class="preset-card-body" data-preset-select="${escapeAttr(key)}">
1816
+ <span>${escapeHtml(preset.purpose || t("none"))}</span>
1817
+ </button>
1818
+ <div class="preset-card-meta">
1819
+ <span>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(preset))}</span>
1820
+ <span>${t("taskKind")}: ${escapeHtml(preset.taskKind || t("none"))}</span>
1821
+ <span>${t("budgets")}: ${escapeHtml((preset.compatibleBudgets || []).join(", ") || t("none"))}</span>
1822
+ </div>
1823
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
1824
+ </article>`;
1825
+ }
1826
+
1827
+ function presetKey(preset) {
1828
+ return preset?.key || `${preset?.source || "unknown"}:${preset?.id || ""}`;
1829
+ }
1830
+
1831
+ function presetSourceRank(source) {
1832
+ return { project: 1, user: 2, builtin: 3 }[source] || 9;
1833
+ }
1834
+
1835
+ function presetLayersForId(id) {
1836
+ return (bundle.presetCatalog?.presets || [])
1837
+ .filter((preset) => preset.id === id)
1838
+ .sort((a, b) => presetSourceRank(a.source) - presetSourceRank(b.source));
1839
+ }
1840
+
1841
+ function syncPresetUninstallScope(preset) {
1842
+ if (preset && ["project", "user"].includes(preset.source)) state.presetUninstallScope = preset.source;
1843
+ }
1844
+
1845
+ function presetPriorityStep(source, index) {
1846
+ return `<div class="preset-priority-step">
1847
+ <span>${index}</span>
1848
+ <strong>${escapeHtml(t(`presetSource_${source}`) || source)}</strong>
1849
+ </div>`;
1850
+ }
1851
+
1852
+ function presetSourceBadge(source) {
1853
+ const normalized = String(source || "unknown");
1854
+ return `<span class="tag preset-source-badge ${escapeAttr(normalized)}">${escapeHtml(t(`presetSource_${normalized}`) || normalized)}</span>`;
1855
+ }
1856
+
1857
+ function presetStatusBadge(preset) {
1858
+ return `<span class="tag ${preset.effective ? "pass" : "warn"}">${escapeHtml(preset.effective ? t("presetEffective") : t("presetShadowed"))}</span>`;
1859
+ }
1860
+
1861
+ function formatPresetVersion(preset) {
1862
+ return preset?.version ?? t("none");
1863
+ }
1864
+
1865
+ function presetSummaryPanel(catalog) {
1866
+ const roots = catalog.roots || [];
1867
+ return `<section class="side-panel preset-summary-panel">
1868
+ <h3>${t("presetSources")}</h3>
1869
+ <p class="preset-helper">${t("presetSourcesHint")}</p>
1870
+ <div class="metrics-grid compact">
1871
+ ${metric(t("presetSourceProject"), catalog.summary?.project || 0)}
1872
+ ${metric(t("presetSourceUser"), catalog.summary?.user || 0)}
1873
+ ${metric(t("presetSourceBuiltin"), catalog.summary?.builtin || 0)}
1874
+ </div>
1875
+ <div class="preset-roots">
1876
+ ${roots.map((root) => `<div><strong>${escapeHtml(t(`presetSource_${root.source}`) || root.source)}</strong><code>${escapeHtml(root.path || "")}</code></div>`).join("")}
1877
+ </div>
1878
+ </section>`;
1879
+ }
1880
+
1881
+ function presetDetailPanel(preset) {
1882
+ if (!preset) return `<section class="flow-panel preset-detail-panel">${emptyState(t("noPresets"))}</section>`;
1883
+ const inspectCommand = `harness preset inspect ${preset.id} --json .`;
1884
+ const checkCommand = `harness preset check ${preset.id} --json .`;
1885
+ const commandRows = preset.effective
1886
+ ? `${presetCommandRow(inspectCommand)}${presetCommandRow(checkCommand)}`
1887
+ : `<div class="preset-command-warning">${escapeHtml(t("presetCommandsEffectiveOnly"))}</div>`;
1888
+ return `<section class="flow-panel preset-detail-panel">
1889
+ <div class="preset-detail-hero">
1890
+ <div>
1891
+ <div class="preset-detail-title-row">
1892
+ <h3>${escapeHtml(preset.id)}</h3>
1893
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}">${t("copyPresetId")}</button>
1894
+ </div>
1895
+ <p>${escapeHtml(preset.purpose || "")}</p>
1896
+ </div>
1897
+ <div class="preset-detail-badges">
1898
+ ${presetSourceBadge(preset.source)}
1899
+ ${presetStatusBadge(preset)}
1900
+ </div>
1901
+ </div>
1902
+ <dl class="preset-detail-list">
1903
+ ${presetDetailRow(t("manifestVersion"), formatPresetVersion(preset))}
1904
+ ${presetDetailRow(t("source"), t(`presetSource_${preset.source}`) || preset.source)}
1905
+ ${presetDetailRow(t("status"), preset.effective ? t("presetEffective") : t("presetShadowed"))}
1906
+ ${presetDetailRow(t("taskKind"), preset.taskKind || t("none"))}
1907
+ ${presetDetailRow(t("budgets"), (preset.compatibleBudgets || []).join(", ") || t("none"))}
1908
+ ${presetDetailRow(t("inputs"), preset.inputCount || 0)}
1909
+ ${presetDetailRow(t("references"), preset.referenceCount || 0)}
1910
+ ${presetDetailRow(t("artifacts"), preset.artifactCount || 0)}
1911
+ ${presetDetailRow(t("writeScopes"), preset.writeScopeCount || 0)}
1912
+ ${presetDetailRow(t("requiredReads"), preset.requiredReadCount || 0)}
1913
+ </dl>
1914
+ <div class="preset-path-block">
1915
+ <span>${t("manifestPath")}</span>
1916
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
1917
+ </div>
1918
+ <div class="preset-command-list">
1919
+ ${commandRows}
1920
+ </div>
1921
+ </section>`;
1922
+ }
1923
+
1924
+ function presetDetailRow(labelText, value) {
1925
+ return `<div><dt>${escapeHtml(labelText)}</dt><dd>${escapeHtml(String(value ?? ""))}</dd></div>`;
1926
+ }
1927
+
1928
+ function presetCommandRow(command) {
1929
+ return `<div class="preset-command-row">
1930
+ <code>${escapeHtml(command)}</code>
1931
+ <button type="button" class="copy-inline" data-copy-preset-command="${escapeAttr(command)}">${t("copyCommand")}</button>
1932
+ </div>`;
1933
+ }
1934
+
1935
+ function presetLayerStackPanel(preset) {
1936
+ if (!preset) return "";
1937
+ const layers = presetLayersForId(preset.id);
1938
+ return `<section class="flow-panel preset-layer-panel">
1939
+ <div class="preset-panel-heading">
1940
+ <div>
1941
+ <h3>${t("presetLayerStack")}</h3>
1942
+ <p>${t("presetLayerStackHint")}</p>
1943
+ </div>
1944
+ </div>
1945
+ <div class="preset-layer-list">
1946
+ ${layers.map((layer) => `<button type="button" class="preset-layer-row ${presetKey(layer) === presetKey(preset) ? "active" : ""}" data-preset-select="${escapeAttr(presetKey(layer))}">
1947
+ <span class="preset-layer-rank">${presetSourceRank(layer.source)}</span>
1948
+ <span>
1949
+ <strong>${escapeHtml(t(`presetSource_${layer.source}`) || layer.source)}</strong>
1950
+ <small>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(layer))}</small>
1951
+ </span>
1952
+ ${presetStatusBadge(layer)}
1953
+ </button>`).join("")}
1954
+ </div>
1955
+ </section>`;
1956
+ }
1957
+
1958
+ function presetActionPanel(preset) {
1959
+ const staticNote = canUseWorkbenchAction("preset-install") ? "" : `<p class="lesson-action-note">${escapeHtml(t("presetWorkbenchRequired"))}</p>`;
1960
+ const lockedUninstallScope = preset && ["project", "user"].includes(preset.source) ? preset.source : "";
1961
+ const confirmMatches = Boolean(preset && state.presetUninstallConfirm.trim() === preset.id);
1962
+ const canCheck = canUseWorkbenchAction("preset-check") && preset && preset.effective;
1963
+ const canUninstall = canUseWorkbenchAction("preset-uninstall") && preset && preset.source !== "builtin" && confirmMatches;
1964
+ return `<section class="side-panel preset-action-panel">
1965
+ <div class="preset-panel-heading">
1966
+ <div>
1967
+ <h3>${t("presetContextActions")}</h3>
1968
+ <p>${preset ? escapeHtml(preset.id) : t("noPresets")}</p>
1969
+ </div>
1970
+ </div>
1971
+ ${staticNote}
1972
+ ${presetActionResult()}
1973
+ <div class="preset-action-group">
1974
+ <h4>${t("presetCheck")}</h4>
1975
+ <p>${preset?.effective ? t("presetCheckHint") : t("presetShadowedActionHint")}</p>
1976
+ <button data-preset-check="${escapeAttr(preset?.id || "")}" ${canCheck ? "" : "disabled"}>${t("presetCheckSelected")}</button>
1977
+ </div>
1978
+ <div class="preset-action-group danger">
1979
+ <h4>${t("presetUninstallSelected")}</h4>
1980
+ <p>${preset?.source === "builtin" ? t("presetBuiltinImmutable") : t("presetUninstallHint")}</p>
1981
+ <label>${t("scope")}<select data-preset-uninstall-scope ${lockedUninstallScope ? "disabled" : ""}>
1982
+ ${presetScopeOptions(lockedUninstallScope || state.presetUninstallScope)}
1983
+ </select></label>
1984
+ <div class="preset-confirm-row">
1985
+ <label>${t("confirmPresetId")}<input data-preset-uninstall-confirm value="${escapeAttr(state.presetUninstallConfirm)}" placeholder="${escapeAttr(preset?.id || "")}"></label>
1986
+ <button type="button" data-preset-fill-confirm="${escapeAttr(preset?.id || "")}" ${preset && preset.source !== "builtin" ? "" : "disabled"}>${t("useSelectedId")}</button>
1987
+ </div>
1988
+ ${preset && preset.source !== "builtin" && !confirmMatches ? `<p class="preset-confirm-warning">${escapeHtml(t("presetConfirmRequired"))}</p>` : ""}
1989
+ <button data-preset-uninstall="${escapeAttr(preset?.id || "")}" ${canUninstall ? "" : "disabled"}>${t("presetUninstallSelected")}</button>
1990
+ </div>
1991
+ </section>`;
1992
+ }
1993
+
1994
+ function presetImportPanel() {
1995
+ return `<section class="side-panel preset-action-panel">
1996
+ <div class="preset-panel-heading">
1997
+ <div>
1998
+ <h3>${t("presetImportTitle")}</h3>
1999
+ <p>${t("presetImportHint")}</p>
2000
+ </div>
2001
+ </div>
2002
+ <div class="preset-action-group">
2003
+ <label>${t("source")}<input data-preset-install-source value="${escapeAttr(state.presetInstallSource)}" placeholder="${escapeAttr(t("presetInstallSourcePlaceholder"))}"></label>
2004
+ <label>${t("scope")}<select data-preset-install-scope>
2005
+ ${presetScopeOptions(state.presetInstallScope)}
2006
+ </select></label>
2007
+ <label class="check-row"><input type="checkbox" data-preset-install-force ${state.presetInstallForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
2008
+ <button data-preset-install ${canUseWorkbenchAction("preset-install") ? "" : "disabled"}>${t("presetInstall")}</button>
2009
+ </div>
2010
+ </section>`;
2011
+ }
2012
+
2013
+ function presetRestorePanel() {
2014
+ return `<section class="side-panel preset-action-panel">
2015
+ <div class="preset-panel-heading">
2016
+ <div>
2017
+ <h3>${t("presetRestoreBundled")}</h3>
2018
+ <p>${t("presetRestoreBundledHint")}</p>
2019
+ </div>
2020
+ </div>
2021
+ <div class="preset-action-group">
2022
+ <label>${t("scope")}<select data-preset-seed-scope>
2023
+ ${presetScopeOptions(state.presetSeedScope)}
2024
+ </select></label>
2025
+ <label class="check-row"><input type="checkbox" data-preset-seed-force ${state.presetSeedForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
2026
+ <button data-preset-seed ${canUseWorkbenchAction("preset-seed") ? "" : "disabled"}>${t("presetRestoreBundled")}</button>
2027
+ </div>
2028
+ </section>`;
2029
+ }
2030
+
2031
+ function presetScopeOptions(current) {
2032
+ return [["project", t("presetSourceProject")], ["user", t("presetSourceUser")]]
2033
+ .map(([value, labelText]) => `<option value="${value}" ${current === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`)
2034
+ .join("");
2035
+ }
2036
+
2037
+ function presetActionResult() {
2038
+ const result = state.presetActionResult;
2039
+ if (!result) return "";
2040
+ const klass = result.ok ? "success" : "failed";
2041
+ return `<div class="workbench-action-result ${klass}">
2042
+ <strong>${escapeHtml(result.title || "")}</strong>
2043
+ <span>${escapeHtml(result.message || "")}</span>
2044
+ </div>`;
2045
+ }
2046
+
1612
2047
  function taskDocument(task, fileName) {
1613
2048
  if (fileName === "__walkthrough__" && task.walkthroughPath) return findDocument(task.walkthroughPath);
1614
2049
  return findDocument(`${task.path}/${fileName}`);
@@ -1639,7 +2074,9 @@ function tag(value) {
1639
2074
  }
1640
2075
 
1641
2076
  function label(value) {
1642
- return t(`state_${value}`) || String(value || "unknown").replaceAll("_", " ");
2077
+ const key = `state_${value}`;
2078
+ const translated = t(key);
2079
+ return translated === key ? String(value || "unknown").replaceAll("_", " ") : translated;
1643
2080
  }
1644
2081
 
1645
2082
  function list(items = []) {
@@ -1722,6 +2159,68 @@ function bind() {
1722
2159
  state.warningPage = 1;
1723
2160
  app();
1724
2161
  }));
2162
+ document.querySelectorAll("[data-preset-search]").forEach((input) => input.addEventListener("input", () => {
2163
+ state.presetQuery = input.value;
2164
+ app();
2165
+ }));
2166
+ document.querySelectorAll("[data-preset-source-filter]").forEach((button) => button.addEventListener("click", () => {
2167
+ state.presetSourceFilter = button.dataset.presetSourceFilter || "all";
2168
+ state.selectedPresetKey = "";
2169
+ state.presetUninstallConfirm = "";
2170
+ app();
2171
+ }));
2172
+ document.querySelectorAll("[data-preset-select]").forEach((button) => button.addEventListener("click", () => {
2173
+ state.selectedPresetKey = button.dataset.presetSelect || "";
2174
+ state.selectedPresetId = "";
2175
+ const selectedPreset = (bundle.presetCatalog?.presets || []).find((preset) => presetKey(preset) === state.selectedPresetKey);
2176
+ if (selectedPreset && state.presetSourceFilter !== "all" && selectedPreset.source !== state.presetSourceFilter) {
2177
+ state.presetSourceFilter = selectedPreset.source;
2178
+ }
2179
+ if (selectedPreset && !presetMatchesQuery(selectedPreset)) state.presetQuery = "";
2180
+ if (selectedPreset && ["project", "user"].includes(selectedPreset.source)) state.presetUninstallScope = selectedPreset.source;
2181
+ state.presetUninstallConfirm = "";
2182
+ app();
2183
+ }));
2184
+ document.querySelectorAll("[data-preset-install-source]").forEach((input) => input.addEventListener("input", () => {
2185
+ state.presetInstallSource = input.value;
2186
+ }));
2187
+ document.querySelectorAll("[data-preset-install-scope]").forEach((select) => select.addEventListener("change", () => {
2188
+ state.presetInstallScope = select.value || "project";
2189
+ }));
2190
+ document.querySelectorAll("[data-preset-install-force]").forEach((input) => input.addEventListener("change", () => {
2191
+ state.presetInstallForce = input.checked;
2192
+ }));
2193
+ document.querySelectorAll("[data-preset-seed-scope]").forEach((select) => select.addEventListener("change", () => {
2194
+ state.presetSeedScope = select.value || "project";
2195
+ }));
2196
+ document.querySelectorAll("[data-preset-seed-force]").forEach((input) => input.addEventListener("change", () => {
2197
+ state.presetSeedForce = input.checked;
2198
+ }));
2199
+ document.querySelectorAll("[data-preset-uninstall-scope]").forEach((select) => select.addEventListener("change", () => {
2200
+ state.presetUninstallScope = select.value || "project";
2201
+ }));
2202
+ document.querySelectorAll("[data-preset-uninstall-confirm]").forEach((input) => input.addEventListener("input", () => {
2203
+ state.presetUninstallConfirm = input.value;
2204
+ }));
2205
+ document.querySelectorAll("[data-preset-fill-confirm]").forEach((button) => button.addEventListener("click", () => {
2206
+ state.presetUninstallConfirm = button.dataset.presetFillConfirm || "";
2207
+ app();
2208
+ }));
2209
+ document.querySelectorAll("[data-preset-check]").forEach((button) => button.addEventListener("click", () => runPresetAction("check", { id: button.dataset.presetCheck || "" })));
2210
+ document.querySelectorAll("[data-preset-install]").forEach((button) => button.addEventListener("click", () => runPresetAction("install", {
2211
+ source: state.presetInstallSource,
2212
+ scope: state.presetInstallScope,
2213
+ force: state.presetInstallForce,
2214
+ })));
2215
+ document.querySelectorAll("[data-preset-seed]").forEach((button) => button.addEventListener("click", () => runPresetAction("seed", {
2216
+ scope: state.presetSeedScope,
2217
+ force: state.presetSeedForce,
2218
+ })));
2219
+ document.querySelectorAll("[data-preset-uninstall]").forEach((button) => button.addEventListener("click", () => runPresetAction("uninstall", {
2220
+ id: button.dataset.presetUninstall || "",
2221
+ scope: state.presetUninstallScope,
2222
+ confirmText: state.presetUninstallConfirm,
2223
+ })));
1725
2224
  document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
1726
2225
  state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
1727
2226
  state.reviewQueuePage = 1;
@@ -1768,6 +2267,7 @@ function bind() {
1768
2267
  openDrawer(taskId);
1769
2268
  }));
1770
2269
  bindCopyTaskNameButtons(document);
2270
+ bindPresetCopyButtons(document);
1771
2271
  bindRepairPromptButtons(document);
1772
2272
  bindLessonSedimentationButtons(document);
1773
2273
  document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
@@ -1850,6 +2350,45 @@ async function completeReviewFromDashboard(taskId) {
1850
2350
  }
1851
2351
  }
1852
2352
 
2353
+ async function runPresetAction(action, body) {
2354
+ state.presetActionResult = { ok: true, title: t("presetActionRunning"), message: action };
2355
+ app();
2356
+ try {
2357
+ const response = await fetch(`/api/presets/${action}`, {
2358
+ method: "POST",
2359
+ headers: {
2360
+ "content-type": "application/json",
2361
+ "x-harness-csrf": state.runtime?.csrfToken || "",
2362
+ },
2363
+ body: JSON.stringify(body),
2364
+ });
2365
+ const payload = await response.json();
2366
+ if (!response.ok) throw payload;
2367
+ state.presetActionResult = {
2368
+ ok: true,
2369
+ title: t("presetActionSuccess"),
2370
+ message: presetActionMessage(action, payload),
2371
+ };
2372
+ app();
2373
+ if (["install", "seed", "uninstall"].includes(action)) setTimeout(() => window.location.reload(), 650);
2374
+ } catch (error) {
2375
+ state.presetActionResult = {
2376
+ ok: false,
2377
+ title: t("presetActionFailed"),
2378
+ message: error?.error || error?.message || String(error || action),
2379
+ };
2380
+ app();
2381
+ }
2382
+ }
2383
+
2384
+ function presetActionMessage(action, payload) {
2385
+ if (action === "check") return `${payload.id || ""} ${payload.status || ""}`.trim();
2386
+ if (action === "install") return `${payload.id || ""} -> ${payload.scope || ""}`.trim();
2387
+ if (action === "seed") return `${payload.created || 0} ${t("created")} · ${payload.skipped || 0} ${t("skipped")}`;
2388
+ if (action === "uninstall") return `${payload.id || ""} ${payload.removed ? t("removed") : t("notInstalled")}`.trim();
2389
+ return action;
2390
+ }
2391
+
1853
2392
  function renderDrawerContent(taskId) {
1854
2393
  const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
1855
2394
  if (!task) return `<div class="empty">${t("taskNotFound")}</div>`;
@@ -1929,6 +2468,35 @@ function bindCopyTaskNameButtons(root) {
1929
2468
  }));
1930
2469
  }
1931
2470
 
2471
+ function bindPresetCopyButtons(root) {
2472
+ root.querySelectorAll("[data-copy-preset-id]").forEach((button) => button.addEventListener("click", async (event) => {
2473
+ event.preventDefault();
2474
+ event.stopPropagation();
2475
+ const presetId = button.dataset.copyPresetId || "";
2476
+ const defaultText = button.textContent;
2477
+ try {
2478
+ await copyText(presetId);
2479
+ button.textContent = t("copyTaskNameSuccess");
2480
+ } catch {
2481
+ button.textContent = t("copyTaskNameFailed");
2482
+ }
2483
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
2484
+ }));
2485
+ root.querySelectorAll("[data-copy-preset-command]").forEach((button) => button.addEventListener("click", async (event) => {
2486
+ event.preventDefault();
2487
+ event.stopPropagation();
2488
+ const command = button.dataset.copyPresetCommand || "";
2489
+ const defaultText = button.textContent;
2490
+ try {
2491
+ await copyText(command);
2492
+ button.textContent = t("copyTaskNameSuccess");
2493
+ } catch {
2494
+ button.textContent = t("copyTaskNameFailed");
2495
+ }
2496
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
2497
+ }));
2498
+ }
2499
+
1932
2500
  function bindRepairPromptButtons(root) {
1933
2501
  root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
1934
2502
  event.preventDefault();