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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
exists,
|
|
11
11
|
existsInDocs,
|
|
12
12
|
readFileSafe,
|
|
13
|
+
readJsonSafe,
|
|
13
14
|
readBundledTemplate,
|
|
14
15
|
walkFiles,
|
|
15
16
|
normalizeLocale,
|
|
@@ -96,8 +97,9 @@ export function readCapabilityRegistry(target) {
|
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
let readError = null;
|
|
101
|
+
const raw = readJsonSafe(registryPath, null, { onError: (error) => { readError = error; } });
|
|
102
|
+
if (raw) {
|
|
101
103
|
const locale = normalizeLocale(raw.locale);
|
|
102
104
|
const capabilities = Array.isArray(raw.capabilities)
|
|
103
105
|
? raw.capabilities.map((entry) =>
|
|
@@ -107,9 +109,8 @@ export function readCapabilityRegistry(target) {
|
|
|
107
109
|
)
|
|
108
110
|
: [];
|
|
109
111
|
return { mode: "declared-capability", path: registryPath, capabilities, raw, locale, errors: [] };
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return { mode: "declared-capability", path: registryPath, capabilities: [], raw: null, errors: [error.message] };
|
|
112
112
|
}
|
|
113
|
+
return { mode: "declared-capability", path: registryPath, capabilities: [], raw: null, errors: [readError?.message || "invalid .harness-capabilities.json"] };
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export function normalizeCapabilityName(name) {
|
|
@@ -157,7 +158,7 @@ function validateDashboardAssetAssembly(root, manifestName, assetName, driftMess
|
|
|
157
158
|
const assetPath = path.join(assetsDir, assetName);
|
|
158
159
|
if (!fs.existsSync(manifestPath) || !fs.existsSync(assetPath)) return [];
|
|
159
160
|
try {
|
|
160
|
-
const manifest =
|
|
161
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
161
162
|
if (!Array.isArray(manifest) || manifest.length === 0) {
|
|
162
163
|
return [`dashboard asset manifest must list source files: ${manifestName}`];
|
|
163
164
|
}
|
|
@@ -221,7 +222,7 @@ export function buildInstallReport({ target, locale, capabilities, changes, dryR
|
|
|
221
222
|
|
|
222
223
|
function packageVersion() {
|
|
223
224
|
try {
|
|
224
|
-
const pkg =
|
|
225
|
+
const pkg = readJsonSafe(path.join(repoRoot, "package.json"), {});
|
|
225
226
|
return pkg.version || "";
|
|
226
227
|
} catch {
|
|
227
228
|
return "";
|
|
@@ -261,19 +262,14 @@ function skillPackageEntries() {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
function listPackageFiles() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
if (stat.isFile()) files.push(toPosix(relativePath));
|
|
274
|
-
}
|
|
275
|
-
for (const entry of skillPackageEntries()) walk(entry);
|
|
276
|
-
return files.sort();
|
|
265
|
+
return skillPackageEntries()
|
|
266
|
+
.flatMap((entry) => {
|
|
267
|
+
const fullPath = path.join(repoRoot, entry);
|
|
268
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
269
|
+
if (fs.statSync(fullPath).isFile()) return [toPosix(path.relative(repoRoot, fullPath))];
|
|
270
|
+
return walkFiles(fullPath).map((file) => toPosix(path.relative(repoRoot, file)));
|
|
271
|
+
})
|
|
272
|
+
.sort();
|
|
277
273
|
}
|
|
278
274
|
|
|
279
275
|
function copySkillPackage(targetRoot, { dryRun = false, force = false } = {}) {
|
|
@@ -324,7 +320,7 @@ export function installUserSkill({ agent = "codex", home = "", dryRun = false, f
|
|
|
324
320
|
|
|
325
321
|
function readInstalledVersion(targetRoot) {
|
|
326
322
|
try {
|
|
327
|
-
const pkg =
|
|
323
|
+
const pkg = readJsonSafe(path.join(targetRoot, "package.json"), {});
|
|
328
324
|
return pkg.version || "";
|
|
329
325
|
} catch {
|
|
330
326
|
return "";
|
|
@@ -536,7 +532,7 @@ function initNextCommands() {
|
|
|
536
532
|
function writeNpmScripts(target, { dryRun = true } = {}) {
|
|
537
533
|
const packagePath = path.join(target.projectRoot, "package.json");
|
|
538
534
|
if (!fs.existsSync(packagePath)) throw new Error("init --add-npm-scripts requires an existing package.json");
|
|
539
|
-
const pkg =
|
|
535
|
+
const pkg = readJsonSafe(packagePath, {});
|
|
540
536
|
const scripts = { ...(pkg.scripts || {}) };
|
|
541
537
|
const additions = {
|
|
542
538
|
"harness:dev": "coding-agent-harness dev .",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { walkFiles } from "./core-shared.mjs";
|
|
3
4
|
|
|
4
5
|
function stripMarkdownCode(value) {
|
|
5
6
|
return String(value || "").replace(/`/g, "").trim();
|
|
@@ -17,22 +18,14 @@ function modulePromptBlock(content, key) {
|
|
|
17
18
|
function listModuleTaskPlans({ targetRoot, rel, filePath }) {
|
|
18
19
|
const modulesRoot = filePath("docs/09-PLANNING/MODULES");
|
|
19
20
|
if (!fs.existsSync(modulesRoot)) return [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
21
|
+
return walkFiles(modulesRoot, {
|
|
22
|
+
dirFilter: (_dirName, fullPath) => {
|
|
23
|
+
const relativePath = rel(path.relative(targetRoot, fullPath));
|
|
24
|
+
return !relativePath.includes("/_archive/") && !relativePath.endsWith("/_task-template");
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
.map((file) => rel(path.relative(targetRoot, file)))
|
|
28
|
+
.filter((relativePath) => /\/TASKS\/[^/]+\/task_plan\.md$/.test(relativePath));
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
function parseModuleTaskPath(taskPlanPath) {
|
|
@@ -22,26 +22,19 @@ import {
|
|
|
22
22
|
firstColumn,
|
|
23
23
|
contentHasAny,
|
|
24
24
|
} from "./markdown-utils.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { validateCapabilities } from "./capability-registry.mjs";
|
|
26
26
|
import { readPresetPackage } from "./preset-registry.mjs";
|
|
27
27
|
import { validateTaskPresetAuditSnapshot } from "./preset-audit-contracts.mjs";
|
|
28
28
|
import { validatePresetResourcesForTask } from "./preset-resource-contracts.mjs";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
readVisualMapContractFile,
|
|
33
|
-
parsePhases,
|
|
34
|
-
taskCutoverCounters,
|
|
35
|
-
} from "./task-scanner.mjs";
|
|
36
|
-
import {
|
|
37
|
-
normalizeReviewBoolean,
|
|
38
|
-
reviewFindingColumns,
|
|
39
|
-
} from "./task-review-model.mjs";
|
|
29
|
+
import { collectTasks, listTaskPlanPaths, parseTaskBudget, readVisualMapContractFile, parsePhases } from "./task-scanner.mjs";
|
|
30
|
+
import { normalizeReviewBoolean, reviewFindingColumns } from "./task-review-model.mjs";
|
|
31
|
+
import { allowedPhaseActors, allowedPhaseKinds } from "./phase-kind.mjs";
|
|
40
32
|
import { validateTaskCompletionConsistency } from "./task-completion-consistency.mjs";
|
|
41
33
|
import { validatePlanContracts } from "./check-task-contracts.mjs";
|
|
42
34
|
import { validateGovernanceTableBoundaries } from "./governance-table-boundary.mjs";
|
|
43
35
|
import { validateSubagentAuthorization } from "./subagent-authorization-audit.mjs";
|
|
44
36
|
import { summarizeGitState } from "./git-status-summary.mjs";
|
|
37
|
+
import { buildStatusData } from "./status-builder.mjs";
|
|
45
38
|
export { renderDashboard } from "./status-dashboard-renderer.mjs";
|
|
46
39
|
|
|
47
40
|
export function runLegacyCheck(target) {
|
|
@@ -144,10 +137,10 @@ export function validateReviewSchema(target, { strict = true } = {}) {
|
|
|
144
137
|
return { failures, warnings };
|
|
145
138
|
}
|
|
146
139
|
|
|
147
|
-
export function validateVisualMaps(target) {
|
|
140
|
+
export function validateVisualMaps(target, { taskPlanPaths } = {}) {
|
|
148
141
|
const failures = [];
|
|
149
142
|
const warnings = [];
|
|
150
|
-
for (const taskPlanPath of listTaskPlanPaths(target)) {
|
|
143
|
+
for (const taskPlanPath of taskPlanPaths || listTaskPlanPaths(target)) {
|
|
151
144
|
const taskDir = path.dirname(taskPlanPath);
|
|
152
145
|
const visualMapPath = path.join(taskDir, visualMapFile);
|
|
153
146
|
const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
|
|
@@ -161,7 +154,10 @@ export function validateVisualMaps(target) {
|
|
|
161
154
|
}
|
|
162
155
|
}
|
|
163
156
|
const phases = parsePhases(visualMap.content);
|
|
157
|
+
const budget = parseTaskBudget(taskPlan);
|
|
164
158
|
for (const phase of phases) {
|
|
159
|
+
if (!allowedPhaseKinds.has(phase.kind)) failures.push(`${relative} phase ${phase.id} invalid kind: ${phase.kind}`);
|
|
160
|
+
if (!allowedPhaseActors.has(phase.actor)) failures.push(`${relative} phase ${phase.id} invalid actor: ${phase.actor}`);
|
|
165
161
|
if (!allowedPhaseStates.has(phase.state)) failures.push(`${relative} phase ${phase.id} invalid state: ${phase.state}`);
|
|
166
162
|
if (!allowedEvidenceStatus.has(phase.evidenceStatus)) {
|
|
167
163
|
failures.push(`${relative} phase ${phase.id} invalid evidence status: ${phase.evidenceStatus}`);
|
|
@@ -176,6 +172,9 @@ export function validateVisualMaps(target) {
|
|
|
176
172
|
failures.push(`${relative} missing Visual Map Contract: v1.0`);
|
|
177
173
|
}
|
|
178
174
|
if (visualMap.source === "canonical" && phases.length === 0) warnings.push(`${relative} has no Visual Map phase table`);
|
|
175
|
+
if (visualMap.source === "canonical" && budget !== "simple" && phases.length > 0 && !phases.some((phase) => phase.kind === "execution" && phase.state !== "skipped")) {
|
|
176
|
+
failures.push(`${relative} requires at least one non-skipped execution phase`);
|
|
177
|
+
}
|
|
179
178
|
if (visualMap.source === "legacy" && fs.existsSync(legacyPath)) {
|
|
180
179
|
warnings.push(`${relative} missing; legacy visual_roadmap.md is rewrite input only`);
|
|
181
180
|
} else if (visualMap.source === "legacy" && phases.length > 0) {
|
|
@@ -185,7 +184,7 @@ export function validateVisualMaps(target) {
|
|
|
185
184
|
return { failures, warnings };
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
export function validateTaskPresetContracts(target) {
|
|
187
|
+
export function validateTaskPresetContracts(target, { tasks } = {}) {
|
|
189
188
|
const failures = [];
|
|
190
189
|
const allowedMigrationLevels = new Set([
|
|
191
190
|
"migration-baseline",
|
|
@@ -193,7 +192,7 @@ export function validateTaskPresetContracts(target) {
|
|
|
193
192
|
"migration-full-cutover",
|
|
194
193
|
"migration-deferred",
|
|
195
194
|
]);
|
|
196
|
-
for (const task of collectTasks(target)) {
|
|
195
|
+
for (const task of tasks || collectTasks(target)) {
|
|
197
196
|
if (!task.taskPreset || task.taskPreset === "none") continue;
|
|
198
197
|
let presetPackage = null;
|
|
199
198
|
try {
|
|
@@ -326,10 +325,13 @@ export function buildStatus(targetInput, options = {}) {
|
|
|
326
325
|
const shouldRunLegacy = !options.skipLegacyCheck && (capabilityState.registry.mode === "legacy-compat" || safeAdoptionMode);
|
|
327
326
|
const legacy = shouldRunLegacy ? runLegacyCheck(target) : { status: "skipped", code: 0, stdout: "", stderr: "" };
|
|
328
327
|
const contractStrict = Boolean(options.strict) || (capabilityState.registry.mode !== "legacy-compat" && !safeAdoptionMode);
|
|
328
|
+
const taskPlanPaths = listTaskPlanPaths(target);
|
|
329
|
+
const closeoutContent = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
|
|
330
|
+
const tasks = collectTasks(target, { requireGeneratedScaffoldProvenance: contractStrict, taskPlanPaths, closeoutContent });
|
|
329
331
|
const reviews = validateReviewSchema(target, { strict: contractStrict });
|
|
330
|
-
const visualMaps = validateVisualMaps(target);
|
|
331
|
-
const planContracts = validatePlanContracts(target, { strict: contractStrict });
|
|
332
|
-
const presetContracts = validateTaskPresetContracts(target);
|
|
332
|
+
const visualMaps = validateVisualMaps(target, { taskPlanPaths });
|
|
333
|
+
const planContracts = validatePlanContracts(target, { strict: contractStrict, taskPlanPaths });
|
|
334
|
+
const presetContracts = validateTaskPresetContracts(target, { tasks });
|
|
333
335
|
const contextDocs = validateContextDocs(target, { strict: contractStrict });
|
|
334
336
|
const governanceBoundaries = validateGovernanceTableBoundaries(target);
|
|
335
337
|
const subagentAuthorization = validateSubagentAuthorization(target, { strict: contractStrict });
|
|
@@ -340,79 +342,31 @@ export function buildStatus(targetInput, options = {}) {
|
|
|
340
342
|
else warnings.push(`adoption-needed: legacy check failed: ${(legacy.stderr || legacy.stdout).trim()}`);
|
|
341
343
|
}
|
|
342
344
|
|
|
343
|
-
const tasks = collectTasks(target);
|
|
344
345
|
const taskCompletionConsistency = validateTaskCompletionConsistency(tasks);
|
|
345
346
|
failures.push(...taskCompletionConsistency.failures);
|
|
346
347
|
warnings.push(...taskCompletionConsistency.warnings);
|
|
347
348
|
const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
|
|
348
349
|
const briefMissing = tasks.length - briefReady;
|
|
349
350
|
for (const task of tasks) {
|
|
351
|
+
for (const issue of task.materialIssues || []) {
|
|
352
|
+
if (!String(issue.code || "").startsWith("missing-task-audit") && !String(issue.code || "").startsWith("legacy-")) continue;
|
|
353
|
+
const message = `${String(issue.sourcePath || task.path).replace(/^TARGET:/, "")} ${issue.message}`;
|
|
354
|
+
if (contractStrict || options.strictLegacy) failures.push(message);
|
|
355
|
+
else warnings.push(`adoption-needed: ${message}`);
|
|
356
|
+
}
|
|
350
357
|
if (task.stateSource === "invalid") {
|
|
351
358
|
const message = `${task.path}/progress.md invalid task state: ${task.stateRaw}`;
|
|
352
359
|
if (contractStrict || options.strictLegacy) failures.push(message);
|
|
353
360
|
else warnings.push(`adoption-needed: ${message}`);
|
|
354
361
|
}
|
|
355
362
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
failures.length === 0 &&
|
|
363
|
-
warnings.length === 0 &&
|
|
364
|
-
cutoverCounters.legacyVisualOnlyCount === 0 &&
|
|
365
|
-
cutoverCounters.unknownClassificationCount === 0 &&
|
|
366
|
-
cutoverCounters.weakBriefCount === 0 &&
|
|
367
|
-
cutoverCounters.missingCanonicalVisualMapCount === 0;
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
project: {
|
|
371
|
-
name: path.basename(target.projectRoot),
|
|
372
|
-
root: `TARGET:${target.docsOnly ? toPosix(path.relative(target.projectRoot, target.docsRoot)) : "."}`,
|
|
373
|
-
docsOnly: target.docsOnly,
|
|
374
|
-
},
|
|
375
|
-
schemaVersion: 2,
|
|
376
|
-
generatedAt: new Date().toISOString(),
|
|
377
|
-
mode: capabilityState.registry.mode,
|
|
378
|
-
checkState: {
|
|
379
|
-
status: failures.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "pass",
|
|
380
|
-
failures: failures.length,
|
|
381
|
-
warnings: warnings.length,
|
|
382
|
-
details: { failures, warnings },
|
|
383
|
-
legacy,
|
|
384
|
-
},
|
|
385
|
-
git: gitState.summary,
|
|
386
|
-
summary: {
|
|
387
|
-
tasks: tasks.length,
|
|
388
|
-
briefCoverage: {
|
|
389
|
-
ready: briefReady,
|
|
390
|
-
missing: briefMissing,
|
|
391
|
-
total: tasks.length,
|
|
392
|
-
},
|
|
393
|
-
visualMapCoverage: {
|
|
394
|
-
canonical: tasks.filter((task) => task.visualMapSource === "canonical").length,
|
|
395
|
-
legacyOnly: cutoverCounters.legacyVisualOnlyCount,
|
|
396
|
-
missing: tasks.filter((task) => task.visualMapStatus === "missing").length,
|
|
397
|
-
total: tasks.length,
|
|
398
|
-
},
|
|
399
|
-
fullCutoverEligible,
|
|
400
|
-
legacyVisualOnlyCount: cutoverCounters.legacyVisualOnlyCount,
|
|
401
|
-
unknownClassificationCount: cutoverCounters.unknownClassificationCount,
|
|
402
|
-
weakBriefCount: cutoverCounters.weakBriefCount,
|
|
403
|
-
visualMapRequiredCount: cutoverCounters.visualMapRequiredCount,
|
|
404
|
-
missingCanonicalVisualMapCount: cutoverCounters.missingCanonicalVisualMapCount,
|
|
405
|
-
},
|
|
406
|
-
capabilities: [...capabilityNames.values()].map((capability) => ({
|
|
407
|
-
name: capability.name,
|
|
408
|
-
state: capability.state || "configured",
|
|
409
|
-
dependencyStatus: capabilityDefinitions[capability.name]?.dependencies.every((dependency) => capabilityNames.has(dependency))
|
|
410
|
-
? "valid"
|
|
411
|
-
: "invalid",
|
|
412
|
-
warnings: capabilityState.warnings.filter((warning) => warning.includes(capability.name)),
|
|
413
|
-
})),
|
|
363
|
+
return buildStatusData(target, {
|
|
364
|
+
capabilityState,
|
|
365
|
+
gitState,
|
|
366
|
+
legacy,
|
|
367
|
+
failures,
|
|
368
|
+
warnings,
|
|
414
369
|
tasks,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
};
|
|
370
|
+
validationMode: "validated",
|
|
371
|
+
});
|
|
418
372
|
}
|
|
@@ -11,24 +11,32 @@ import {
|
|
|
11
11
|
parseTaskBudget,
|
|
12
12
|
parseTaskContractInfo,
|
|
13
13
|
} from "./task-scanner.mjs";
|
|
14
|
+
import { parseTaskAuditMetadata } from "./task-audit-metadata.mjs";
|
|
14
15
|
|
|
15
|
-
export function validatePlanContracts(target, { strict = true } = {}) {
|
|
16
|
+
export function validatePlanContracts(target, { strict = true, taskPlanPaths } = {}) {
|
|
16
17
|
const failures = [];
|
|
17
18
|
const warnings = [];
|
|
18
19
|
const report = (message) => {
|
|
19
20
|
if (strict) failures.push(message);
|
|
20
21
|
else warnings.push(`adoption-needed: ${message}`);
|
|
21
22
|
};
|
|
22
|
-
for (const taskPlanPath of listTaskPlanPaths(target)) {
|
|
23
|
+
for (const taskPlanPath of taskPlanPaths || listTaskPlanPaths(target)) {
|
|
23
24
|
const taskDir = path.dirname(taskPlanPath);
|
|
24
25
|
const relativeDir = toPosix(path.relative(target.projectRoot, taskDir));
|
|
25
26
|
const taskPlanContent = readFileSafe(taskPlanPath);
|
|
27
|
+
const indexContent = readFileSafe(path.join(taskDir, "INDEX.md"));
|
|
26
28
|
const budget = parseTaskBudget(taskPlanContent);
|
|
27
29
|
const taskContract = parseTaskContractInfo(taskPlanContent);
|
|
30
|
+
const taskAudit = parseTaskAuditMetadata(indexContent, { required: strict && taskContract.generated });
|
|
28
31
|
if (!taskContract.generated) {
|
|
29
32
|
warnings.push(`adoption-needed: ${relativeDir} missing Task Contract: harness-task/v1 marker`);
|
|
30
33
|
}
|
|
31
|
-
for (const
|
|
34
|
+
for (const issue of taskAudit.issues) {
|
|
35
|
+
if (taskContract.generated || taskAudit.present) failures.push(`${relativeDir}/INDEX.md ${issue.message}`);
|
|
36
|
+
else report(`${relativeDir}/INDEX.md ${issue.message}`);
|
|
37
|
+
}
|
|
38
|
+
const indexRequired = /^Task Package Index\s*[::]\s*(required|yes|true|必需|必须|required)\s*$/im.test(taskPlanContent);
|
|
39
|
+
for (const fileName of requiredTaskFilesForBudget(budget, { indexRequired })) {
|
|
32
40
|
if (!fs.existsSync(path.join(taskDir, fileName))) {
|
|
33
41
|
if (taskContract.generated) failures.push(`${relativeDir} missing ${fileName}`);
|
|
34
42
|
else report(`${relativeDir} missing ${fileName}`);
|
|
@@ -38,8 +46,8 @@ export function validatePlanContracts(target, { strict = true } = {}) {
|
|
|
38
46
|
return { failures, warnings };
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
function requiredTaskFilesForBudget(budget) {
|
|
42
|
-
const simpleFiles = ["brief.md", "task_plan.md", visualMapFile, "progress.md"];
|
|
49
|
+
function requiredTaskFilesForBudget(budget, { indexRequired = false } = {}) {
|
|
50
|
+
const simpleFiles = [...(indexRequired ? ["INDEX.md"] : []), "brief.md", "task_plan.md", visualMapFile, "progress.md"];
|
|
43
51
|
if (budget === "simple") return simpleFiles;
|
|
44
52
|
const standardFiles = [...simpleFiles, "execution_strategy.md", "findings.md", lessonCandidatesFile, "review.md"];
|
|
45
53
|
if (budget === "complex") return [...standardFiles, "references/INDEX.md", "artifacts/INDEX.md"];
|
|
@@ -70,6 +70,15 @@ export function readFileSafe(filePath) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export function readJsonSafe(filePath, fallback = null, { onError } = {}) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (typeof onError === "function") onError(error);
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
export function readBundledTemplate(source) {
|
|
74
83
|
const sourcePath = path.join(repoRoot, source);
|
|
75
84
|
if (!fs.existsSync(sourcePath)) throw new Error(`Bundled template missing: ${source}`);
|
|
@@ -78,15 +87,17 @@ export function readBundledTemplate(source) {
|
|
|
78
87
|
return content;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
|
-
export function walkFiles(root) {
|
|
90
|
+
export function walkFiles(root, options = {}) {
|
|
82
91
|
const results = [];
|
|
83
92
|
if (!fs.existsSync(root)) return results;
|
|
93
|
+
const dirFilter = typeof options.dirFilter === "function" ? options.dirFilter : () => true;
|
|
84
94
|
function walk(dir) {
|
|
85
95
|
for (const entry of fs.readdirSync(dir)) {
|
|
86
96
|
const full = path.join(dir, entry);
|
|
87
97
|
const stat = fs.statSync(full);
|
|
88
98
|
if (stat.isDirectory()) {
|
|
89
99
|
if ([".git", "node_modules", "tmp"].includes(entry)) continue;
|
|
100
|
+
if (!dirFilter(entry, full)) continue;
|
|
90
101
|
walk(full);
|
|
91
102
|
} else {
|
|
92
103
|
results.push(full);
|
|
@@ -180,14 +191,56 @@ export function normalizeTaskId(value) {
|
|
|
180
191
|
return slug(value || "task");
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
export function renderTaskTemplate(content, { taskId, title, locale, budget = "standard" }) {
|
|
194
|
+
export function renderTaskTemplate(content, { taskId, title, locale, budget = "standard", moduleKey = "", preset = "none", presetVersion = "", evidenceBundle = "", longRunning = false, scaffoldProvenance = {}, taskAudit = {} }) {
|
|
184
195
|
const date = todayDate();
|
|
196
|
+
const provenance = {
|
|
197
|
+
createdBy: scaffoldProvenance.createdBy || "harness new-task",
|
|
198
|
+
command: scaffoldProvenance.command || "harness new-task [task-id] <target>",
|
|
199
|
+
createdAt: scaffoldProvenance.createdAt || date,
|
|
200
|
+
budget: scaffoldProvenance.budget || budget,
|
|
201
|
+
templateSource: scaffoldProvenance.templateSource || "templates/planning/brief.md",
|
|
202
|
+
exceptionReason: scaffoldProvenance.exceptionReason || "n/a",
|
|
203
|
+
};
|
|
185
204
|
return String(content)
|
|
186
205
|
.replaceAll("{{TASK_ID}}", taskId)
|
|
187
206
|
.replaceAll("{{TASK_TITLE}}", title)
|
|
188
207
|
.replaceAll("{{DATE}}", date)
|
|
189
208
|
.replaceAll("{{LOCALE}}", normalizeLocale(locale))
|
|
190
209
|
.replaceAll("{{TASK_BUDGET}}", budget)
|
|
210
|
+
.replaceAll("{{TASK_MODULE}}", moduleKey || "n/a")
|
|
211
|
+
.replaceAll("{{TASK_PRESET}}", preset || "none")
|
|
212
|
+
.replaceAll("{{TASK_PRESET_VERSION}}", presetVersion || "n/a")
|
|
213
|
+
.replaceAll("{{TASK_EVIDENCE_BUNDLE}}", evidenceBundle || "n/a")
|
|
214
|
+
.replaceAll("{{TASK_LONG_RUNNING}}", longRunning ? "yes" : "no")
|
|
215
|
+
.replaceAll("{{SCAFFOLD_CREATED_BY}}", provenance.createdBy)
|
|
216
|
+
.replaceAll("{{SCAFFOLD_COMMAND}}", provenance.command)
|
|
217
|
+
.replaceAll("{{SCAFFOLD_CREATED_AT}}", provenance.createdAt)
|
|
218
|
+
.replaceAll("{{SCAFFOLD_BUDGET}}", provenance.budget)
|
|
219
|
+
.replaceAll("{{SCAFFOLD_TEMPLATE_SOURCE}}", provenance.templateSource)
|
|
220
|
+
.replaceAll("{{SCAFFOLD_EXCEPTION_REASON}}", provenance.exceptionReason)
|
|
221
|
+
.replaceAll("{{TASK_AUDIT_CREATED_BY}}", taskAudit["Created By"] || provenance.createdBy)
|
|
222
|
+
.replaceAll("{{TASK_AUDIT_CREATED_AT}}", taskAudit["Created At"] || provenance.createdAt)
|
|
223
|
+
.replaceAll("{{TASK_AUDIT_COMMAND_SHAPE}}", taskAudit["Command Shape"] || provenance.command)
|
|
224
|
+
.replaceAll("{{TASK_AUDIT_BUDGET}}", taskAudit.Budget || provenance.budget)
|
|
225
|
+
.replaceAll("{{TASK_AUDIT_TEMPLATE_SOURCE}}", taskAudit["Template Source"] || provenance.templateSource)
|
|
226
|
+
.replaceAll("{{TASK_AUDIT_TASK_CREATOR}}", taskAudit["Task Creator"] || "n/a")
|
|
227
|
+
.replaceAll("{{TASK_AUDIT_TASK_CREATOR_SOURCE}}", taskAudit["Task Creator Source"] || "git-unavailable")
|
|
228
|
+
.replaceAll("{{TASK_AUDIT_HUMAN_REVIEW_STATUS}}", taskAudit["Human Review Status"] || "not-confirmed")
|
|
229
|
+
.replaceAll("{{TASK_AUDIT_CONFIRMATION_ID}}", taskAudit["Confirmation ID"] || "n/a")
|
|
230
|
+
.replaceAll("{{TASK_AUDIT_CONFIRMED_AT}}", taskAudit["Confirmed At"] || "n/a")
|
|
231
|
+
.replaceAll("{{TASK_AUDIT_REVIEWER}}", taskAudit.Reviewer || "n/a")
|
|
232
|
+
.replaceAll("{{TASK_AUDIT_REVIEWER_EMAIL}}", taskAudit["Reviewer Email"] || "n/a")
|
|
233
|
+
.replaceAll("{{TASK_AUDIT_CONFIRM_TEXT}}", taskAudit["Confirm Text"] || "n/a")
|
|
234
|
+
.replaceAll("{{TASK_AUDIT_EVIDENCE_CHECKED}}", taskAudit["Evidence Checked"] || "n/a")
|
|
235
|
+
.replaceAll("{{TASK_AUDIT_REVIEW_COMMIT_SHA}}", taskAudit["Review Commit SHA"] || "n/a")
|
|
236
|
+
.replaceAll("{{TASK_AUDIT_AUDIT_SOURCE}}", taskAudit["Audit Source"] || "native-index")
|
|
237
|
+
.replaceAll("{{TASK_AUDIT_AUDIT_STATUS}}", taskAudit["Audit Status"] || "created")
|
|
238
|
+
.replaceAll("{{TASK_AUDIT_EXCEPTION_REASON}}", taskAudit["Exception Reason"] || provenance.exceptionReason)
|
|
239
|
+
.replaceAll("{{TASK_AUDIT_MESSAGE}}", taskAudit.Message || "n/a")
|
|
240
|
+
.replaceAll("{{TASK_AUDIT_MIGRATION_STATUS}}", taskAudit["Migration Status"] || "native")
|
|
241
|
+
.replaceAll("{{TASK_AUDIT_MIGRATED_FROM}}", taskAudit["Migrated From"] || "n/a")
|
|
242
|
+
.replaceAll("{{TASK_AUDIT_LEGACY_EXTRA_FIELDS}}", taskAudit["Legacy Extra Fields"] || "{}")
|
|
243
|
+
.replaceAll("{{TASK_AUDIT_MIGRATION_NOTES}}", taskAudit["Migration Notes"] || "n/a")
|
|
191
244
|
.replaceAll("[simple / standard / complex]", budget)
|
|
192
245
|
.replaceAll("[simple / standard / long-running / module-parallel]", budget)
|
|
193
246
|
.replaceAll("[simple / complex]", budget)
|