coding-agent-harness 1.0.2 → 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 +32 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +244 -87
- package/README.zh-CN.md +77 -35
- package/SKILL.md +32 -24
- 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/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/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +39 -15
- package/docs-release/guides/agent-installation.md +43 -16
- 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 +238 -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 +224 -0
- package/docs-release/guides/task-state-machine.md +231 -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/INDEX.md +60 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +10 -4
- 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 +126 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +328 -0
- package/scripts/harness.mjs +59 -260
- package/scripts/lib/capability-registry.mjs +82 -28
- package/scripts/lib/check-module-parallel.mjs +230 -0
- package/scripts/lib/check-profiles.mjs +90 -228
- package/scripts/lib/check-task-contracts.mjs +55 -0
- package/scripts/lib/core-shared.mjs +65 -2
- package/scripts/lib/dashboard-data.mjs +155 -24
- package/scripts/lib/dashboard-workbench.mjs +131 -12
- package/scripts/lib/dashboard-writer.mjs +20 -4
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +611 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +6 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/migration-support.mjs +1 -1
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +494 -0
- package/scripts/lib/preset-registry.mjs +776 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +105 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- 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 +26 -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/create-task-helpers.mjs +67 -0
- package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
- 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/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +338 -477
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +455 -0
- package/scripts/lib/task-scanner.mjs +193 -372
- 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 +43 -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 +24 -18
- package/templates/dashboard/assets/app-src/00-state.js +13 -0
- package/templates/dashboard/assets/app-src/10-router.js +5 -1
- package/templates/dashboard/assets/app-src/20-overview.js +18 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +286 -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/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 +302 -29
- package/templates/dashboard/assets/app.css +1501 -376
- package/templates/dashboard/assets/app.css.manifest.json +10 -0
- package/templates/dashboard/assets/app.js +1240 -101
- package/templates/dashboard/assets/app.manifest.json +2 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +346 -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 +489 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +263 -23
- 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/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +41 -0
- package/templates/planning/task_plan.md +5 -21
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +31 -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 +25 -19
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- 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/module_session_prompt.md +1 -0
- package/templates-zh-CN/planning/review.md +41 -1
- package/templates-zh-CN/planning/task_plan.md +4 -44
- 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/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 +33 -7
- 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
|
@@ -5,13 +5,21 @@ import http from "node:http";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { URL } from "node:url";
|
|
7
7
|
import { confirmTaskReview } from "./task-lifecycle.mjs";
|
|
8
|
+
import { createLessonSedimentationTask } from "./task-lesson-sedimentation.mjs";
|
|
8
9
|
import { normalizeTarget } from "./core-shared.mjs";
|
|
9
10
|
import { collectTasks } from "./task-scanner.mjs";
|
|
10
11
|
import { writeDashboardFolder } from "./dashboard-data.mjs";
|
|
12
|
+
import {
|
|
13
|
+
checkPresetPackage,
|
|
14
|
+
installPresetPackage,
|
|
15
|
+
listPresetPackages,
|
|
16
|
+
seedBundledPresets,
|
|
17
|
+
uninstallPresetPackage,
|
|
18
|
+
} from "./preset-registry.mjs";
|
|
11
19
|
|
|
12
20
|
const jsonHeaders = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
|
|
13
21
|
|
|
14
|
-
export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench" } = {}) {
|
|
22
|
+
export async function serveDashboardWorkbench(outDir, targetInput, { host = "127.0.0.1", port = 0, localeOverride = "", autoRefresh = false, open = false, label = "dashboard workbench", recoverGeneratedDashboard = false } = {}) {
|
|
15
23
|
if (host !== "127.0.0.1") throw new Error("dashboard workbench only supports --host 127.0.0.1");
|
|
16
24
|
const target = normalizeTarget(targetInput);
|
|
17
25
|
const outputDir = path.resolve(outDir);
|
|
@@ -19,7 +27,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
|
|
|
19
27
|
const options = localeOverride ? { localeOverride } : {};
|
|
20
28
|
let snapshotVersion = Date.now();
|
|
21
29
|
const regenerate = () => {
|
|
22
|
-
writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true });
|
|
30
|
+
writeDashboardFolder(outputDir, targetInput, { ...options, workbenchRuntime: true, recoverGeneratedDashboard });
|
|
23
31
|
snapshotVersion = Date.now();
|
|
24
32
|
};
|
|
25
33
|
regenerate();
|
|
@@ -35,7 +43,7 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
|
|
|
35
43
|
writeJson(response, 200, {
|
|
36
44
|
mode: "workbench",
|
|
37
45
|
csrfToken,
|
|
38
|
-
writableActions: ["review-complete"],
|
|
46
|
+
writableActions: ["review-complete", "lesson-sedimentation-task", "preset-check", "preset-install", "preset-seed", "preset-uninstall"],
|
|
39
47
|
target: target.projectRoot,
|
|
40
48
|
autoRefresh: autoRefresh === true,
|
|
41
49
|
snapshotVersion,
|
|
@@ -52,8 +60,8 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
|
|
|
52
60
|
writeJson(response, 404, { error: "Task not found" });
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
55
|
-
if (!
|
|
56
|
-
writeJson(response, 409,
|
|
63
|
+
if (!isTaskInReviewQueue(task)) {
|
|
64
|
+
writeJson(response, 409, reviewQueueRejectionPayload(task));
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
67
|
if (task.reviewStatus === "confirmed") {
|
|
@@ -71,14 +79,102 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
|
|
|
71
79
|
return;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
if (requestUrl.pathname === "/api/tasks/lesson-sedimentation" && request.method === "POST") {
|
|
83
|
+
assertTrustedWorkbenchRequest(request, { origin, csrfToken });
|
|
84
|
+
const body = await readJsonBody(request);
|
|
85
|
+
const taskId = String(body.taskId || "");
|
|
86
|
+
const candidateId = String(body.candidateId || "");
|
|
87
|
+
const task = collectTasks(target).find((item) => item.id === taskId);
|
|
88
|
+
if (!task) {
|
|
89
|
+
writeJson(response, 404, { error: "Task not found" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!candidateId) {
|
|
93
|
+
writeJson(response, 400, { error: "Missing lesson candidate id" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const result = createLessonSedimentationTask(target.projectRoot, taskId, candidateId, {
|
|
97
|
+
title: body.title || "",
|
|
98
|
+
});
|
|
99
|
+
regenerate();
|
|
100
|
+
writeJson(response, 200, result);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
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
|
+
|
|
74
170
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
75
171
|
writeJson(response, 405, { error: "Method not allowed" });
|
|
76
172
|
return;
|
|
77
173
|
}
|
|
78
174
|
serveStaticFile(response, outputDir, requestUrl.pathname, request.method === "HEAD");
|
|
79
175
|
} catch (error) {
|
|
80
|
-
const status = /CSRF|Origin|Host/.test(error.message) ? 403 : 400;
|
|
81
|
-
writeJson(response, status,
|
|
176
|
+
const status = error.status || (/CSRF|Origin|Host/.test(error.message) ? 403 : 400);
|
|
177
|
+
writeJson(response, status, errorPayload(error));
|
|
82
178
|
}
|
|
83
179
|
});
|
|
84
180
|
|
|
@@ -103,11 +199,26 @@ export async function serveDashboardWorkbench(outDir, targetInput, { host = "127
|
|
|
103
199
|
await new Promise(() => {});
|
|
104
200
|
}
|
|
105
201
|
|
|
106
|
-
function
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
|
|
208
|
+
function isTaskInReviewQueue(task) {
|
|
209
|
+
return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function reviewQueueRejectionPayload(task) {
|
|
213
|
+
return {
|
|
214
|
+
error: "Review completion is only available for tasks in the review queue.",
|
|
215
|
+
reviewQueueState: task?.reviewQueueState || "unknown",
|
|
216
|
+
taskQueues: Array.isArray(task?.taskQueues) ? task.taskQueues : [],
|
|
217
|
+
queueReasons: Array.isArray(task?.queueReasons) ? task.queueReasons : [],
|
|
218
|
+
repairPrompt: task?.repairPrompt || "",
|
|
219
|
+
reviewStatus: task?.reviewStatus || "unknown",
|
|
220
|
+
taskId: task?.id || "",
|
|
221
|
+
};
|
|
111
222
|
}
|
|
112
223
|
|
|
113
224
|
function startPollingWatch(root, regenerate) {
|
|
@@ -202,6 +313,14 @@ function writeJson(response, status, payload) {
|
|
|
202
313
|
response.end(`${JSON.stringify(payload)}\n`);
|
|
203
314
|
}
|
|
204
315
|
|
|
316
|
+
function errorPayload(error) {
|
|
317
|
+
const payload = { error: error.message };
|
|
318
|
+
if (error.code) payload.code = error.code;
|
|
319
|
+
if (Array.isArray(error.recovery) && error.recovery.length > 0) payload.recovery = error.recovery;
|
|
320
|
+
if (error.details) payload.details = error.details;
|
|
321
|
+
return payload;
|
|
322
|
+
}
|
|
323
|
+
|
|
205
324
|
function mimeType(filePath) {
|
|
206
325
|
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
|
|
207
326
|
if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
|
|
@@ -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
|
|
|
@@ -18,15 +21,17 @@ export function writeDashboardDirectory(outDir, bundle, options = {}) {
|
|
|
18
21
|
const target = path.resolve(outDir);
|
|
19
22
|
assertSafeDashboardTarget(target, options);
|
|
20
23
|
if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
|
24
|
+
fs.mkdirSync(target, { recursive: true });
|
|
25
|
+
fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
|
|
21
26
|
copyDashboardAssets(target, options);
|
|
22
27
|
fs.writeFileSync(path.join(target, "assets/app.js"), readDashboardApp(dashboardTemplateRootForLocale(options.locale)));
|
|
23
28
|
fs.writeFileSync(path.join(target, "index.html"), renderDashboardIndex(options.locale, options));
|
|
24
|
-
fs.writeFileSync(path.join(target, dashboardMarker), "generated dashboard directory\n");
|
|
25
29
|
writeJsonFile(path.join(target, "data/status.json"), bundle.status);
|
|
26
30
|
writeJsonFile(path.join(target, "data/tables.json"), bundle.tables);
|
|
27
31
|
writeJsonFile(path.join(target, "data/documents.json"), bundle.documents);
|
|
28
32
|
writeJsonFile(path.join(target, "data/graph.json"), bundle.graph);
|
|
29
33
|
writeJsonFile(path.join(target, "data/adoption.json"), bundle.adoption);
|
|
34
|
+
writeJsonFile(path.join(target, "data/presetCatalog.json"), bundle.presetCatalog);
|
|
30
35
|
fs.writeFileSync(
|
|
31
36
|
path.join(target, "assets/dashboard-data.js"),
|
|
32
37
|
`window.__HARNESS_DASHBOARD__ = ${JSON.stringify(bundle, null, 2)};\n`,
|
|
@@ -113,7 +118,7 @@ function renderDashboardIndex(locale = "en-US", options = {}) {
|
|
|
113
118
|
function readDashboardApp(templateRoot) {
|
|
114
119
|
const manifestPath = path.join(templateRoot, "assets/app.manifest.json");
|
|
115
120
|
if (!fs.existsSync(manifestPath)) return fs.readFileSync(path.join(templateRoot, "assets/app.js"), "utf8");
|
|
116
|
-
const manifest =
|
|
121
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
117
122
|
if (!Array.isArray(manifest) || manifest.length === 0) throw new Error(`Invalid dashboard app manifest: ${manifestPath}`);
|
|
118
123
|
return `${manifest.map((relativePath) => {
|
|
119
124
|
const source = path.join(templateRoot, "assets", relativePath);
|
|
@@ -156,12 +161,23 @@ function assertSafeDashboardTarget(target, options) {
|
|
|
156
161
|
if (fs.existsSync(target)) {
|
|
157
162
|
const entries = fs.readdirSync(target);
|
|
158
163
|
const hasMarker = fs.existsSync(path.join(target, dashboardMarker));
|
|
159
|
-
|
|
164
|
+
const canRecoverGeneratedDashboard = options.recoverGeneratedDashboard === true && looksLikeGeneratedDashboardDirectory(target);
|
|
165
|
+
if (entries.length > 0 && !hasMarker && !canRecoverGeneratedDashboard) {
|
|
160
166
|
throw new Error(`Refusing to overwrite non-dashboard directory: ${target}`);
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
|
|
171
|
+
function looksLikeGeneratedDashboardDirectory(target) {
|
|
172
|
+
return [
|
|
173
|
+
"index.html",
|
|
174
|
+
"README.md",
|
|
175
|
+
"assets/app.js",
|
|
176
|
+
"assets/dashboard-data.js",
|
|
177
|
+
"data/status.json",
|
|
178
|
+
].every((relativePath) => fs.existsSync(path.join(target, relativePath)));
|
|
179
|
+
}
|
|
180
|
+
|
|
165
181
|
function dashboardTemplateRootForLocale(locale = "en-US") {
|
|
166
182
|
return dashboardTemplateRoot;
|
|
167
183
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { inspectGit } from "./governance-sync.mjs";
|
|
4
|
+
|
|
5
|
+
export function summarizeGitState(target) {
|
|
6
|
+
const state = inspectGit(target.projectRoot);
|
|
7
|
+
if (!state.inGit) {
|
|
8
|
+
return {
|
|
9
|
+
summary: {
|
|
10
|
+
inGit: false,
|
|
11
|
+
root: "",
|
|
12
|
+
dirty: false,
|
|
13
|
+
entries: [],
|
|
14
|
+
blocksCliAutoCommit: false,
|
|
15
|
+
},
|
|
16
|
+
warnings: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const targetRoot = real(target.projectRoot);
|
|
20
|
+
const gitRoot = real(state.gitRoot);
|
|
21
|
+
const rootMatches = gitRoot === targetRoot;
|
|
22
|
+
const entries = rootMatches ? state.entries.map((entry) => ({
|
|
23
|
+
path: entry.path,
|
|
24
|
+
index: entry.index,
|
|
25
|
+
worktree: entry.worktree,
|
|
26
|
+
})) : [];
|
|
27
|
+
const dirty = entries.length > 0;
|
|
28
|
+
const warnings = [];
|
|
29
|
+
if (dirty) {
|
|
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
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
summary: {
|
|
34
|
+
inGit: true,
|
|
35
|
+
root: rootMatches ? "TARGET:." : "outside-target",
|
|
36
|
+
dirty,
|
|
37
|
+
entries,
|
|
38
|
+
blocksCliAutoCommit: !rootMatches || dirty,
|
|
39
|
+
},
|
|
40
|
+
warnings,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function real(filePath) {
|
|
45
|
+
return fs.realpathSync(filePath);
|
|
46
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
normalizeTarget,
|
|
5
|
+
readBundledTemplate,
|
|
6
|
+
todayDate,
|
|
7
|
+
toPosix,
|
|
8
|
+
} from "./core-shared.mjs";
|
|
9
|
+
import { splitMarkdownRow } from "./markdown-utils.mjs";
|
|
10
|
+
import { collectTasks } from "./task-scanner.mjs";
|
|
11
|
+
import {
|
|
12
|
+
beginGovernanceSync,
|
|
13
|
+
commitGovernanceSync,
|
|
14
|
+
moduleGeneratedIndexSurfaces,
|
|
15
|
+
releaseGovernanceSync,
|
|
16
|
+
} from "./governance-sync.mjs";
|
|
17
|
+
import { markdownCell } from "./task-lifecycle/text-utils.mjs";
|
|
18
|
+
|
|
19
|
+
export function rebuildGovernanceIndexes(targetInput, { dryRun = false, archive = false, apply = false } = {}) {
|
|
20
|
+
const target = normalizeTarget(targetInput);
|
|
21
|
+
const effectiveApply = Boolean(apply && !dryRun);
|
|
22
|
+
const context = beginGovernanceSync(target, { operation: "governance rebuild", dryRun: !effectiveApply });
|
|
23
|
+
try {
|
|
24
|
+
const tasks = collectTasks(target)
|
|
25
|
+
.filter((task) => task.deletionState !== "deleted")
|
|
26
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
27
|
+
const surfaces = [...governanceSurfaces(target, tasks), ...moduleGeneratedIndexSurfaces(target, tasks)];
|
|
28
|
+
const archiveDir = archive ? uniqueArchiveDir(target) : "";
|
|
29
|
+
const changes = surfaces.map((surface) => ({
|
|
30
|
+
surface: surface.surface,
|
|
31
|
+
destination: surface.relative,
|
|
32
|
+
action: surface.archiveOnly
|
|
33
|
+
? apply ? "archive-legacy-governance-table" : "would-archive-legacy-governance-table"
|
|
34
|
+
: apply ? "rebuild-governance-index" : "would-rebuild-governance-index",
|
|
35
|
+
generatedRows: surface.rows.length,
|
|
36
|
+
archive: archive ? `${archiveDir}/${surface.relative}` : "",
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
if (!effectiveApply) {
|
|
40
|
+
return { dryRun: true, archive, applied: false, archiveDir, changes };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allowed = [];
|
|
44
|
+
for (const surface of surfaces) {
|
|
45
|
+
if (archive && fs.existsSync(surface.absolute)) {
|
|
46
|
+
const archivePath = path.join(target.projectRoot, archiveDir, surface.relative);
|
|
47
|
+
fs.mkdirSync(path.dirname(archivePath), { recursive: true });
|
|
48
|
+
fs.copyFileSync(surface.absolute, archivePath);
|
|
49
|
+
allowed.push(toPosix(path.relative(target.projectRoot, archivePath)));
|
|
50
|
+
}
|
|
51
|
+
if (surface.archiveOnly) {
|
|
52
|
+
if (archive && fs.existsSync(surface.absolute)) {
|
|
53
|
+
fs.rmSync(surface.absolute);
|
|
54
|
+
allowed.push(surface.relative);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
fs.mkdirSync(path.dirname(surface.absolute), { recursive: true });
|
|
59
|
+
fs.writeFileSync(surface.absolute, surface.content);
|
|
60
|
+
allowed.push(surface.relative);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const commit = commitGovernanceSync(context, allowed, { message: "chore(harness): rebuild generated governance indexes" });
|
|
64
|
+
return { dryRun: false, archive, applied: true, archiveDir, changes, commit };
|
|
65
|
+
} finally {
|
|
66
|
+
releaseGovernanceSync(context);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function governanceSurfaces(target, tasks) {
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
surface: "harness-ledger",
|
|
74
|
+
absolute: path.join(target.docsRoot, "Harness-Ledger.md"),
|
|
75
|
+
relative: toPosix(path.relative(target.projectRoot, path.join(target.docsRoot, "Harness-Ledger.md"))),
|
|
76
|
+
rows: tasks.map(ledgerRow),
|
|
77
|
+
content: replaceTableRows(readBundledTemplate("templates/ledger/Harness-Ledger.md"), /^ID$/i, tasks.map(ledgerRow)),
|
|
78
|
+
},
|
|
79
|
+
...legacyFeatureSurfaces(target),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function legacyFeatureSurfaces(target) {
|
|
84
|
+
return [
|
|
85
|
+
["legacy-feature-ssot", path.join(target.docsRoot, "09-PLANNING/Feature-SSoT.md")],
|
|
86
|
+
["legacy-private-feature-ssot", path.join(target.docsRoot, "09-PLANNING/Private-Feature-SSoT.md")],
|
|
87
|
+
].filter(([, absolute]) => fs.existsSync(absolute)).map(([surface, absolute]) => ({
|
|
88
|
+
surface,
|
|
89
|
+
absolute,
|
|
90
|
+
relative: toPosix(path.relative(target.projectRoot, absolute)),
|
|
91
|
+
rows: [],
|
|
92
|
+
content: "",
|
|
93
|
+
archiveOnly: true,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function replaceTableRows(content, headerPattern, rows) {
|
|
98
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
99
|
+
for (let index = 0; index < lines.length - 1; index += 1) {
|
|
100
|
+
if (!lines[index].trim().startsWith("|")) continue;
|
|
101
|
+
const header = splitMarkdownRow(lines[index]);
|
|
102
|
+
if (!header.some((cell) => headerPattern.test(cell))) continue;
|
|
103
|
+
const separator = splitMarkdownRow(lines[index + 1]);
|
|
104
|
+
if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
|
105
|
+
let end = index + 2;
|
|
106
|
+
while (end < lines.length && lines[end].trim().startsWith("|")) end += 1;
|
|
107
|
+
const fitted = rows.map((row) => fitRow(row, header.length));
|
|
108
|
+
lines.splice(index + 2, end - index - 2, ...fitted.map((row) => `| ${row.join(" | ")} |`));
|
|
109
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
110
|
+
}
|
|
111
|
+
return `${String(content || "").trimEnd()}\n\n${rows.map((row) => `| ${row.join(" | ")} |`).join("\n")}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fitRow(row, length) {
|
|
115
|
+
const next = row.map((cell) => markdownCell(cell));
|
|
116
|
+
while (next.length < length) next.push("");
|
|
117
|
+
return next.slice(0, length);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ledgerRow(task) {
|
|
121
|
+
const plan = stripTarget(task.taskPlanPath || `${stripTarget(task.path)}/task_plan.md`);
|
|
122
|
+
const scope = task.module ? "module" : "task";
|
|
123
|
+
const moduleKey = task.module || "none";
|
|
124
|
+
return [
|
|
125
|
+
taskLedgerId(task),
|
|
126
|
+
scope,
|
|
127
|
+
moduleKey,
|
|
128
|
+
task.title || task.shortId || task.id,
|
|
129
|
+
mapLedgerState(task.state),
|
|
130
|
+
Array.isArray(task.taskQueues) && task.taskQueues.length ? task.taskQueues.join(",") : "none",
|
|
131
|
+
plan,
|
|
132
|
+
task.reviewStatus === "confirmed" ? stripTarget(task.reviewPath) : (task.reviewStatus || "pending"),
|
|
133
|
+
task.lessonCandidateDecisionComplete ? "checked" : (task.lessonCandidateStatus || "pending"),
|
|
134
|
+
task.walkthroughPath ? stripTarget(task.walkthroughPath) : (task.closeoutStatus || "pending"),
|
|
135
|
+
residual(task),
|
|
136
|
+
todayDate(),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function residual(task) {
|
|
141
|
+
if (Array.isArray(task.stateConflicts) && task.stateConflicts.length) return `state-conflicts:${task.stateConflicts.length}`;
|
|
142
|
+
if (Array.isArray(task.materialIssues) && task.materialIssues.length) return `material-issues:${task.materialIssues.length}`;
|
|
143
|
+
return "none";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function taskLedgerId(task) {
|
|
147
|
+
return `HL-${taskSlug(task)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function taskSlug(task) {
|
|
151
|
+
return String(task.shortId || task.id || "task").replace(/^TASKS\//, "").replace(/^MODULES\//, "").replace(/[^A-Za-z0-9-]+/g, "-").slice(0, 72);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function stripTarget(value) {
|
|
155
|
+
return String(value || "").replace(/^TARGET:/, "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mapLedgerState(state) {
|
|
159
|
+
if (state === "in_progress") return "active";
|
|
160
|
+
if (state === "review") return "review";
|
|
161
|
+
if (state === "done") return "closed";
|
|
162
|
+
if (state === "blocked") return "blocked";
|
|
163
|
+
return "planned";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function uniqueArchiveDir(target) {
|
|
167
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(".", "-");
|
|
168
|
+
const base = `docs/01-GOVERNANCE/archive/generated-governance-tables/${stamp}`;
|
|
169
|
+
for (let index = 0; index < 100; index += 1) {
|
|
170
|
+
const candidate = index === 0 ? base : `${base}-${String(index).padStart(2, "0")}`;
|
|
171
|
+
if (!fs.existsSync(path.join(target.projectRoot, candidate))) return candidate;
|
|
172
|
+
}
|
|
173
|
+
throw new Error("Unable to allocate a unique governance table archive directory");
|
|
174
|
+
}
|