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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +33 -1
- package/README.zh-CN.md +23 -1
- package/SKILL.md +9 -8
- package/docs-release/architecture/overview.md +1 -1
- package/docs-release/architecture/overview.zh-CN.md +1 -1
- package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
- package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
- package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
- package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
- package/docs-release/architecture/system-explainer/README.md +67 -0
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
- package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
- package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
- package/docs-release/guides/agent-installation.en-US.md +8 -7
- package/docs-release/guides/agent-installation.md +9 -7
- package/docs-release/guides/preset-development.md +26 -2
- package/docs-release/guides/task-state-machine.en-US.md +30 -13
- package/docs-release/guides/task-state-machine.md +30 -13
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
- package/package.json +3 -2
- package/references/harness-ledger.md +1 -1
- package/scripts/commands/migration-command.mjs +30 -0
- package/scripts/commands/task-command.mjs +26 -25
- package/scripts/harness.mjs +7 -3
- package/scripts/lib/capability-registry.mjs +17 -21
- package/scripts/lib/check-module-parallel.mjs +9 -16
- package/scripts/lib/check-profiles.mjs +35 -81
- package/scripts/lib/check-task-contracts.mjs +13 -5
- package/scripts/lib/core-shared.mjs +55 -2
- package/scripts/lib/dashboard-data.mjs +126 -18
- package/scripts/lib/dashboard-workbench.mjs +80 -1
- package/scripts/lib/dashboard-writer.mjs +6 -2
- package/scripts/lib/git-status-summary.mjs +1 -1
- package/scripts/lib/governance-sync.mjs +180 -83
- package/scripts/lib/harness-core.mjs +1 -0
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-engine.mjs +5 -8
- package/scripts/lib/preset-registry.mjs +188 -39
- package/scripts/lib/review-confirm-git-gate.mjs +1 -1
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +7 -4
- package/scripts/lib/task-audit-metadata.mjs +385 -0
- package/scripts/lib/task-audit-migration.mjs +350 -0
- package/scripts/lib/task-completion-consistency.mjs +11 -1
- package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
- package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
- package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
- package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
- package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
- package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
- package/scripts/lib/task-lifecycle.mjs +114 -147
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +54 -68
- package/scripts/lib/task-scanner.mjs +70 -143
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
- package/templates/AGENTS.md.template +7 -5
- package/templates/dashboard/assets/app-src/00-state.js +12 -0
- package/templates/dashboard/assets/app-src/10-router.js +3 -0
- package/templates/dashboard/assets/app-src/20-overview.js +7 -3
- package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
- package/templates/dashboard/assets/app-src/55-presets.js +375 -0
- package/templates/dashboard/assets/app-src/60-shared.js +3 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
- package/templates/dashboard/assets/app.css +583 -0
- package/templates/dashboard/assets/app.css.manifest.json +1 -0
- package/templates/dashboard/assets/app.js +578 -10
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/i18n.js +140 -2
- package/templates/planning/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/review.md +0 -18
- package/templates/planning/task_plan.md +4 -43
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +29 -2
- package/templates-zh-CN/AGENTS.md.template +7 -5
- package/templates-zh-CN/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- package/templates-zh-CN/planning/module_session_prompt.md +1 -0
- package/templates-zh-CN/planning/review.md +0 -18
- package/templates-zh-CN/planning/task_plan.md +3 -63
- package/templates-zh-CN/planning/visual_map.md +14 -7
- package/templates-zh-CN/planning/visual_map.simple.md +48 -0
- 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 {
|
|
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((
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
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))
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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)
|
|
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: {
|