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
@@ -1,8 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import {
5
+ legacyChecker,
4
6
  repoRoot,
7
+ builtinPresetRoot,
5
8
  normalizeTarget,
9
+ projectPresetRoot,
6
10
  readFileSafe,
7
11
  sanitizeText,
8
12
  sanitizeDeep,
@@ -15,24 +19,29 @@ import {
15
19
  legacyVisualRoadmapFile,
16
20
  lessonCandidatesFile,
17
21
  longRunningTaskContractFile,
22
+ userPresetRoot,
18
23
  } from "./core-shared.mjs";
19
24
  import {
20
25
  parseAllMarkdownTables,
21
26
  getCell,
22
27
  splitDependencies,
23
28
  } from "./markdown-utils.mjs";
24
- import { readCapabilityRegistry } from "./capability-registry.mjs";
25
- import { buildStatus } from "./check-profiles.mjs";
29
+ import { readCapabilityRegistry, validateCapabilities } from "./capability-registry.mjs";
30
+ import { buildStatusData } from "./status-builder.mjs";
26
31
  import {
27
32
  listTaskPlanPaths,
28
33
  parseTaskState,
29
34
  isActiveTaskState,
30
35
  } from "./task-scanner.mjs";
31
36
  import { writeDashboardDirectory, writeDashboardFile } from "./dashboard-writer.mjs";
37
+ import { listPresetPackageLayers } from "./preset-registry.mjs";
38
+ import { validateGovernanceTableBoundaries } from "./governance-table-boundary.mjs";
39
+ import { summarizeGitState } from "./git-status-summary.mjs";
32
40
 
33
- export function collectMarkdownDocuments(target) {
34
- const docs = collectDashboardDocumentPaths(target);
35
- return docs.map((file, index) => {
41
+ export function collectMarkdownDocuments(target, options = {}) {
42
+ const docs = collectDashboardDocumentPaths(target, options);
43
+ return docs.map((entry, index) => {
44
+ const file = typeof entry === "string" ? entry : entry.file;
36
45
  const content = sanitizeText(readFileSafe(file));
37
46
  const source = prefixedPath(target, file);
38
47
  return {
@@ -41,12 +50,14 @@ export function collectMarkdownDocuments(target) {
41
50
  title: titleFromMarkdown(content, path.basename(file)),
42
51
  type: documentKind(source),
43
52
  content,
53
+ ...(entry.partial ? { partial: true, partialReason: entry.partialReason || "partial", taskId: entry.taskId || "" } : {}),
44
54
  };
45
55
  });
46
56
  }
47
57
 
48
- function collectDashboardDocumentPaths(target) {
58
+ function collectDashboardDocumentPaths(target, options = {}) {
49
59
  const selected = new Set();
60
+ const partial = new Map();
50
61
  const addDocsPath = (relativePath) => {
51
62
  const file = path.join(target.docsRoot, relativePath);
52
63
  if (fs.existsSync(file)) selected.add(file);
@@ -66,21 +77,38 @@ function collectDashboardDocumentPaths(target) {
66
77
  if (path.basename(file).startsWith("_")) continue;
67
78
  selected.add(file);
68
79
  }
69
- for (const taskPlanPath of listTaskPlanPaths(target)) {
80
+ const tasksByPlanPath = new Map((options.tasks || []).map((task) => [
81
+ path.join(target.projectRoot, String(task.taskPlanPath || "").replace(/^TARGET:/, "")),
82
+ task,
83
+ ]));
84
+ for (const taskPlanPath of options.taskPlanPaths || listTaskPlanPaths(target)) {
70
85
  const taskDir = path.dirname(taskPlanPath);
71
86
  const progress = readFileSafe(path.join(taskDir, "progress.md"));
72
87
  const state = parseTaskState(progress);
73
88
  const active = isActiveTaskState(state);
74
- const documentNames = active
75
- ? ["brief.md", "task_plan.md", "execution_strategy.md", visualMapFile, legacyVisualRoadmapFile, lessonCandidatesFile, longRunningTaskContractFile, "progress.md", "review.md", "findings.md"]
89
+ const task = tasksByPlanPath.get(taskPlanPath);
90
+ const historicalClosed = !active && task?.closeoutStatus === "closed";
91
+ const documentNames = historicalClosed
92
+ ? ["brief.md"]
76
93
  : ["brief.md", "task_plan.md", "execution_strategy.md", visualMapFile, legacyVisualRoadmapFile, lessonCandidatesFile, longRunningTaskContractFile, "progress.md", "review.md", "findings.md"];
77
94
  for (const fileName of documentNames) {
78
95
  const file = path.join(taskDir, fileName);
79
- if (fs.existsSync(file)) selected.add(file);
96
+ if (fs.existsSync(file)) {
97
+ selected.add(file);
98
+ if (historicalClosed) {
99
+ partial.set(file, {
100
+ partial: true,
101
+ partialReason: "historical-closed",
102
+ taskId: task?.id || path.basename(taskDir),
103
+ });
104
+ }
105
+ }
80
106
  }
81
- for (const indexFile of ["references/INDEX.md", "artifacts/INDEX.md"]) {
82
- const file = path.join(taskDir, indexFile);
83
- if (fs.existsSync(file)) selected.add(file);
107
+ if (!historicalClosed) {
108
+ for (const indexFile of ["references/INDEX.md", "artifacts/INDEX.md"]) {
109
+ const file = path.join(taskDir, indexFile);
110
+ if (fs.existsSync(file)) selected.add(file);
111
+ }
84
112
  }
85
113
  }
86
114
  for (const file of walkFiles(path.join(target.docsRoot, "09-PLANNING/MODULES"))) {
@@ -94,7 +122,8 @@ function collectDashboardDocumentPaths(target) {
94
122
  .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`))
95
123
  .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
96
124
  .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
97
- .sort();
125
+ .sort()
126
+ .map((file) => ({ file, ...(partial.get(file) || {}) }));
98
127
  }
99
128
 
100
129
  function documentKind(source) {
@@ -145,7 +174,17 @@ export function collectGraph(status, tables = { tables: [] }) {
145
174
  addNode({ id: `task:${task.id}`, type: "task", label: task.title, state: task.state, completion: task.completion });
146
175
  for (const phase of task.phases || []) {
147
176
  const phaseId = `phase:${task.id}:${phase.id}`;
148
- addNode({ id: phaseId, type: "phase", label: phase.id, state: phase.state, completion: phase.completion, taskId: task.id });
177
+ addNode({
178
+ id: phaseId,
179
+ type: "phase",
180
+ label: phase.id,
181
+ state: phase.state,
182
+ completion: phase.completion,
183
+ kind: phase.kind,
184
+ actor: phase.actor,
185
+ exitCommand: phase.exitCommand,
186
+ taskId: task.id,
187
+ });
149
188
  addEdge({ from: `task:${task.id}`, to: phaseId, type: "contains" });
150
189
  for (const dependency of phase.dependsOn || []) {
151
190
  addEdge({ from: `phase:${task.id}:${dependency}`, to: phaseId, type: "depends_on" });
@@ -386,13 +425,82 @@ function warningAction(message) {
386
425
  }
387
426
 
388
427
  export function buildDashboardBundle(targetInput, options = {}) {
389
- const status = buildStatus(targetInput, options);
390
428
  const target = normalizeTarget(targetInput);
391
- const documents = { documents: collectMarkdownDocuments(target) };
429
+ const taskPlanPaths = listTaskPlanPaths(target);
430
+ const capabilityState = validateCapabilities(target);
431
+ const gitState = summarizeGitState(target);
432
+ const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
433
+ const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || declaredCapabilities.has("safe-adoption"));
434
+ const legacy = shouldRunLegacy ? runDashboardLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
435
+ const legacyWarnings = legacy.status === "fail" ? [`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`] : [];
436
+ const governanceBoundaries = validateGovernanceTableBoundaries(target);
437
+ const status = buildStatusData(target, {
438
+ ...options,
439
+ capabilityState,
440
+ gitState,
441
+ taskPlanPaths,
442
+ legacy,
443
+ failures: [...capabilityState.failures, ...governanceBoundaries.failures],
444
+ warnings: [...capabilityState.warnings, ...legacyWarnings, ...governanceBoundaries.warnings, ...gitState.warnings],
445
+ });
446
+ const documents = { documents: collectMarkdownDocuments(target, { taskPlanPaths, tasks: status.tasks }) };
392
447
  const tables = collectTables(documents.documents);
393
448
  const graph = collectGraph(status, tables);
394
449
  const adoption = collectAdoption(status);
395
- return sanitizeDeep({ status, tables, documents, graph, adoption });
450
+ const presetCatalog = collectPresetCatalog(targetInput, target, options);
451
+ return sanitizeDeep({ status, tables, documents, graph, adoption, presetCatalog });
452
+ }
453
+
454
+ function runDashboardLegacyCheck(target) {
455
+ const checkTarget = target.docsOnly ? target.projectRoot : target.input;
456
+ const result = spawnSync(process.execPath, [legacyChecker, checkTarget], {
457
+ cwd: repoRoot,
458
+ encoding: "utf8",
459
+ });
460
+ return {
461
+ status: result.status === 0 ? "pass" : "fail",
462
+ code: result.status ?? 1,
463
+ stdout: result.stdout || "",
464
+ stderr: result.stderr || "",
465
+ };
466
+ }
467
+
468
+ export function collectPresetCatalog(targetInput, target = normalizeTarget(targetInput), options = {}) {
469
+ const home = options.home || "";
470
+ const presets = listPresetPackageLayers({ targetInput: target.projectRoot, home }).map((preset) => ({
471
+ key: `${preset.source}:${preset.id}`,
472
+ id: preset.id,
473
+ version: preset.version,
474
+ source: preset.source,
475
+ effective: preset.effective === true,
476
+ purpose: preset.purpose,
477
+ compatibleBudgets: preset.compatibleBudgets,
478
+ manifestPath: preset.manifestRelativePath,
479
+ manifestSha256: preset.manifestSha256,
480
+ taskKind: preset.task?.kind || "",
481
+ inputCount: Object.keys(preset.inputs || {}).length,
482
+ referenceCount: Object.keys(preset.resources?.references || {}).length,
483
+ artifactCount: Object.keys(preset.resources?.artifacts || {}).length,
484
+ writeScopeCount: Object.keys(preset.writeScopes || {}).length,
485
+ evidenceFileCount: Object.keys(preset.evidence?.files || {}).length,
486
+ requiredReadCount: Array.isArray(preset.context?.requiredReads) ? preset.context.requiredReads.length : 0,
487
+ checkStatus: "unknown",
488
+ }));
489
+ const countSource = (source) => presets.filter((preset) => preset.source === source).length;
490
+ return {
491
+ summary: {
492
+ total: presets.length,
493
+ project: countSource("project"),
494
+ user: countSource("user"),
495
+ builtin: countSource("builtin"),
496
+ },
497
+ roots: [
498
+ { source: "project", path: projectPresetRoot(target.projectRoot) },
499
+ { source: "user", path: home ? path.join(path.resolve(home), ".coding-agent-harness/presets") : userPresetRoot },
500
+ { source: "builtin", path: builtinPresetRoot },
501
+ ],
502
+ presets,
503
+ };
396
504
  }
397
505
 
398
506
  export function writeDashboardFolder(outDir, targetInput, options = {}) {
@@ -9,6 +9,13 @@ import { createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
9
9
  import { normalizeTarget } from "./core-shared.mjs";
10
10
  import { collectTasks } from "./task-scanner.mjs";
11
11
  import { writeDashboardFolder } from "./dashboard-data.mjs";
12
+ import {
13
+ checkPresetPackage,
14
+ installPresetPackage,
15
+ listPresetPackages,
16
+ seedBundledPresets,
17
+ uninstallPresetPackage,
18
+ } from "./preset-registry.mjs";
12
19
 
13
20
  const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
14
21
 
@@ -36,7 +43,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
36
43
  writeJson(response, 200, {
37
44
  mode: "workbench",
38
45
  csrfToken,
39
- writableActions: ["review-complete", "lesson-sedimentation-task"],
46
+ writableActions: ["review-complete", "lesson-sedimentation-task", "preset-check", "preset-install", "preset-seed", "preset-uninstall"],
40
47
  target: target.projectRoot,
41
48
  autoRefresh: autoRefresh === true,
42
49
  snapshotVersion,
@@ -94,6 +101,72 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
94
101
  return;
95
102
  }
96
103
 
104
+ if (requestUrl.pathname === "/api/presets/check" && request.method === "POST") {
105
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
106
+ const body = await readJsonBody(request);
107
+ const id = String(body.id || "");
108
+ if (!id) {
109
+ writeJson(response, 400, { error: "Missing preset id" });
110
+ return;
111
+ }
112
+ const result = checkPresetPackage(id, { targetInput: target.projectRoot });
113
+ writeJson(response, 200, result);
114
+ return;
115
+ }
116
+
117
+ if (requestUrl.pathname === "/api/presets/install" && request.method === "POST") {
118
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
119
+ const body = await readJsonBody(request);
120
+ const source = String(body.source || "");
121
+ if (!source) {
122
+ writeJson(response, 400, { error: "Missing preset source" });
123
+ return;
124
+ }
125
+ if (/^https?:\/\//i.test(source)) {
126
+ writeJson(response, 400, { error: "Network preset sources are not supported by the dashboard workbench." });
127
+ return;
128
+ }
129
+ const scope = normalizePresetScope(body.scope);
130
+ const result = installPresetPackage(source, { force: body.force === true, scope, targetInput: target.projectRoot });
131
+ regenerate();
132
+ writeJson(response, 200, { ...result, scope });
133
+ return;
134
+ }
135
+
136
+ if (requestUrl.pathname === "/api/presets/seed" && request.method === "POST") {
137
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
138
+ const body = await readJsonBody(request);
139
+ const scope = normalizePresetScope(body.scope);
140
+ const result = seedBundledPresets({ force: body.force === true, dryRun: body.dryRun === true, scope, targetInput: target.projectRoot });
141
+ if (body.dryRun !== true) regenerate();
142
+ writeJson(response, 200, result);
143
+ return;
144
+ }
145
+
146
+ if (requestUrl.pathname === "/api/presets/uninstall" && request.method === "POST") {
147
+ assertTrustedWorkbenchRequest(request, { origin, csrfToken });
148
+ const body = await readJsonBody(request);
149
+ const id = String(body.id || "");
150
+ if (!id) {
151
+ writeJson(response, 400, { error: "Missing preset id" });
152
+ return;
153
+ }
154
+ if (String(body.confirmText || "").trim() !== id) {
155
+ writeJson(response, 400, { error: "Preset uninstall requires typing the preset id." });
156
+ return;
157
+ }
158
+ const scope = normalizePresetScope(body.scope);
159
+ const discovered = listPresetPackages({ targetInput: target.projectRoot }).find((preset) => preset.id === id);
160
+ if (discovered?.source === "builtin") {
161
+ writeJson(response, 409, { error: "Builtin preset cannot be uninstalled from the dashboard workbench.", id, source: "builtin" });
162
+ return;
163
+ }
164
+ const result = uninstallPresetPackage(id, { scope, targetInput: target.projectRoot });
165
+ regenerate();
166
+ writeJson(response, 200, { ...result, scope });
167
+ return;
168
+ }
169
+
97
170
  if (request.method !== "GET" && request.method !== "HEAD") {
98
171
  writeJson(response, 405, { error: "Method not allowed" });
99
172
  return;
@@ -126,6 +199,12 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
126
199
  await new Promise(() => {});
127
200
  }
128
201
 
202
+ function normalizePresetScope(value) {
203
+ const scope = String(value || "project");
204
+ if (scope !== "project" && scope !== "user") throw new Error(`Invalid preset scope: ${scope}`);
205
+ return scope;
206
+ }
207
+
129
208
  function isTaskInReviewQueue(task) {
130
209
  return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
131
210
  }
@@ -1,7 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readJsonSafe } from "./core-shared.mjs";
3
5
 
4
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const repoRoot = path.resolve(__dirname, "../..");
5
8
  const dashboardTemplateRoot = path.join(repoRoot, "templates/dashboard");
6
9
  const dashboardMarker = ".harness-dashboard";
7
10
 
@@ -28,6 +31,7 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
28
31
  writeJsonFile(path.join(target, "data/documents.json"), bundle.documents);
29
32
  writeJsonFile(path.join(target, "data/graph.json"), bundle.graph);
30
33
  writeJsonFile(path.join(target, "data/adoption.json"), bundle.adoption);
34
+ writeJsonFile(path.join(target, "data/presetCatalog.json"), bundle.presetCatalog);
31
35
  fs.writeFileSync(
32
36
  path.join(target, "assets/dashboard-data.js"),
33
37
  `window.__HARNESS_DASHBOARD__ = ${JSON.stringify(bundle, null, 2)};\n`,
@@ -114,7 +118,7 @@ function renderDashboardIndex(locale = "en-US", options = {}) {
114
118
  function readDashboardApp(templateRoot) {
115
119
  const manifestPath = path.join(templateRoot, "assets/app.manifest.json");
116
120
  if (!fs.existsSync(manifestPath)) return fs.readFileSync(path.join(templateRoot, "assets/app.js"), "utf8");
117
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
121
+ const manifest = readJsonSafe(manifestPath, null);
118
122
  if (!Array.isArray(manifest) || manifest.length === 0) throw new Error(`Invalid dashboard app manifest: ${manifestPath}`);
119
123
  return `${manifest.map((relativePath) => {
120
124
  const source = path.join(templateRoot, "assets", relativePath);
@@ -27,7 +27,7 @@ export function summarizeGitState(target) {
27
27
  const dirty = entries.length > 0;
28
28
  const warnings = [];
29
29
  if (dirty) {
30
- warnings.push(`dirty-state: ${entries.length} uncommitted Git path(s) will block CLI-owned auto-commit; commit them or record owner/no-commit reason before lifecycle commands.`);
30
+ warnings.push(`dirty-state: ${entries.length} uncommitted Git path(s) may block CLI-owned auto-commit when they overlap a command write scope or are staged; commit them or record owner/no-commit reason before lifecycle commands.`);
31
31
  }
32
32
  return {
33
33
  summary: {