coding-agent-harness 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +98 -0
- package/README.md +211 -86
- package/README.zh-CN.md +54 -34
- package/SKILL.md +25 -18
- package/docs-release/README.md +9 -5
- package/docs-release/architecture/overview.md +17 -5
- package/docs-release/architecture/overview.zh-CN.md +9 -5
- package/docs-release/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +31 -8
- package/docs-release/guides/agent-installation.md +34 -9
- package/docs-release/guides/contributing.md +100 -0
- package/docs-release/guides/contributing.zh-CN.md +99 -0
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
- package/docs-release/guides/document-audience-and-surfaces.md +3 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
- package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
- package/docs-release/guides/migration-playbook.en-US.md +14 -15
- package/docs-release/guides/migration-playbook.md +14 -15
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
- package/docs-release/guides/parent-control-repository-pattern.md +7 -5
- package/docs-release/guides/preset-development.md +214 -0
- package/docs-release/guides/repository-operating-models.en-US.md +5 -4
- package/docs-release/guides/repository-operating-models.md +5 -4
- package/docs-release/guides/task-state-machine.en-US.md +207 -0
- package/docs-release/guides/task-state-machine.md +214 -0
- package/docs-release/intl/en-US.md +1 -1
- package/docs-release/intl/zh-CN.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +8 -3
- package/presets/legacy-migration/checks/preset-check.mjs +3 -0
- package/presets/legacy-migration/preset.yaml +134 -0
- package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
- package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
- package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
- package/presets/legacy-migration/templates/findings.seed.md +17 -0
- package/presets/legacy-migration/templates/review.seed.md +12 -0
- package/presets/legacy-migration/templates/task_plan.append.md +9 -0
- package/presets/legacy-migration/templates/visual_map.append.md +12 -0
- package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
- package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
- package/presets/lesson-sedimentation/preset.yaml +23 -0
- package/presets/lesson-sedimentation/templates/prompt.md +23 -0
- package/presets/module/preset.yaml +25 -0
- package/presets/module/templates/execution_strategy.append.md +8 -0
- package/presets/module/templates/task_plan.append.md +17 -0
- package/presets/standard-task/preset.yaml +31 -0
- package/presets/standard-task/templates/task_plan.append.md +7 -0
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +2 -2
- package/references/delivery-operating-model-standard.md +3 -3
- package/references/docs-directory-standard.md +6 -7
- package/references/harness-ledger.md +53 -96
- package/references/lessons-governance.md +88 -93
- package/references/module-parallel-standard.md +14 -14
- package/references/planning-loop.md +12 -6
- package/references/pull-request-standard.md +118 -0
- package/references/repo-governance-standard.md +11 -2
- package/references/review-routing-standard.md +7 -1
- package/references/ssot-governance.md +67 -59
- package/references/taskr-gap-analysis.md +600 -0
- package/references/walkthrough-closeout.md +7 -7
- package/scripts/check-harness.mjs +40 -301
- package/scripts/commands/dashboard-command.mjs +67 -0
- package/scripts/commands/migration-command.mjs +96 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +327 -0
- package/scripts/harness.mjs +55 -260
- package/scripts/lib/capability-registry.mjs +66 -8
- package/scripts/lib/check-module-parallel.mjs +237 -0
- package/scripts/lib/check-profiles.mjs +61 -153
- package/scripts/lib/check-task-contracts.mjs +47 -0
- package/scripts/lib/core-shared.mjs +10 -0
- package/scripts/lib/dashboard-data.mjs +29 -6
- package/scripts/lib/dashboard-workbench.mjs +52 -12
- package/scripts/lib/dashboard-writer.mjs +14 -2
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +514 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +5 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/migration-support.mjs +1 -1
- package/scripts/lib/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +497 -0
- package/scripts/lib/preset-registry.mjs +627 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-dashboard-renderer.mjs +102 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- package/scripts/lib/task-completion-consistency.mjs +16 -0
- package/scripts/lib/task-index.mjs +93 -0
- package/scripts/lib/task-lesson-candidates.mjs +242 -0
- package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
- package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +297 -403
- package/scripts/lib/task-review-model.mjs +469 -0
- package/scripts/lib/task-scanner.mjs +130 -236
- package/scripts/lib/task-tombstone-commands.mjs +140 -0
- package/scripts/postinstall.mjs +14 -0
- package/skills/preset-creator/SKILL.md +179 -0
- package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
- package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
- package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
- package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
- package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
- package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
- package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
- package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
- package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
- package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
- package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
- package/templates/AGENTS.md.template +19 -15
- package/templates/dashboard/assets/app-src/00-state.js +1 -0
- package/templates/dashboard/assets/app-src/10-router.js +2 -1
- package/templates/dashboard/assets/app-src/20-overview.js +11 -5
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
- package/templates/dashboard/assets/app-src/45-review.js +241 -22
- package/templates/dashboard/assets/app-src/50-migration.js +24 -10
- package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
- package/templates/dashboard/assets/app.css +698 -156
- package/templates/dashboard/assets/app.css.manifest.json +9 -0
- package/templates/dashboard/assets/app.js +662 -91
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
- package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
- package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
- package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +123 -21
- package/templates/ledger/Harness-Ledger.md +13 -25
- package/templates/lessons/lesson-arch-process-change.md +1 -1
- package/templates/lessons/lesson-new-doc.md +1 -1
- package/templates/lessons/lesson-ref-change.md +1 -1
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- package/templates/planning/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +59 -0
- package/templates/planning/task_plan.md +36 -13
- package/templates/reference/execution-workflow-standard.md +4 -3
- package/templates/reference/pull-request-standard.md +80 -0
- package/templates/reference/repo-governance-standard.md +7 -6
- package/templates/reference/review-routing-standard.md +6 -0
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/verifier/verifier-output.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +20 -16
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/execution_strategy.md +30 -0
- package/templates-zh-CN/planning/lesson_candidates.md +18 -6
- package/templates-zh-CN/planning/review.md +59 -1
- package/templates-zh-CN/planning/task_plan.md +30 -10
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/docs-library-standard.md +1 -1
- package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
- package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
- package/templates-zh-CN/reference/pull-request-standard.md +106 -0
- package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
- package/templates-zh-CN/reference/review-routing-standard.md +8 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/scripts/smoke-dashboard.mjs +0 -92
- package/scripts/test-harness.mjs +0 -1395
- package/templates/ssot/Feature-SSoT.md +0 -43
- package/templates/ssot/Lessons-SSoT.md +0 -44
- package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
- package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function stripMarkdownCode(value) {
|
|
5
|
+
return String(value || "").replace(/`/g, "").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function modulePromptBlock(content, key) {
|
|
9
|
+
const heading = `## Module: ${key}`;
|
|
10
|
+
const start = content.indexOf(heading);
|
|
11
|
+
if (start < 0) return "";
|
|
12
|
+
const rest = content.slice(start + heading.length);
|
|
13
|
+
const next = rest.search(/\n## Module: /);
|
|
14
|
+
return next >= 0 ? rest.slice(0, next) : rest;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listModuleTaskPlans({ targetRoot, rel, filePath }) {
|
|
18
|
+
const modulesRoot = filePath("docs/09-PLANNING/MODULES");
|
|
19
|
+
if (!fs.existsSync(modulesRoot)) return [];
|
|
20
|
+
const results = [];
|
|
21
|
+
function walk(dir) {
|
|
22
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
23
|
+
const full = path.join(dir, entry);
|
|
24
|
+
const relativePath = rel(path.relative(targetRoot, full));
|
|
25
|
+
const stat = fs.statSync(full);
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
if (relativePath.includes("/_archive/") || relativePath.endsWith("/_task-template")) continue;
|
|
28
|
+
walk(full);
|
|
29
|
+
} else if (/\/TASKS\/[^/]+\/task_plan\.md$/.test(relativePath)) {
|
|
30
|
+
results.push(relativePath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
walk(modulesRoot);
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseModuleTaskPath(taskPlanPath) {
|
|
39
|
+
const match = taskPlanPath.match(/^docs\/09-PLANNING\/MODULES\/([^/]+)\/TASKS\/([^/]+)\/task_plan\.md$/);
|
|
40
|
+
if (!match) return null;
|
|
41
|
+
return { moduleKey: match[1], taskDir: match[2] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractStepId(taskPlanContent, taskDir) {
|
|
45
|
+
const fromPlan = taskPlanContent.match(/^- Step ID:\s*`?([A-Z]{2,5}-\d{2})`?/m);
|
|
46
|
+
if (fromPlan) return fromPlan[1];
|
|
47
|
+
const fromModuleSection = taskPlanContent.match(/^- Step:\s*`?([A-Z]{2,5}-\d{2})`?/m);
|
|
48
|
+
if (fromModuleSection) return fromModuleSection[1];
|
|
49
|
+
const fromDir = taskDir.match(/^([A-Z]{2,5}-\d{2})-/);
|
|
50
|
+
return fromDir ? fromDir[1] : "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readTaskProgress(taskPlanPath, { exists, read }) {
|
|
54
|
+
const progressPath = taskPlanPath.replace(/task_plan\.md$/, "progress.md");
|
|
55
|
+
return exists(progressPath) ? read(progressPath) : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeModuleTaskStatus(status) {
|
|
59
|
+
const value = String(status || "").trim().toLowerCase();
|
|
60
|
+
const aliases = new Map([
|
|
61
|
+
["未开始", "not-started"],
|
|
62
|
+
["未启动", "not-started"],
|
|
63
|
+
["进行中", "in-progress"],
|
|
64
|
+
["开发中", "in-progress"],
|
|
65
|
+
["规划审查", "planning-review"],
|
|
66
|
+
["已完成", "completed"],
|
|
67
|
+
["完成", "completed"],
|
|
68
|
+
["已关闭", "closed"],
|
|
69
|
+
["关闭", "closed"],
|
|
70
|
+
["已阻塞", "blocked"],
|
|
71
|
+
["阻塞", "blocked"],
|
|
72
|
+
]);
|
|
73
|
+
return aliases.get(value) || value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readTaskProgressStatus(taskPlanPath, context) {
|
|
77
|
+
const progress = readTaskProgress(taskPlanPath, context);
|
|
78
|
+
if (!progress) return "";
|
|
79
|
+
const match = progress.match(/^##\s*(?:Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
|
|
80
|
+
return match ? normalizeModuleTaskStatus(stripMarkdownCode(match[1])) : "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isActiveModuleTaskStatus(status) {
|
|
84
|
+
if (!status) return false;
|
|
85
|
+
return !new Set([
|
|
86
|
+
"not-started",
|
|
87
|
+
"blocked-not-started",
|
|
88
|
+
"complete",
|
|
89
|
+
"completed",
|
|
90
|
+
"closed",
|
|
91
|
+
"closed-with-residual",
|
|
92
|
+
"closed-local-only",
|
|
93
|
+
"superseded",
|
|
94
|
+
]).has(status);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function hasPendingCoordinatorHandoff(taskPlanContent, progressContent) {
|
|
98
|
+
const combined = `${taskPlanContent}\n${progressContent}`;
|
|
99
|
+
return /Coordinator Handoff/i.test(combined) && /pending-coordinator-pass/i.test(combined);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkModuleTaskSsotIndex(registryRows, context) {
|
|
103
|
+
const { exists, read, fail, warn, requireGlobalModuleSync } = context;
|
|
104
|
+
const registryByModule = new Map(registryRows.map((cells) => [cells[0], cells]));
|
|
105
|
+
const ledgerContent = exists("docs/Harness-Ledger.md") ? read("docs/Harness-Ledger.md") : "";
|
|
106
|
+
const taskPlans = listModuleTaskPlans(context);
|
|
107
|
+
|
|
108
|
+
for (const taskPlanPath of taskPlans) {
|
|
109
|
+
const parsed = parseModuleTaskPath(taskPlanPath);
|
|
110
|
+
if (!parsed) continue;
|
|
111
|
+
const { moduleKey, taskDir } = parsed;
|
|
112
|
+
const modulePlanPath = `docs/09-PLANNING/MODULES/${moduleKey}/module_plan.md`;
|
|
113
|
+
if (!exists(modulePlanPath)) continue;
|
|
114
|
+
|
|
115
|
+
const taskPlan = read(taskPlanPath);
|
|
116
|
+
const taskProgress = readTaskProgress(taskPlanPath, context);
|
|
117
|
+
const taskProgressStatus = readTaskProgressStatus(taskPlanPath, context);
|
|
118
|
+
const taskIsActive = isActiveModuleTaskStatus(taskProgressStatus);
|
|
119
|
+
const stepId = extractStepId(taskPlan, taskDir);
|
|
120
|
+
if (!stepId) {
|
|
121
|
+
if (taskIsActive) {
|
|
122
|
+
fail(`${taskPlanPath} does not expose a Step ID and task directory does not start with <PREFIX-NN>`);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const modulePlan = read(modulePlanPath);
|
|
128
|
+
const moduleRelativeTaskPlan = `TASKS/${taskDir}/task_plan.md`;
|
|
129
|
+
if (!modulePlan.includes(stepId) || !modulePlan.includes(moduleRelativeTaskPlan)) {
|
|
130
|
+
fail(`${modulePlanPath} does not index ${stepId} task plan ${moduleRelativeTaskPlan}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!taskIsActive) continue;
|
|
134
|
+
|
|
135
|
+
const registryRow = registryByModule.get(moduleKey);
|
|
136
|
+
const reviewPath = taskPlanPath.replace(/task_plan\.md$/, "review.md");
|
|
137
|
+
const registrySynced = Boolean(registryRow && registryRow[4] === stepId);
|
|
138
|
+
const ledgerSynced = ledgerContent.includes(taskPlanPath) && (!exists(reviewPath) || ledgerContent.includes(reviewPath));
|
|
139
|
+
if (registrySynced && ledgerSynced) continue;
|
|
140
|
+
|
|
141
|
+
if (requireGlobalModuleSync) {
|
|
142
|
+
if (!registryRow) {
|
|
143
|
+
fail(`docs/09-PLANNING/Module-Registry.md does not include active module ${moduleKey} for ${taskPlanPath}`);
|
|
144
|
+
} else if (registryRow[4] !== stepId) {
|
|
145
|
+
fail(`docs/09-PLANNING/Module-Registry.md row ${moduleKey} current step is ${registryRow[4]}, but active task is ${stepId}`);
|
|
146
|
+
}
|
|
147
|
+
if (!ledgerContent.includes(taskPlanPath)) {
|
|
148
|
+
fail(`docs/Harness-Ledger.md does not index active module task plan ${taskPlanPath}`);
|
|
149
|
+
}
|
|
150
|
+
if (exists(reviewPath) && !ledgerContent.includes(reviewPath)) {
|
|
151
|
+
fail(`docs/Harness-Ledger.md does not index active module review ${reviewPath}`);
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasPendingCoordinatorHandoff(taskPlan, taskProgress)) {
|
|
157
|
+
warn(`${taskPlanPath} has pending coordinator handoff; run coordinator pass before final integration or set HARNESS_REQUIRE_GLOBAL_MODULE_SYNC=1 for strict gate`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
fail(`${taskPlanPath} is active but is neither globally synced nor marked with Coordinator Handoff: pending-coordinator-pass`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function checkModuleParallelStructure(context) {
|
|
165
|
+
const { exists, read, fail, requireFile, markdownTable } = context;
|
|
166
|
+
if (!exists("docs/09-PLANNING/Module-Registry.md")) return;
|
|
167
|
+
|
|
168
|
+
requireFile("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md");
|
|
169
|
+
const hasPromptPack = exists("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md");
|
|
170
|
+
for (const templateFile of [
|
|
171
|
+
"docs/09-PLANNING/MODULES/_task-template/task_plan.md",
|
|
172
|
+
"docs/09-PLANNING/MODULES/_task-template/progress.md",
|
|
173
|
+
"docs/09-PLANNING/MODULES/_task-template/findings.md",
|
|
174
|
+
"docs/09-PLANNING/MODULES/_task-template/review.md",
|
|
175
|
+
]) {
|
|
176
|
+
requireFile(templateFile);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const registryContent = read("docs/09-PLANNING/Module-Registry.md");
|
|
180
|
+
for (const term of ["PREFIX", "Current Step", "Status", "Write Scope"]) {
|
|
181
|
+
if (!registryContent.includes(term)) {
|
|
182
|
+
fail(`docs/09-PLANNING/Module-Registry.md missing registry column or section: ${term}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const registryRows = markdownTable(registryContent)
|
|
187
|
+
.filter((cells) => cells.length >= 6)
|
|
188
|
+
.filter((cells) => /^(_shared|[a-z][a-z0-9-]*)$/.test(cells[0] || "") && /^[A-Z]{2,5}$/.test(cells[2] || ""));
|
|
189
|
+
|
|
190
|
+
if (registryRows.length === 0) {
|
|
191
|
+
fail("docs/09-PLANNING/Module-Registry.md has no active module rows");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const promptPack = hasPromptPack ? read("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md") : "";
|
|
195
|
+
if (hasPromptPack && !/Subagent Worker Invariant|worker[\s\S]{0,120}worktree[\s\S]{0,120}commit SHA/i.test(promptPack)) {
|
|
196
|
+
fail("docs/09-PLANNING/MODULES/Session-Prompt-Pack.md missing subagent worker worktree/commit handoff rule");
|
|
197
|
+
}
|
|
198
|
+
for (const cells of registryRows) {
|
|
199
|
+
const [key, , prefix, branch, currentStep, status] = cells;
|
|
200
|
+
requireFile(`docs/09-PLANNING/MODULES/${key}/module_plan.md`);
|
|
201
|
+
if (!/^(planned|in-progress|paused|completed)$/.test(status)) {
|
|
202
|
+
fail(`docs/09-PLANNING/Module-Registry.md row ${key} has invalid status: ${status}`);
|
|
203
|
+
}
|
|
204
|
+
if (currentStep !== `${prefix}-00` && !currentStep.startsWith(`${prefix}-`)) {
|
|
205
|
+
fail(`docs/09-PLANNING/Module-Registry.md row ${key} current step does not match prefix ${prefix}: ${currentStep}`);
|
|
206
|
+
}
|
|
207
|
+
const branchName = stripMarkdownCode(branch);
|
|
208
|
+
if (!branchName.startsWith("codex/")) {
|
|
209
|
+
fail(`docs/09-PLANNING/Module-Registry.md row ${key} branch must use codex/ prefix: ${branch}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const block = modulePromptBlock(promptPack, key);
|
|
213
|
+
if (!block) {
|
|
214
|
+
if (!exists(`docs/09-PLANNING/MODULES/${key}/session_prompt.md`)) {
|
|
215
|
+
fail(`missing module session prompt for ${key}`);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (const term of [
|
|
220
|
+
"Current Step",
|
|
221
|
+
branchName,
|
|
222
|
+
"Preflight:",
|
|
223
|
+
"Before code edits:",
|
|
224
|
+
"Write scope:",
|
|
225
|
+
"Forbidden without coordination:",
|
|
226
|
+
"Shared Coordination:",
|
|
227
|
+
"Verification:",
|
|
228
|
+
"Closeout:",
|
|
229
|
+
"Stop conditions:",
|
|
230
|
+
]) {
|
|
231
|
+
if (!block.includes(term)) {
|
|
232
|
+
fail(`module session prompt for ${key} missing required term: ${term}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
checkModuleTaskSsotIndex(registryRows, context);
|
|
237
|
+
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
legacyChecker,
|
|
7
7
|
visualMapFile,
|
|
8
8
|
legacyVisualRoadmapFile,
|
|
9
|
-
lessonCandidatesFile,
|
|
10
9
|
allowedReviewDispositions,
|
|
11
10
|
allowedPhaseStates,
|
|
12
11
|
allowedEvidenceStatus,
|
|
@@ -23,19 +22,27 @@ import {
|
|
|
23
22
|
firstColumn,
|
|
24
23
|
contentHasAny,
|
|
25
24
|
} from "./markdown-utils.mjs";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "./
|
|
25
|
+
import { capabilityDefinitions, validateCapabilities } from "./capability-registry.mjs";
|
|
26
|
+
import { readPresetPackage } from "./preset-registry.mjs";
|
|
27
|
+
import { validateTaskPresetAuditSnapshot } from "./preset-audit-contracts.mjs";
|
|
28
|
+
import { validatePresetResourcesForTask } from "./preset-resource-contracts.mjs";
|
|
30
29
|
import {
|
|
31
30
|
collectTasks,
|
|
32
31
|
listTaskPlanPaths,
|
|
33
|
-
parseTaskBudget,
|
|
34
|
-
parseTaskContractInfo,
|
|
35
32
|
readVisualMapContractFile,
|
|
36
33
|
parsePhases,
|
|
37
34
|
taskCutoverCounters,
|
|
38
35
|
} from "./task-scanner.mjs";
|
|
36
|
+
import {
|
|
37
|
+
normalizeReviewBoolean,
|
|
38
|
+
reviewFindingColumns,
|
|
39
|
+
} from "./task-review-model.mjs";
|
|
40
|
+
import { validateTaskCompletionConsistency } from "./task-completion-consistency.mjs";
|
|
41
|
+
import { validatePlanContracts } from "./check-task-contracts.mjs";
|
|
42
|
+
import { validateGovernanceTableBoundaries } from "./governance-table-boundary.mjs";
|
|
43
|
+
import { validateSubagentAuthorization } from "./subagent-authorization-audit.mjs";
|
|
44
|
+
import { summarizeGitState } from "./git-status-summary.mjs";
|
|
45
|
+
export { renderDashboard } from "./status-dashboard-renderer.mjs";
|
|
39
46
|
|
|
40
47
|
export function runLegacyCheck(target) {
|
|
41
48
|
const checkTarget = target.docsOnly ? target.projectRoot : target.input;
|
|
@@ -94,10 +101,10 @@ export function validateReviewSchema(target, { strict = true } = {}) {
|
|
|
94
101
|
}
|
|
95
102
|
const { header, rows } = tableAfterHeading(content, /^ID$/i);
|
|
96
103
|
if (rows.length === 0) continue;
|
|
97
|
-
const severityIndex = getColumnAny(header,
|
|
98
|
-
const openIndex = getColumnAny(header,
|
|
99
|
-
const dispositionIndex = getColumnAny(header,
|
|
100
|
-
const blocksIndex = getColumnAny(header,
|
|
104
|
+
const severityIndex = getColumnAny(header, reviewFindingColumns.severity);
|
|
105
|
+
const openIndex = getColumnAny(header, reviewFindingColumns.open);
|
|
106
|
+
const dispositionIndex = getColumnAny(header, reviewFindingColumns.disposition);
|
|
107
|
+
const blocksIndex = getColumnAny(header, reviewFindingColumns.blocksRelease);
|
|
101
108
|
const followUpIndex = getColumnAny(header, ["Follow-up", "跟进"]);
|
|
102
109
|
const evidenceCheckedIndex = getColumnAny(header, ["Evidence Checked", "已检查证据"]);
|
|
103
110
|
if ([severityIndex, openIndex, dispositionIndex, blocksIndex].some((index) => index < 0)) {
|
|
@@ -108,9 +115,9 @@ export function validateReviewSchema(target, { strict = true } = {}) {
|
|
|
108
115
|
const id = row[0] || "";
|
|
109
116
|
const severity = row[severityIndex] || "";
|
|
110
117
|
if (!/^P[0-3]$/.test(severity) && !/^(R|SR)-\d+/i.test(id)) continue;
|
|
111
|
-
const open = (row[openIndex] || "")
|
|
118
|
+
const open = normalizeReviewBoolean(row[openIndex] || "");
|
|
112
119
|
const disposition = (row[dispositionIndex] || "").toLowerCase();
|
|
113
|
-
const blocks = (row[blocksIndex] || "")
|
|
120
|
+
const blocks = normalizeReviewBoolean(row[blocksIndex] || "");
|
|
114
121
|
const followUp = row[followUpIndex] || "";
|
|
115
122
|
if (!/^P[0-3]$/.test(severity)) report(`${relative} ${id} invalid severity: ${severity}`);
|
|
116
123
|
if (!["yes", "no"].includes(open)) report(`${relative} ${id} invalid Open value: ${open}`);
|
|
@@ -128,11 +135,12 @@ export function validateReviewSchema(target, { strict = true } = {}) {
|
|
|
128
135
|
for (const ref of refs) {
|
|
129
136
|
if (ref !== "none" && /^E-\d+/i.test(ref) && !evidenceIds.has(ref)) {
|
|
130
137
|
failures.push(`${relative} ${id} references missing evidence id: ${ref}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
136
144
|
return { failures, warnings };
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -177,33 +185,6 @@ export function validateVisualMaps(target) {
|
|
|
177
185
|
return { failures, warnings };
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
export function validatePlanContracts(target, { strict = true } = {}) {
|
|
181
|
-
const failures = [];
|
|
182
|
-
const warnings = [];
|
|
183
|
-
const report = (message) => {
|
|
184
|
-
if (strict) failures.push(message);
|
|
185
|
-
else warnings.push(`adoption-needed: ${message}`);
|
|
186
|
-
};
|
|
187
|
-
for (const taskPlanPath of listTaskPlanPaths(target)) {
|
|
188
|
-
const taskDir = path.dirname(taskPlanPath);
|
|
189
|
-
const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
|
|
190
|
-
const taskPlanContent = readFileSafe(taskPlanPath);
|
|
191
|
-
const budget = parseTaskBudget(taskPlanContent);
|
|
192
|
-
const taskContract = parseTaskContractInfo(taskPlanContent);
|
|
193
|
-
if (!taskContract.generated) {
|
|
194
|
-
warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
|
|
195
|
-
}
|
|
196
|
-
const requiredFiles = budget === "simple" ? [visualMapFile] : ["execution_strategy.md", visualMapFile, lessonCandidatesFile];
|
|
197
|
-
for (const fileName of requiredFiles) {
|
|
198
|
-
if (!fs.existsSync(path.join(taskDir, fileName))) {
|
|
199
|
-
if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);
|
|
200
|
-
else report(`${relativeDir} missing ${fileName}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return { failures, warnings };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
188
|
export function validateTaskPresetContracts(target) {
|
|
208
189
|
const failures = [];
|
|
209
190
|
const allowedMigrationLevels = new Set([
|
|
@@ -214,13 +195,38 @@ export function validateTaskPresetContracts(target) {
|
|
|
214
195
|
]);
|
|
215
196
|
for (const task of collectTasks(target)) {
|
|
216
197
|
if (!task.taskPreset || task.taskPreset === "none") continue;
|
|
198
|
+
let presetPackage = null;
|
|
199
|
+
try {
|
|
200
|
+
presetPackage = readPresetPackage(task.taskPreset, { targetInput: target.projectRoot });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
failures.push(`${task.path} unsupported Task Preset: ${task.taskPreset} (${error.message})`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (presetPackage?.task?.kind && task.taskKind !== presetPackage.task.kind) {
|
|
206
|
+
failures.push(`${task.path} ${task.taskPreset} preset Task Kind mismatch: expected ${presetPackage.task.kind}, got ${task.taskKind || "(missing)"}`);
|
|
207
|
+
}
|
|
208
|
+
if (String(task.presetVersion || "") !== String(presetPackage.version)) {
|
|
209
|
+
failures.push(`${task.path} ${task.taskPreset} preset missing Preset Version ${presetPackage.version}`);
|
|
210
|
+
}
|
|
211
|
+
if (task.taskPreset !== "lesson-sedimentation" && (presetPackage.evidence?.bundleDir || presetPackage.audit?.evidenceFiles?.length || Object.keys(presetPackage.evidence?.files || {}).length)) {
|
|
212
|
+
if (!task.evidenceBundle) failures.push(`${task.path} ${task.taskPreset} preset missing Evidence Bundle`);
|
|
213
|
+
else if (!fs.existsSync(path.join(target.projectRoot, String(task.evidenceBundle).replace(/^TARGET:/, "").replace(/^\/+/, "")))) {
|
|
214
|
+
failures.push(`${task.path} ${task.taskPreset} preset Evidence Bundle missing: ${task.evidenceBundle}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (task.taskPreset !== "lesson-sedimentation") {
|
|
218
|
+
failures.push(...validateTaskPresetAuditSnapshot(target, task, presetPackage));
|
|
219
|
+
}
|
|
220
|
+
failures.push(...validatePresetResourcesForTask(target, task, presetPackage));
|
|
221
|
+
if (task.taskPreset === "lesson-sedimentation") {
|
|
222
|
+
if (!["standard", "complex"].includes(task.budget)) failures.push(`${task.path} lesson-sedimentation preset requires Selected budget: standard or complex`);
|
|
223
|
+
if (!task.taskPlanPath) failures.push(`${task.path} lesson-sedimentation preset missing task plan`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
217
226
|
if (task.taskPreset !== "legacy-migration") {
|
|
218
|
-
failures.push(`${task.path} unsupported Task Preset: ${task.taskPreset}`);
|
|
219
227
|
continue;
|
|
220
228
|
}
|
|
221
229
|
if (task.budget !== "complex") failures.push(`${task.path} legacy-migration preset requires Selected budget: complex`);
|
|
222
|
-
if (!task.presetVersion) failures.push(`${task.path} legacy-migration preset missing Preset Version`);
|
|
223
|
-
if (!task.taskKind || task.taskKind === "general") failures.push(`${task.path} legacy-migration preset missing Task Kind`);
|
|
224
230
|
if (!allowedMigrationLevels.has(task.migrationTargetLevel)) {
|
|
225
231
|
failures.push(`${task.path} legacy-migration preset invalid Migration Target Level: ${task.migrationTargetLevel || "(missing)"}`);
|
|
226
232
|
}
|
|
@@ -228,9 +234,7 @@ export function validateTaskPresetContracts(target) {
|
|
|
228
234
|
if (achievedLevel !== "pending" && !allowedMigrationLevels.has(achievedLevel)) {
|
|
229
235
|
failures.push(`${task.path} legacy-migration preset invalid Migration Achieved Level: ${achievedLevel || "(missing)"}`);
|
|
230
236
|
}
|
|
231
|
-
if (!task.
|
|
232
|
-
failures.push(`${task.path} legacy-migration preset missing Evidence Bundle`);
|
|
233
|
-
} else if (!task.migrationSnapshot?.evidencePresent) {
|
|
237
|
+
if (task.evidenceBundle && !task.migrationSnapshot?.evidencePresent) {
|
|
234
238
|
failures.push(`${task.path} legacy-migration preset Evidence Bundle missing: ${task.evidenceBundle}`);
|
|
235
239
|
} else if (!task.migrationSnapshot?.sessionPresent) {
|
|
236
240
|
failures.push(`${task.path} legacy-migration preset Evidence Bundle missing session.json`);
|
|
@@ -315,6 +319,7 @@ export function validateContextDocs(target, { strict = true } = {}) {
|
|
|
315
319
|
|
|
316
320
|
export function buildStatus(targetInput, options = {}) {
|
|
317
321
|
const target = normalizeTarget(targetInput);
|
|
322
|
+
const gitState = summarizeGitState(target);
|
|
318
323
|
const capabilityState = validateCapabilities(target);
|
|
319
324
|
const declaredCapabilities = new Set(capabilityState.registry.capabilities.map((capability) => capability.name));
|
|
320
325
|
const safeAdoptionMode = declaredCapabilities.has("safe-adoption");
|
|
@@ -326,14 +331,19 @@ export function buildStatus(targetInput, options = {}) {
|
|
|
326
331
|
const planContracts = validatePlanContracts(target, { strict: contractStrict });
|
|
327
332
|
const presetContracts = validateTaskPresetContracts(target);
|
|
328
333
|
const contextDocs = validateContextDocs(target, { strict: contractStrict });
|
|
329
|
-
const
|
|
330
|
-
const
|
|
334
|
+
const governanceBoundaries = validateGovernanceTableBoundaries(target);
|
|
335
|
+
const subagentAuthorization = validateSubagentAuthorization(target, { strict: contractStrict });
|
|
336
|
+
const failures = [...capabilityState.failures, ...reviews.failures, ...visualMaps.failures, ...planContracts.failures, ...presetContracts.failures, ...contextDocs.failures, ...governanceBoundaries.failures, ...subagentAuthorization.failures];
|
|
337
|
+
const warnings = [...capabilityState.warnings, ...reviews.warnings, ...visualMaps.warnings, ...planContracts.warnings, ...presetContracts.warnings, ...contextDocs.warnings, ...governanceBoundaries.warnings, ...subagentAuthorization.warnings, ...gitState.warnings];
|
|
331
338
|
if (legacy.status === "fail") {
|
|
332
339
|
if (options.strictLegacy) failures.push("legacy check failed");
|
|
333
340
|
else warnings.push(`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`);
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
const tasks = collectTasks(target);
|
|
344
|
+
const taskCompletionConsistency = validateTaskCompletionConsistency(tasks);
|
|
345
|
+
failures.push(...taskCompletionConsistency.failures);
|
|
346
|
+
warnings.push(...taskCompletionConsistency.warnings);
|
|
337
347
|
const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
|
|
338
348
|
const briefMissing = tasks.length - briefReady;
|
|
339
349
|
for (const task of tasks) {
|
|
@@ -372,6 +382,7 @@ export function buildStatus(targetInput, options = {}) {
|
|
|
372
382
|
details: { failures, warnings },
|
|
373
383
|
legacy,
|
|
374
384
|
},
|
|
385
|
+
git: gitState.summary,
|
|
375
386
|
summary: {
|
|
376
387
|
tasks: tasks.length,
|
|
377
388
|
briefCoverage: {
|
|
@@ -405,106 +416,3 @@ export function buildStatus(targetInput, options = {}) {
|
|
|
405
416
|
recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
|
|
406
417
|
};
|
|
407
418
|
}
|
|
408
|
-
|
|
409
|
-
export function renderDashboard(status) {
|
|
410
|
-
const taskCards = status.tasks
|
|
411
|
-
.map((task) => {
|
|
412
|
-
const phases = task.phases
|
|
413
|
-
.map(
|
|
414
|
-
(phase) => `<div class="phase ${escapeHtml(phase.state)}">
|
|
415
|
-
<div class="phase-top"><strong>${escapeHtml(phase.id)}</strong><span>${phase.completion}%</span></div>
|
|
416
|
-
<div class="phase-output">${escapeHtml(phase.output)}</div>
|
|
417
|
-
<div class="meter"><i style="width:${phase.completion}%"></i></div>
|
|
418
|
-
<div class="muted">${escapeHtml(phase.state)} · evidence ${escapeHtml(phase.evidenceStatus)}</div>
|
|
419
|
-
</div>`,
|
|
420
|
-
)
|
|
421
|
-
.join("");
|
|
422
|
-
const risks = task.risks
|
|
423
|
-
.map((risk) => `<span class="risk ${risk.open || risk.blocksRelease ? "open" : ""}">${escapeHtml(risk.severity)} ${escapeHtml(risk.summary)}</span>`)
|
|
424
|
-
.join("");
|
|
425
|
-
const evidence = task.evidence
|
|
426
|
-
.map((item) => `<span class="evidence">${escapeHtml(item.type)} · ${escapeHtml(item.summary)}</span>`)
|
|
427
|
-
.join("");
|
|
428
|
-
const evidenceMeter = evidenceCompletion(task.phases);
|
|
429
|
-
return `<section class="task">
|
|
430
|
-
<div class="task-head">
|
|
431
|
-
<div><h2>${escapeHtml(task.title)}</h2><p>${escapeHtml(task.path)}</p></div>
|
|
432
|
-
<div class="score">${task.completion}%</div>
|
|
433
|
-
</div>
|
|
434
|
-
<div class="meter"><i style="width:${task.completion}%"></i></div>
|
|
435
|
-
<div class="phases">${phases || '<div class="empty">No phase table</div>'}</div>
|
|
436
|
-
<div class="evidence-row"><strong>Evidence</strong><div class="meter small"><i style="width:${evidenceMeter}%"></i></div>${evidence || '<span class="empty">No evidence</span>'}</div>
|
|
437
|
-
<div class="risks">${risks || '<span class="ok">No open visual risk</span>'}</div>
|
|
438
|
-
</section>`;
|
|
439
|
-
})
|
|
440
|
-
.join("");
|
|
441
|
-
const chips = status.capabilities
|
|
442
|
-
.map((capability) => `<span class="chip ${escapeHtml(capability.state)}">${escapeHtml(capability.name)} · ${escapeHtml(capability.state)}</span>`)
|
|
443
|
-
.join("");
|
|
444
|
-
const failures = status.checkState.details.failures.map((failure) => `<li>${escapeHtml(failure)}</li>`).join("");
|
|
445
|
-
const warnings = status.checkState.details.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("");
|
|
446
|
-
const handoffs = status.handoffs
|
|
447
|
-
.map((handoff) => `<span class="handoff">${escapeHtml(handoff.state)} · ${escapeHtml(handoff.summary)}</span>`)
|
|
448
|
-
.join("");
|
|
449
|
-
const activity = status.recentActivity
|
|
450
|
-
.map((item) => `<li><strong>${escapeHtml(item.type)}</strong> ${escapeHtml(item.summary)}</li>`)
|
|
451
|
-
.join("");
|
|
452
|
-
return `<!doctype html>
|
|
453
|
-
<html lang="zh-CN">
|
|
454
|
-
<head>
|
|
455
|
-
<meta charset="utf-8">
|
|
456
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
457
|
-
<title>${escapeHtml(status.project.name)} Harness Dashboard</title>
|
|
458
|
-
<style>
|
|
459
|
-
:root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17202a;background:#f6f7f9}
|
|
460
|
-
body{margin:0}.shell{max-width:1180px;margin:0 auto;padding:28px}
|
|
461
|
-
header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}
|
|
462
|
-
h1,h2{margin:0;letter-spacing:0}h1{font-size:30px}h2{font-size:18px}p{margin:6px 0;color:#687382}
|
|
463
|
-
.pill,.chip,.risk,.ok{display:inline-flex;align-items:center;border-radius:999px;padding:6px 10px;font-size:12px;margin:4px;background:#e8edf3;color:#273444}
|
|
464
|
-
.pass,.verified{background:#dff5e8;color:#125c32}.warn,.configured{background:#fff0cc;color:#765100}.fail,.open{background:#ffe1df;color:#8a1c12}.scaffolded{background:#e8edf3;color:#273444}
|
|
465
|
-
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}.stat,.task{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px}
|
|
466
|
-
.stat strong{font-size:24px;display:block}.capabilities{margin-bottom:20px}.task{margin-bottom:16px}.task-head{display:flex;justify-content:space-between;gap:16px}
|
|
467
|
-
.score{font-size:28px;font-weight:700;color:#223047}.meter{height:8px;background:#edf1f5;border-radius:99px;overflow:hidden;margin:10px 0}.meter i{display:block;height:100%;background:#2f6fed}.meter.small{height:6px;max-width:180px}
|
|
468
|
-
.evidence,.handoff{display:inline-flex;padding:5px 8px;margin:4px;border-radius:6px;background:#edf7ff;color:#214d72;font-size:12px}.handoff{background:#fff3d8;color:#745000}
|
|
469
|
-
.phases{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin-top:12px}.phase{border:1px solid #e5eaf0;border-radius:8px;padding:12px;background:#fbfcfe}.phase-top{display:flex;justify-content:space-between}.phase-output{min-height:38px;margin-top:8px}
|
|
470
|
-
.risks{margin-top:12px}.empty{color:#8a95a3}.panel{background:#fff;border:1px solid #e4e8ee;border-radius:8px;padding:16px;margin-top:16px}
|
|
471
|
-
@media(max-width:760px){.shell{padding:16px}header{display:block}.grid{grid-template-columns:1fr 1fr}.task-head{display:block}}
|
|
472
|
-
</style>
|
|
473
|
-
</head>
|
|
474
|
-
<body><main class="shell">
|
|
475
|
-
<header>
|
|
476
|
-
<div><h1>${escapeHtml(status.project.name)} Harness Dashboard</h1><p>${escapeHtml(status.project.root)} · ${escapeHtml(status.generatedAt)}</p></div>
|
|
477
|
-
<span class="pill ${escapeHtml(status.checkState.status)}">${escapeHtml(status.checkState.status)} · ${escapeHtml(status.mode)}</span>
|
|
478
|
-
</header>
|
|
479
|
-
<section class="grid">
|
|
480
|
-
<div class="stat"><strong>${status.tasks.length}</strong><span>Tasks</span></div>
|
|
481
|
-
<div class="stat"><strong>${status.capabilities.length}</strong><span>Capabilities</span></div>
|
|
482
|
-
<div class="stat"><strong>${status.checkState.failures}</strong><span>Failures</span></div>
|
|
483
|
-
<div class="stat"><strong>${status.checkState.warnings}</strong><span>Warnings</span></div>
|
|
484
|
-
</section>
|
|
485
|
-
<section class="capabilities">${chips}</section>
|
|
486
|
-
<section class="panel"><h2>Handoffs</h2>${handoffs || '<span class="ok">No pending handoff</span>'}</section>
|
|
487
|
-
${taskCards || '<section class="task">No tasks found.</section>'}
|
|
488
|
-
<section class="panel"><h2>Recent Activity</h2><ul>${activity || "<li>None</li>"}</ul></section>
|
|
489
|
-
<section class="panel"><h2>Failures</h2><ul>${failures || "<li>None</li>"}</ul><h2>Warnings</h2><ul>${warnings || "<li>None</li>"}</ul></section>
|
|
490
|
-
</main></body></html>`;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function escapeHtml(value) {
|
|
494
|
-
return String(value ?? "")
|
|
495
|
-
.replaceAll("&", "&")
|
|
496
|
-
.replaceAll("<", "<")
|
|
497
|
-
.replaceAll(">", ">")
|
|
498
|
-
.replaceAll('"', """);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function evidenceCompletion(phases) {
|
|
502
|
-
const scored = phases.filter((phase) => phase.state !== "skipped");
|
|
503
|
-
if (scored.length === 0) return 0;
|
|
504
|
-
const score = scored.reduce((sum, phase) => {
|
|
505
|
-
if (["present", "waived"].includes(phase.evidenceStatus)) return sum + 100;
|
|
506
|
-
if (phase.evidenceStatus === "partial") return sum + 50;
|
|
507
|
-
return sum;
|
|
508
|
-
}, 0);
|
|
509
|
-
return Math.round(score / scored.length);
|
|
510
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
lessonCandidatesFile,
|
|
5
|
+
readFileSafe,
|
|
6
|
+
toPosix,
|
|
7
|
+
visualMapFile,
|
|
8
|
+
} from "./core-shared.mjs";
|
|
9
|
+
import {
|
|
10
|
+
listTaskPlanPaths,
|
|
11
|
+
parseTaskBudget,
|
|
12
|
+
parseTaskContractInfo,
|
|
13
|
+
} from "./task-scanner.mjs";
|
|
14
|
+
|
|
15
|
+
export function validatePlanContracts(target, { strict = true } = {}) {
|
|
16
|
+
const failures = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
const report = (message) => {
|
|
19
|
+
if (strict) failures.push(message);
|
|
20
|
+
else warnings.push(`adoption-needed: ${message}`);
|
|
21
|
+
};
|
|
22
|
+
for (const taskPlanPath of listTaskPlanPaths(target)) {
|
|
23
|
+
const taskDir = path.dirname(taskPlanPath);
|
|
24
|
+
const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
|
|
25
|
+
const taskPlanContent = readFileSafe(taskPlanPath);
|
|
26
|
+
const budget = parseTaskBudget(taskPlanContent);
|
|
27
|
+
const taskContract = parseTaskContractInfo(taskPlanContent);
|
|
28
|
+
if (!taskContract.generated) {
|
|
29
|
+
warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
|
|
30
|
+
}
|
|
31
|
+
for (const fileName of requiredTaskFilesForBudget(budget)) {
|
|
32
|
+
if (!fs.existsSync(path.join(taskDir, fileName))) {
|
|
33
|
+
if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);
|
|
34
|
+
else report(`${relativeDir} missing ${fileName}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { failures, warnings };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requiredTaskFilesForBudget(budget) {
|
|
42
|
+
const simpleFiles = ["brief.md", "task_plan.md", visualMapFile, "progress.md"];
|
|
43
|
+
if (budget === "simple") return simpleFiles;
|
|
44
|
+
const standardFiles = [...simpleFiles, "execution_strategy.md", "findings.md", lessonCandidatesFile, "review.md"];
|
|
45
|
+
if (budget === "complex") return [...standardFiles, "references/INDEX.md", "artifacts/INDEX.md"];
|
|
46
|
+
return standardFiles;
|
|
47
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
@@ -10,6 +11,11 @@ export const legacyVisualRoadmapFile = "visual_roadmap.md";
|
|
|
10
11
|
export const lessonCandidatesFile = "lesson_candidates.md";
|
|
11
12
|
export const longRunningTaskContractFile = "long-running-task-contract.md";
|
|
12
13
|
export const taskContractMarker = "Task Contract: harness-task/v1";
|
|
14
|
+
export const builtinPresetRoot = path.join(repoRoot, "presets");
|
|
15
|
+
export function userPresetRootForHome(home = "") {
|
|
16
|
+
return path.join(path.resolve(home || os.homedir()), ".coding-agent-harness/presets");
|
|
17
|
+
}
|
|
18
|
+
export const userPresetRoot = userPresetRootForHome();
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
export const supportedLocales = new Set(["zh-CN", "en-US"]);
|
|
@@ -40,6 +46,10 @@ export function normalizeTarget(input = ".") {
|
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export function projectPresetRoot(targetInput = ".") {
|
|
50
|
+
return path.join(normalizeTarget(targetInput).projectRoot, ".coding-agent-harness/presets");
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
export function toPosix(value) {
|
|
44
54
|
return value.split(path.sep).join("/");
|
|
45
55
|
}
|