coding-agent-harness 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +661 -21
  3. package/LICENSE-EXCEPTION.md +37 -0
  4. package/README.md +33 -1
  5. package/README.zh-CN.md +23 -1
  6. package/SKILL.md +9 -8
  7. package/docs-release/architecture/overview.md +1 -1
  8. package/docs-release/architecture/overview.zh-CN.md +1 -1
  9. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  10. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  11. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  12. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  13. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  14. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  15. package/docs-release/architecture/system-explainer/README.md +67 -0
  16. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  17. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  18. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  19. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  20. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  21. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  22. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  23. package/docs-release/guides/agent-installation.en-US.md +8 -7
  24. package/docs-release/guides/agent-installation.md +9 -7
  25. package/docs-release/guides/preset-development.md +26 -2
  26. package/docs-release/guides/task-state-machine.en-US.md +30 -13
  27. package/docs-release/guides/task-state-machine.md +30 -13
  28. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  29. package/package.json +3 -2
  30. package/references/harness-ledger.md +1 -1
  31. package/scripts/commands/migration-command.mjs +30 -0
  32. package/scripts/commands/task-command.mjs +26 -25
  33. package/scripts/harness.mjs +7 -3
  34. package/scripts/lib/capability-registry.mjs +17 -21
  35. package/scripts/lib/check-module-parallel.mjs +9 -16
  36. package/scripts/lib/check-profiles.mjs +35 -81
  37. package/scripts/lib/check-task-contracts.mjs +13 -5
  38. package/scripts/lib/core-shared.mjs +55 -2
  39. package/scripts/lib/dashboard-data.mjs +126 -18
  40. package/scripts/lib/dashboard-workbench.mjs +80 -1
  41. package/scripts/lib/dashboard-writer.mjs +6 -2
  42. package/scripts/lib/git-status-summary.mjs +1 -1
  43. package/scripts/lib/governance-sync.mjs +180 -83
  44. package/scripts/lib/harness-core.mjs +1 -0
  45. package/scripts/lib/markdown-utils.mjs +33 -0
  46. package/scripts/lib/migration-planner.mjs +4 -6
  47. package/scripts/lib/phase-kind.mjs +50 -0
  48. package/scripts/lib/preset-engine.mjs +5 -8
  49. package/scripts/lib/preset-registry.mjs +188 -39
  50. package/scripts/lib/review-confirm-git-gate.mjs +1 -1
  51. package/scripts/lib/status-builder.mjs +88 -0
  52. package/scripts/lib/status-dashboard-renderer.mjs +7 -4
  53. package/scripts/lib/task-audit-metadata.mjs +385 -0
  54. package/scripts/lib/task-audit-migration.mjs +350 -0
  55. package/scripts/lib/task-completion-consistency.mjs +11 -1
  56. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  57. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  58. package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
  59. package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
  60. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  61. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  62. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  63. package/scripts/lib/task-lifecycle.mjs +114 -147
  64. package/scripts/lib/task-metadata.mjs +118 -0
  65. package/scripts/lib/task-review-model.mjs +54 -68
  66. package/scripts/lib/task-scanner.mjs +70 -143
  67. package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
  68. package/templates/AGENTS.md.template +7 -5
  69. package/templates/dashboard/assets/app-src/00-state.js +12 -0
  70. package/templates/dashboard/assets/app-src/10-router.js +3 -0
  71. package/templates/dashboard/assets/app-src/20-overview.js +7 -3
  72. package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
  73. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  74. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  75. package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
  76. package/templates/dashboard/assets/app.css +583 -0
  77. package/templates/dashboard/assets/app.css.manifest.json +1 -0
  78. package/templates/dashboard/assets/app.js +578 -10
  79. package/templates/dashboard/assets/app.manifest.json +1 -0
  80. package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
  81. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
  82. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  83. package/templates/dashboard/assets/i18n.js +140 -2
  84. package/templates/planning/INDEX.md +87 -0
  85. package/templates/planning/brief.md +1 -1
  86. package/templates/planning/module_session_prompt.md +1 -0
  87. package/templates/planning/review.md +0 -18
  88. package/templates/planning/task_plan.md +4 -43
  89. package/templates/planning/visual_map.md +13 -9
  90. package/templates/planning/visual_map.simple.md +52 -0
  91. package/templates/reference/execution-workflow-standard.md +29 -2
  92. package/templates-zh-CN/AGENTS.md.template +7 -5
  93. package/templates-zh-CN/planning/INDEX.md +87 -0
  94. package/templates-zh-CN/planning/brief.md +1 -1
  95. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  96. package/templates-zh-CN/planning/review.md +0 -18
  97. package/templates-zh-CN/planning/task_plan.md +3 -63
  98. package/templates-zh-CN/planning/visual_map.md +14 -7
  99. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  100. package/templates-zh-CN/reference/execution-workflow-standard.md +31 -6
@@ -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
- try {
100
- const raw = JSON.parse(fs.readFileSync(registryPath, "utf8"));
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 = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
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 = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"));
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
- const files = [];
265
- function walk(relativePath) {
266
- const full = path.join(repoRoot, relativePath);
267
- if (!fs.existsSync(full)) return;
268
- const stat = fs.statSync(full);
269
- if (stat.isDirectory()) {
270
- for (const entry of fs.readdirSync(full)) walk(path.join(relativePath, entry));
271
- return;
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 = JSON.parse(fs.readFileSync(path.join(targetRoot, "package.json"), "utf8"));
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 = JSON.parse(fs.readFileSync(packagePath, "utf8"));
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
- 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;
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 { capabilityDefinitions, validateCapabilities } from "./capability-registry.mjs";
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
- collectTasks,
31
- listTaskPlanPaths,
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
- const capabilityNames = new Map(capabilityState.registry.capabilities.map((capability) => [capability.name, capability]));
357
- for (const detected of capabilityState.detected) {
358
- if (!capabilityNames.has(detected)) capabilityNames.set(detected, { name: detected, state: "configured" });
359
- }
360
- const cutoverCounters = taskCutoverCounters(tasks);
361
- const fullCutoverEligible =
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
- handoffs: tasks.flatMap((task) => task.handoffs || []),
416
- recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
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 fileName of requiredTaskFilesForBudget(budget)) {
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)