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.
Files changed (177) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.md +211 -86
  4. package/README.zh-CN.md +54 -34
  5. package/SKILL.md +25 -18
  6. package/docs-release/README.md +9 -5
  7. package/docs-release/architecture/overview.md +17 -5
  8. package/docs-release/architecture/overview.zh-CN.md +9 -5
  9. package/docs-release/assets/dashboard-overview.png +0 -0
  10. package/docs-release/guides/agent-installation.en-US.md +31 -8
  11. package/docs-release/guides/agent-installation.md +34 -9
  12. package/docs-release/guides/contributing.md +100 -0
  13. package/docs-release/guides/contributing.zh-CN.md +99 -0
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  15. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  20. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  21. package/docs-release/guides/migration-playbook.md +14 -15
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  23. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  24. package/docs-release/guides/preset-development.md +214 -0
  25. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  26. package/docs-release/guides/repository-operating-models.md +5 -4
  27. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  28. package/docs-release/guides/task-state-machine.md +214 -0
  29. package/docs-release/intl/en-US.md +1 -1
  30. package/docs-release/intl/zh-CN.md +1 -1
  31. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  32. package/package.json +8 -3
  33. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  34. package/presets/legacy-migration/preset.yaml +134 -0
  35. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  36. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  37. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  38. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  39. package/presets/legacy-migration/templates/review.seed.md +12 -0
  40. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  41. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  42. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  43. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  44. package/presets/lesson-sedimentation/preset.yaml +23 -0
  45. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  46. package/presets/module/preset.yaml +25 -0
  47. package/presets/module/templates/execution_strategy.append.md +8 -0
  48. package/presets/module/templates/task_plan.append.md +17 -0
  49. package/presets/standard-task/preset.yaml +31 -0
  50. package/presets/standard-task/templates/task_plan.append.md +7 -0
  51. package/references/adversarial-review-standard.md +2 -2
  52. package/references/agents-md-pattern.md +2 -2
  53. package/references/delivery-operating-model-standard.md +3 -3
  54. package/references/docs-directory-standard.md +6 -7
  55. package/references/harness-ledger.md +53 -96
  56. package/references/lessons-governance.md +88 -93
  57. package/references/module-parallel-standard.md +14 -14
  58. package/references/planning-loop.md +12 -6
  59. package/references/pull-request-standard.md +118 -0
  60. package/references/repo-governance-standard.md +11 -2
  61. package/references/review-routing-standard.md +7 -1
  62. package/references/ssot-governance.md +67 -59
  63. package/references/taskr-gap-analysis.md +600 -0
  64. package/references/walkthrough-closeout.md +7 -7
  65. package/scripts/check-harness.mjs +40 -301
  66. package/scripts/commands/dashboard-command.mjs +67 -0
  67. package/scripts/commands/migration-command.mjs +96 -0
  68. package/scripts/commands/preset-command.mjs +73 -0
  69. package/scripts/commands/task-command.mjs +327 -0
  70. package/scripts/harness.mjs +55 -260
  71. package/scripts/lib/capability-registry.mjs +66 -8
  72. package/scripts/lib/check-module-parallel.mjs +237 -0
  73. package/scripts/lib/check-profiles.mjs +61 -153
  74. package/scripts/lib/check-task-contracts.mjs +47 -0
  75. package/scripts/lib/core-shared.mjs +10 -0
  76. package/scripts/lib/dashboard-data.mjs +29 -6
  77. package/scripts/lib/dashboard-workbench.mjs +52 -12
  78. package/scripts/lib/dashboard-writer.mjs +14 -2
  79. package/scripts/lib/git-status-summary.mjs +46 -0
  80. package/scripts/lib/governance-index-generator.mjs +174 -0
  81. package/scripts/lib/governance-sync.mjs +514 -0
  82. package/scripts/lib/governance-table-boundary.mjs +175 -0
  83. package/scripts/lib/harness-core.mjs +5 -0
  84. package/scripts/lib/lesson-maintenance.mjs +36 -29
  85. package/scripts/lib/migration-support.mjs +1 -1
  86. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  87. package/scripts/lib/preset-engine.mjs +497 -0
  88. package/scripts/lib/preset-registry.mjs +627 -0
  89. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  90. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  91. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  92. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  93. package/scripts/lib/task-completion-consistency.mjs +16 -0
  94. package/scripts/lib/task-index.mjs +93 -0
  95. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  96. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  97. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  98. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  99. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  100. package/scripts/lib/task-lifecycle.mjs +297 -403
  101. package/scripts/lib/task-review-model.mjs +469 -0
  102. package/scripts/lib/task-scanner.mjs +130 -236
  103. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  104. package/scripts/postinstall.mjs +14 -0
  105. package/skills/preset-creator/SKILL.md +179 -0
  106. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  107. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  108. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  109. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  110. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  111. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  112. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  113. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  114. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  115. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  116. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  117. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  118. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  119. package/templates/AGENTS.md.template +19 -15
  120. package/templates/dashboard/assets/app-src/00-state.js +1 -0
  121. package/templates/dashboard/assets/app-src/10-router.js +2 -1
  122. package/templates/dashboard/assets/app-src/20-overview.js +11 -5
  123. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  124. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  125. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  126. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  127. package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
  128. package/templates/dashboard/assets/app.css +698 -156
  129. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  130. package/templates/dashboard/assets/app.js +662 -91
  131. package/templates/dashboard/assets/app.manifest.json +1 -0
  132. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  133. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  134. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  135. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  136. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  137. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  138. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  139. package/templates/dashboard/assets/i18n.js +123 -21
  140. package/templates/ledger/Harness-Ledger.md +13 -25
  141. package/templates/lessons/lesson-arch-process-change.md +1 -1
  142. package/templates/lessons/lesson-new-doc.md +1 -1
  143. package/templates/lessons/lesson-ref-change.md +1 -1
  144. package/templates/planning/execution_strategy.md +31 -0
  145. package/templates/planning/lesson_candidates.md +18 -6
  146. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  147. package/templates/planning/optional/references/INDEX.md +3 -3
  148. package/templates/planning/review.md +59 -0
  149. package/templates/planning/task_plan.md +36 -13
  150. package/templates/reference/execution-workflow-standard.md +4 -3
  151. package/templates/reference/pull-request-standard.md +80 -0
  152. package/templates/reference/repo-governance-standard.md +7 -6
  153. package/templates/reference/review-routing-standard.md +6 -0
  154. package/templates/reference/walkthrough-standard.md +2 -1
  155. package/templates/verifier/verifier-output.md +1 -1
  156. package/templates-zh-CN/AGENTS.md.template +20 -16
  157. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  158. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  159. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  160. package/templates-zh-CN/planning/review.md +59 -1
  161. package/templates-zh-CN/planning/task_plan.md +30 -10
  162. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  163. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  164. package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
  165. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  166. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  167. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  168. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  169. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  170. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  171. package/docs-release/assets/dashboard-overview-en.png +0 -0
  172. package/scripts/smoke-dashboard.mjs +0 -92
  173. package/scripts/test-harness.mjs +0 -1395
  174. package/templates/ssot/Feature-SSoT.md +0 -43
  175. package/templates/ssot/Lessons-SSoT.md +0 -44
  176. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  177. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -6,5 +6,10 @@ export * from "./check-profiles.mjs";
6
6
  export * from "./dashboard-data.mjs";
7
7
  export * from "./dashboard-workbench.mjs";
8
8
  export * from "./migration-planner.mjs";
9
+ export * from "./preset-registry.mjs";
10
+ export * from "./governance-index-generator.mjs";
9
11
  export * from "./task-lifecycle.mjs";
12
+ export * from "./task-lesson-sedimentation.mjs";
10
13
  export * from "./lesson-maintenance.mjs";
14
+ export * from "./task-index.mjs";
15
+ export * from "./task-tombstone-commands.mjs";
@@ -12,8 +12,13 @@ import {
12
12
  collectTasks,
13
13
  parseLessonCandidateStatus,
14
14
  } from "./task-scanner.mjs";
15
+ import {
16
+ beginGovernanceSync,
17
+ commitGovernanceSync,
18
+ releaseGovernanceSync,
19
+ } from "./governance-sync.mjs";
15
20
 
16
- export function promoteLessonCandidate(targetInput, taskId, candidateId, { dryRun = false } = {}) {
21
+ export function promoteLessonCandidate(targetInput, taskId, candidateId, { dryRun = false, apply = false } = {}) {
17
22
  const target = normalizeTarget(targetInput);
18
23
  const normalizedRef = slug(taskId);
19
24
  const matchesBareSlug = (item) => {
@@ -45,23 +50,43 @@ export function promoteLessonCandidate(targetInput, taskId, candidateId, { dryRu
45
50
  const title = row.title || lessonId;
46
51
  const detailRelative = `docs/01-GOVERNANCE/lessons/${lessonId}-${slug(title)}.md`;
47
52
  const detailPath = path.join(target.projectRoot, detailRelative);
48
- const ssotPath = path.join(target.docsRoot, "01-GOVERNANCE/Lessons-SSoT.md");
49
- const ssotContent = readFileSafe(ssotPath);
50
- if (!ssotContent.trim()) throw new Error("Lessons SSoT not found");
51
53
 
52
54
  const changes = [];
53
55
  if (!fs.existsSync(detailPath)) changes.push({ action: dryRun ? "would-create" : "create", path: `TARGET:${detailRelative}` });
54
- if (!ssotContent.includes(lessonId)) changes.push({ action: dryRun ? "would-append" : "append", path: "TARGET:docs/01-GOVERNANCE/Lessons-SSoT.md" });
55
56
  if (row.status !== "promoted" || parsed.status !== "promoted") changes.push({ action: dryRun ? "would-update" : "update", path: task.lessonCandidatePath || `TARGET:${toPosix(path.relative(target.projectRoot, candidatePath))}` });
56
57
 
57
- if (dryRun) return { dryRun: true, taskId: task.id, candidateId: row.id, lessonId, detailDoc: `TARGET:${detailRelative}`, changes };
58
+ const effectiveDryRun = dryRun || !apply;
59
+ if (effectiveDryRun) {
60
+ return {
61
+ dryRun: true,
62
+ applyRequired: true,
63
+ taskId: task.id,
64
+ candidateId: row.id,
65
+ lessonId,
66
+ detailDoc: `TARGET:${detailRelative}`,
67
+ changes: changes.map((change) => ({ ...change, action: change.action.replace(/^(create|append|update)$/, "would-$1") })),
68
+ nextCommand: `harness lesson-promote ${task.shortId} ${row.id} --apply`,
69
+ };
70
+ }
58
71
 
59
- fs.mkdirSync(path.dirname(detailPath), { recursive: true });
60
- if (!fs.existsSync(detailPath)) fs.writeFileSync(detailPath, renderLessonDetail({ lessonId, candidate: row, task, detailRelative }));
61
- if (!ssotContent.includes(lessonId)) fs.writeFileSync(ssotPath, appendLessonSsotRow(ssotContent, { lessonId, candidate: row, task, detailRelative }));
62
- fs.writeFileSync(candidatePath, markCandidatePromoted(candidateContent, row.id, lessonId));
72
+ const governanceContext = beginGovernanceSync(target, { operation: `lesson-promote ${task.id} ${row.id}` });
73
+ try {
74
+ fs.mkdirSync(path.dirname(detailPath), { recursive: true });
75
+ if (!fs.existsSync(detailPath)) fs.writeFileSync(detailPath, renderLessonDetail({ lessonId, candidate: row, task, detailRelative }));
76
+ fs.writeFileSync(candidatePath, markCandidatePromoted(candidateContent, row.id, lessonId));
77
+ const commit = commitGovernanceSync(
78
+ governanceContext,
79
+ [
80
+ detailRelative,
81
+ toPosix(path.relative(target.projectRoot, candidatePath)),
82
+ ],
83
+ { message: `chore(harness): promote lesson ${row.id}` },
84
+ );
63
85
 
64
- return { dryRun: false, taskId: task.id, candidateId: row.id, lessonId, detailDoc: `TARGET:${detailRelative}`, changes };
86
+ return { dryRun: false, taskId: task.id, candidateId: row.id, lessonId, detailDoc: `TARGET:${detailRelative}`, changes, governance: { commit } };
87
+ } finally {
88
+ releaseGovernanceSync(governanceContext);
89
+ }
65
90
  }
66
91
 
67
92
  function lessonIdFromCandidate(candidateId) {
@@ -95,24 +120,6 @@ function renderLessonDetail({ lessonId, candidate, task }) {
95
120
  ].join("\n");
96
121
  }
97
122
 
98
- function appendLessonSsotRow(content, { lessonId, candidate, task, detailRelative }) {
99
- const lines = String(content || "").split(/\r?\n/);
100
- const headerIndex = lines.findIndex((line) => /^\|\s*(ID|Lesson ID)\s*\|/.test(line));
101
- if (headerIndex < 0) throw new Error("Lessons SSoT active table not found");
102
- const columnCount = splitSimpleRow(lines[headerIndex]).length;
103
- const date = lessonId.match(/^L-(\d{4}-\d{2}-\d{2})-/)?.[1] || new Date().toISOString().slice(0, 10);
104
- const detail = `\`${detailRelative}\``;
105
- const source = `\`${task.path.replace(/^TARGET:/, "docs/").replace(/^docs\/docs\//, "docs/")}/task_plan.md\``;
106
- const row =
107
- columnCount === 10
108
- ? `| ${lessonId} | ${escapeCell(candidate.title || lessonId)} | ${source} | process-change | coordinator | candidate | ${escapeCell(candidate.promotionTarget || "governance review")} | ${detail} | ${escapeCell(candidate.id)} | ${date} |`
109
- : `| ${lessonId} | ${date} | ${source} | process-change | ${escapeCell(candidate.promotionTarget || "governance review")} | ${escapeCell(candidate.title || lessonId)} | ${detail} | pending | ${escapeCell(candidate.id)} |`;
110
- let insertAt = headerIndex + 1;
111
- while (insertAt < lines.length && lines[insertAt].trim().startsWith("|")) insertAt += 1;
112
- lines.splice(insertAt, 0, row);
113
- return `${lines.join("\n").trimEnd()}\n`;
114
- }
115
-
116
123
  function markCandidatePromoted(content, candidateId, lessonId) {
117
124
  const lines = String(content || "").split(/\r?\n/);
118
125
  const headerIndex = lines.findIndex((line) => /^\|\s*ID\s*\|/.test(line));
@@ -22,8 +22,8 @@ export function migrationSampleFiles(target) {
22
22
  path.join(target.projectRoot, "AGENTS.md"),
23
23
  path.join(target.projectRoot, "CLAUDE.md"),
24
24
  path.join(target.docsRoot, "Harness-Ledger.md"),
25
- path.join(target.docsRoot, "09-PLANNING/Feature-SSoT.md"),
26
25
  path.join(target.docsRoot, "05-TEST-QA/Regression-SSoT.md"),
26
+ path.join(target.docsRoot, "09-PLANNING/Delivery-SSoT.md"),
27
27
  ];
28
28
  const taskPlans = listTaskPlanPaths(target).slice(0, 20);
29
29
  return [...candidates, ...taskPlans].filter((file) => fs.existsSync(file));
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readFileSafe, toPosix } from "./core-shared.mjs";
4
+
5
+ export function validateTaskPresetAuditSnapshot(target, task, presetPackage) {
6
+ const failures = [];
7
+ if (!presetPackage?.audit?.manifestRequired) return failures;
8
+ const bundle = String(task.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
9
+ if (!bundle) {
10
+ failures.push(`${task.path} ${task.taskPreset} preset missing Evidence Bundle for manifest audit`);
11
+ return failures;
12
+ }
13
+ const auditPath = path.join(target.projectRoot, bundle, "preset-audit.json");
14
+ if (!fs.existsSync(auditPath)) {
15
+ failures.push(`${task.path} ${task.taskPreset} preset audit missing: TARGET:${toPosix(path.relative(target.projectRoot, auditPath))}`);
16
+ return failures;
17
+ }
18
+ let audit = null;
19
+ try {
20
+ audit = JSON.parse(readFileSafe(auditPath));
21
+ } catch (error) {
22
+ failures.push(`${task.path} ${task.taskPreset} preset audit invalid JSON: ${error.message}`);
23
+ return failures;
24
+ }
25
+ if (audit.preset !== task.taskPreset) {
26
+ failures.push(`${task.path} ${task.taskPreset} preset audit id mismatch: ${audit.preset || "(missing)"}`);
27
+ }
28
+ if (String(audit.version || "") !== String(task.presetVersion || "")) {
29
+ failures.push(`${task.path} ${task.taskPreset} preset audit version mismatch: ${audit.version || "(missing)"}`);
30
+ }
31
+ if (!audit.manifestSha256) {
32
+ failures.push(`${task.path} ${task.taskPreset} preset audit missing manifestSha256`);
33
+ } else if (audit.manifestSha256 !== presetPackage.manifestSha256) {
34
+ failures.push(`${task.path} ${task.taskPreset} preset manifest hash mismatch: task audit ${audit.manifestSha256}, current ${presetPackage.manifestSha256}`);
35
+ }
36
+ return failures;
37
+ }
@@ -0,0 +1,497 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { repoRoot, taskContractMarker, toPosix, visualMapFile } from "./core-shared.mjs";
6
+ import { verifyMigrationSession } from "./migration-planner.mjs";
7
+ import { buildPresetAudit, renderPresetTemplate } from "./preset-registry.mjs";
8
+
9
+ export function resolvePresetInputs(preset, { cliArgs = [], fromSession = "", targetInput = "" } = {}) {
10
+ const inputs = {};
11
+ let targetFromInput = "";
12
+ for (const [name, declaration] of Object.entries(preset.inputs || {})) {
13
+ const rawValue = inputValue(declaration, { cliArgs, fromSession });
14
+ if ((rawValue == null || rawValue === "") && declaration.required) {
15
+ throw new Error(`Missing required preset input ${declaration.flag || name}`);
16
+ }
17
+ if (declaration.type === "flag") {
18
+ inputs[name] = rawValue === true;
19
+ continue;
20
+ }
21
+ if (declaration.type === "json-file") {
22
+ if (rawValue == null || rawValue === "") {
23
+ inputs[name] = null;
24
+ continue;
25
+ }
26
+ const filePath = path.resolve(String(rawValue));
27
+ if (!fs.existsSync(filePath)) throw new Error(`Preset input file not found for ${declaration.flag || name}: ${rawValue}`);
28
+ let value;
29
+ try {
30
+ value = JSON.parse(fs.readFileSync(filePath, "utf8"));
31
+ } catch (error) {
32
+ throw new Error(`Invalid preset JSON input ${declaration.flag || name}: ${error.message}`);
33
+ }
34
+ if (declaration.validateOperation && value.operation !== declaration.validateOperation) {
35
+ throw new Error(`${preset.id} preset requires ${declaration.flag || name} operation ${declaration.validateOperation}`);
36
+ }
37
+ if (declaration.rejectPlanOnly && value.planOnly) throw new Error(`${preset.id} preset cannot use plan-only session evidence`);
38
+ if (declaration.requireTarget && (!value.target || !fs.existsSync(value.target))) throw new Error(`Preset input target missing: ${value.target || "(none)"}`);
39
+ if (declaration.targetFromSession) targetFromInput = value.target || targetFromInput;
40
+ inputs[name] = { ...value, sourcePath: filePath };
41
+ continue;
42
+ }
43
+ inputs[name] = rawValue == null || rawValue === "" ? declaration.default || "" : String(rawValue);
44
+ }
45
+ return {
46
+ inputs,
47
+ targetInput: targetFromInput || targetInput,
48
+ };
49
+ }
50
+
51
+ export function evaluateTemplateValues(preset, resolvedInputs, { taskId = "", taskTitle = "", moduleKey = "" } = {}) {
52
+ const computed = computedValues(preset, resolvedInputs);
53
+ const base = {
54
+ inputs: resolvedInputs,
55
+ computed,
56
+ preset: {
57
+ id: preset.id,
58
+ version: String(preset.version),
59
+ source: preset.source,
60
+ },
61
+ task: {
62
+ id: taskId,
63
+ title: taskTitle,
64
+ moduleKey,
65
+ kind: preset.task?.kind || "general",
66
+ },
67
+ };
68
+ const values = {
69
+ preset: preset.id,
70
+ presetVersion: String(preset.version),
71
+ kind: preset.task?.kind || "general",
72
+ ...computed,
73
+ };
74
+ for (const [name, declaration] of Object.entries(preset.templateValues || {})) {
75
+ if (Object.prototype.hasOwnProperty.call(declaration, "from")) {
76
+ values[name] = getPath(base, declaration.from);
77
+ } else if (Object.prototype.hasOwnProperty.call(declaration, "value")) {
78
+ values[name] = declaration.value;
79
+ } else if (Object.prototype.hasOwnProperty.call(declaration, "default")) {
80
+ values[name] = declaration.default;
81
+ }
82
+ }
83
+ return values;
84
+ }
85
+
86
+ export function buildPresetContext(preset, { target, taskDir, taskId, taskTitle, resolvedInputs, evaluatedValues }) {
87
+ const taskRelativeDir = toPosix(path.relative(target.projectRoot, taskDir));
88
+ const evidenceBundle = presetEvidenceBundle(preset, { target, taskDir, evaluatedValues });
89
+ const audit = buildPresetAudit(preset, {
90
+ taskId,
91
+ targetRoot: target.projectRoot,
92
+ entrypoint: "newTask",
93
+ });
94
+ const context = {
95
+ kind: evaluatedValues.kind || preset.task?.kind || "general",
96
+ preset: preset.id,
97
+ presetVersion: String(preset.version),
98
+ presetPackage: preset,
99
+ audit,
100
+ resolvedInputs,
101
+ taskId,
102
+ taskTitle,
103
+ taskRelativeDir,
104
+ values: {
105
+ ...evaluatedValues,
106
+ evidenceBundle,
107
+ },
108
+ migrationTargetLevel: evaluatedValues.migrationTargetLevel || "",
109
+ migrationAchievedLevel: evaluatedValues.migrationAchievedLevel || "",
110
+ evidenceBundle,
111
+ };
112
+ context.evidenceFiles = generateEvidenceFiles(preset, { target, taskDir, context });
113
+ const resources = generateResourceFiles(preset, { context });
114
+ context.resourceFiles = resources.files;
115
+ context.resourceIndexRows = resources.indexRows;
116
+ return context;
117
+ }
118
+
119
+ export function renderPresetTaskTemplate(destination, content, presetContext) {
120
+ if (!presetContext) return content;
121
+ let next = String(content);
122
+ if (destination === "task_plan.md" || destination === "task_plan") {
123
+ next = renderPresetMetadata(next, presetContext);
124
+ }
125
+ const templateKey = {
126
+ task_plan: "taskPlanAppend",
127
+ "task_plan.md": "taskPlanAppend",
128
+ execution_strategy: "executionStrategyAppend",
129
+ "execution_strategy.md": "executionStrategyAppend",
130
+ findings: "findingsSeed",
131
+ "findings.md": "findingsSeed",
132
+ review: "reviewSeed",
133
+ "review.md": "reviewSeed",
134
+ [visualMapFile]: "visualMapAppend",
135
+ }[destination];
136
+ const templatePath = presetContext.presetPackage?.newTaskTemplates?.[templateKey];
137
+ if (templatePath) {
138
+ next = `${next.trimEnd()}\n\n${renderPresetTemplate(presetContext.presetPackage, templatePath, presetContext.values).trimEnd()}\n`;
139
+ }
140
+ if (destination === "task_plan.md" || destination === "task_plan") {
141
+ next = appendPresetRequiredReads(next, presetContext);
142
+ }
143
+ return next;
144
+ }
145
+
146
+ export function renderPresetResourceIndex(content, kind, rows) {
147
+ if (!rows.length) return content;
148
+ const renderedRows = rows.map((row) => kind === "references"
149
+ ? `| ${markdownTableCell(row.id)} | ${markdownTableCell(row.type || "preset")} | ${markdownTableCell(row.path)} | ${markdownTableCell(row.summary)} | ${markdownTableCell(row.usedBy || "coordinator")} |`
150
+ : `| ${markdownTableCell(row.id)} | ${markdownTableCell(row.type || "preset")} | ${markdownTableCell(row.path)} | ${markdownTableCell(row.summary)} | ${markdownTableCell(row.producedBy || "preset")} |`);
151
+ const base = String(content || "").trim() ? String(content || "") : presetIndexSkeleton(kind);
152
+ const lines = base.trimEnd().split(/\r?\n/);
153
+ const separatorIndex = lines.findIndex((line) => /^\|\s*---/.test(line));
154
+ if (separatorIndex >= 0) {
155
+ lines.splice(separatorIndex + 1, 0, ...renderedRows);
156
+ return `${lines.join("\n")}\n`;
157
+ }
158
+ return `${String(content || "").trimEnd()}\n${renderedRows.join("\n")}\n`;
159
+ }
160
+
161
+ export function assertPresetWriteScope(preset, relativePath) {
162
+ const normalized = toPosix(path.normalize(relativePath));
163
+ if (normalized.startsWith("../") || path.isAbsolute(normalized)) {
164
+ throw new Error(`Preset write scope violation for ${relativePath}`);
165
+ }
166
+ if (!preset.writeScopes.some((scope) => matchesScope(scope.path, normalized))) {
167
+ throw new Error(`Preset write scope violation for ${normalized}`);
168
+ }
169
+ }
170
+
171
+ function inputValue(declaration, { cliArgs, fromSession }) {
172
+ if (declaration.flag === "--from-session" && fromSession) return fromSession;
173
+ if (!declaration.flag) return declaration.default;
174
+ const index = cliArgs.indexOf(declaration.flag);
175
+ if (index < 0) return declaration.default;
176
+ if (declaration.type === "flag") return true;
177
+ const value = cliArgs[index + 1];
178
+ if (!value || value.startsWith("--")) return "";
179
+ return value;
180
+ }
181
+
182
+ function computedValues(preset, inputs) {
183
+ const values = {};
184
+ const migrationSession = Object.values(inputs).find((value) => value && typeof value === "object" && value.operation === "migrate-run");
185
+ if (migrationSession) {
186
+ values.migrationTargetLevel = preset.task?.migrationTargetLevel || "migration-baseline";
187
+ values.migrationAchievedLevel = migrationSession.strictDeferred ? "migration-deferred" : migrationSession.result === "complete" ? "migration-full-cutover" : "migration-baseline";
188
+ values.strictDeferred = migrationSession.strictDeferred ? "yes" : "no";
189
+ values.fullCutoverClaimAllowed = values.migrationAchievedLevel === "migration-full-cutover" ? "yes" : "no";
190
+ values.warnings = migrationSession.plan?.summary?.warnings || 0;
191
+ values.taskActions = migrationSession.plan?.summary?.taskActions || 0;
192
+ values.legacyResiduals = migrationSession.plan?.summary?.legacyResiduals || 0;
193
+ values.generatedAt = migrationSession.generatedAt || "";
194
+ }
195
+ return values;
196
+ }
197
+
198
+ function presetEvidenceBundle(preset, { target, taskDir, evaluatedValues }) {
199
+ const bundleDir = String(preset.evidence?.bundleDir || "artifacts/preset").trim();
200
+ const stampSource = evaluatedValues.generatedAt || new Date().toISOString();
201
+ const stamp = String(stampSource).replace(/[^0-9A-Za-z-]+/g, "-").replace(/-+$/g, "");
202
+ const relativeTaskDir = toPosix(path.relative(target.projectRoot, taskDir));
203
+ return toPosix(path.join(relativeTaskDir, bundleDir, stamp || "generated"));
204
+ }
205
+
206
+ function generateEvidenceFiles(preset, { target, context }) {
207
+ const files = [];
208
+ const add = (relativePath, source, content) => {
209
+ assertPresetWriteScope(preset, relativePath);
210
+ files.push({ relativePath, source, content });
211
+ };
212
+ const evidenceFiles = preset.evidence?.files || {};
213
+ for (const [name, declaration] of Object.entries(evidenceFiles)) {
214
+ addEvidenceFile({ name, declaration, preset, target, context, add });
215
+ }
216
+ for (const name of preset.audit.evidenceFiles || []) {
217
+ if (files.some((file) => path.basename(file.relativePath) === name)) continue;
218
+ addAuditFile({ name, preset, context, add });
219
+ }
220
+ return files;
221
+ }
222
+
223
+ function generateResourceFiles(preset, { context }) {
224
+ const files = [];
225
+ const indexRows = { references: [], artifacts: [] };
226
+ const add = (relativePath, source, content) => {
227
+ assertPresetWriteScope(preset, relativePath);
228
+ files.push({ relativePath, source, content });
229
+ };
230
+ for (const resource of Object.values(preset.resources?.references || {})) {
231
+ const relativePath = toPosix(path.join(context.taskRelativeDir, resource.path));
232
+ add(relativePath, resource.source || resource.template, renderResourceContent(preset, resource, context));
233
+ indexRows.references.push(renderReferenceIndexRow(resource, relativePath, context.values));
234
+ }
235
+ for (const resource of Object.values(preset.resources?.artifacts || {})) {
236
+ const relativePath = toPosix(path.join(context.taskRelativeDir, resource.path));
237
+ add(relativePath, resource.source || resource.template, renderResourceContent(preset, resource, context));
238
+ indexRows.artifacts.push(renderArtifactIndexRow(resource, relativePath, context.values));
239
+ }
240
+ return { files, indexRows };
241
+ }
242
+
243
+ function renderResourceContent(preset, resource, context) {
244
+ if (resource.template) return renderPresetTemplate(preset, resource.template, context.values);
245
+ return fs.readFileSync(path.join(preset.directory, resource.source), "utf8");
246
+ }
247
+
248
+ function renderReferenceIndexRow(resource, relativePath, values) {
249
+ return {
250
+ id: resource.index.id,
251
+ type: renderInline(resource.index.type, values),
252
+ path: `TARGET:${relativePath}`,
253
+ summary: renderInline(resource.index.summary, values),
254
+ usedBy: renderInline(resource.index.usedBy, values),
255
+ };
256
+ }
257
+
258
+ function renderArtifactIndexRow(resource, relativePath, values) {
259
+ return {
260
+ id: resource.index.id,
261
+ type: renderInline(resource.index.type, values),
262
+ path: `TARGET:${relativePath}`,
263
+ summary: renderInline(resource.index.summary, values),
264
+ producedBy: renderInline(resource.index.producedBy || "preset", values),
265
+ };
266
+ }
267
+
268
+ function appendPresetRequiredReads(content, context) {
269
+ const requiredReads = context.presetPackage?.context?.requiredReads || [];
270
+ if (!requiredReads.length) return content;
271
+ const rowsById = new Map((context.resourceIndexRows?.references || []).map((row) => [row.id, row]));
272
+ const rows = requiredReads.map((id) => {
273
+ const row = rowsById.get(id);
274
+ return `| ${markdownTableCell(id)} | ${markdownTableCell(row?.path || "references/INDEX.md")} | ${markdownTableCell(row?.summary || "Preset-provided reference")} |`;
275
+ });
276
+ return `${content.trimEnd()}\n\n## Preset Required Reads\n\nOpen \`references/INDEX.md\`, then read these preset-provided references before implementation.\n\n| Reference | Path | Why |\n| --- | --- | --- |\n${rows.join("\n")}\n`;
277
+ }
278
+
279
+ function addEvidenceFile({ name, declaration, preset, target, context, add }) {
280
+ const fileName = declaration.path || `${name}.txt`;
281
+ const relativePath = toPosix(path.join(context.evidenceBundle, fileName));
282
+ const type = declaration.type || "text";
283
+ if (type === "input-json") {
284
+ add(relativePath, declaration.value || "input-json", `${JSON.stringify(getPath({ inputs: context.resolvedInputs }, declaration.value || ""), null, 2)}\n`);
285
+ } else if (type === "json") {
286
+ add(relativePath, declaration.value || "json", `${JSON.stringify(getPath({ inputs: context.resolvedInputs, values: context.values }, declaration.value || ""), null, 2)}\n`);
287
+ } else if (type === "text") {
288
+ add(relativePath, declaration.value || "text", `${String(getPath({ inputs: context.resolvedInputs, values: context.values }, declaration.value || "") || "").trim()}\n`);
289
+ } else if (type === "migration-verify") {
290
+ const session = migrationSession(context);
291
+ add(relativePath, "migrate-verify", `${JSON.stringify(verifyMigrationSession(session.sourcePath, { fullCutover: false }), null, 2)}\n`);
292
+ } else if (type === "migration-ledger") {
293
+ const session = migrationSession(context);
294
+ const verifyResult = verifyMigrationSession(session.sourcePath, { fullCutover: false });
295
+ add(relativePath, "preset-ledger", `${JSON.stringify(migrationLedger({ session, preset, verifyResult }), null, 2)}\n`);
296
+ } else if (type === "preset-manifest") {
297
+ add(relativePath, "preset.yaml", `${JSON.stringify(presetManifestSnapshot(preset), null, 2)}\n`);
298
+ } else if (type === "preset-audit") {
299
+ add(relativePath, "preset-audit", `${JSON.stringify(context.audit, null, 2)}\n`);
300
+ } else if (type === "write-scope") {
301
+ add(relativePath, "preset.yaml", `${JSON.stringify({ preset: preset.id, scopes: preset.writeScopes, entrypointScopes: context.audit.writeScopes }, null, 2)}\n`);
302
+ } else if (type === "dashboard-hash") {
303
+ add(relativePath, "dashboard", `${dashboardHash(migrationSession(context).dashboard?.indexPath || "")}\n`);
304
+ } else if (type === "target-git-status") {
305
+ add(relativePath, "session.git.after", `${JSON.stringify(migrationSession(context).git?.after || {}, null, 2)}\n`);
306
+ } else if (type === "target-commit") {
307
+ add(relativePath, "git", `${targetCommit(target.projectRoot)}\n`);
308
+ } else if (type === "harness-version") {
309
+ add(relativePath, "package.json", `${packageVersion()}\n`);
310
+ } else if (type === "generated-at") {
311
+ add(relativePath, "generated", `${new Date().toISOString()}\n`);
312
+ } else {
313
+ throw new Error(`Unsupported preset evidence type: ${type}`);
314
+ }
315
+ }
316
+
317
+ function addAuditFile({ name, preset, context, add }) {
318
+ const relativePath = toPosix(path.join(context.evidenceBundle, name));
319
+ if (name === "preset-manifest.json") {
320
+ add(relativePath, "preset.yaml", `${JSON.stringify(presetManifestSnapshot(preset), null, 2)}\n`);
321
+ } else if (name === "preset-audit.json") {
322
+ add(relativePath, "preset-audit", `${JSON.stringify(context.audit, null, 2)}\n`);
323
+ } else if (name === "write-scope.json") {
324
+ add(relativePath, "preset.yaml", `${JSON.stringify({ preset: preset.id, scopes: preset.writeScopes, entrypointScopes: context.audit.writeScopes }, null, 2)}\n`);
325
+ } else {
326
+ add(relativePath, "preset-audit", `${JSON.stringify({ preset: preset.id, generatedAt: new Date().toISOString() }, null, 2)}\n`);
327
+ }
328
+ }
329
+
330
+ function renderPresetMetadata(content, context) {
331
+ const metadata = [
332
+ context.kind && context.kind !== "general" ? `Task Kind: ${context.kind}` : "",
333
+ `Task Preset: ${context.preset}`,
334
+ `Preset Version: ${context.presetVersion}`,
335
+ context.migrationTargetLevel ? `Migration Target Level: ${context.migrationTargetLevel}` : "",
336
+ context.migrationAchievedLevel ? `Migration Achieved Level: ${context.migrationAchievedLevel}` : "",
337
+ context.evidenceBundle ? `Evidence Bundle: ${context.evidenceBundle}` : "",
338
+ ...declaredMetadataLines(context),
339
+ ].filter(Boolean).join("\n");
340
+ let next = String(content).replace(new RegExp(`^(${escapeRegExp(taskContractMarker)}\\s*)$`, "im"), `$1\n${metadata}`);
341
+ const outcome = context.presetPackage.task?.defaultOutcome || "";
342
+ if (outcome) {
343
+ next = next
344
+ .replace("[State the outcome this task must deliver in one sentence.]", outcome)
345
+ .replace("[用一句话说明本任务完成后应达到的状态。]", outcome);
346
+ }
347
+ return next;
348
+ }
349
+
350
+ function declaredMetadataLines(context) {
351
+ const base = {
352
+ inputs: context.resolvedInputs || {},
353
+ values: context.values || {},
354
+ preset: {
355
+ id: context.preset,
356
+ version: context.presetVersion,
357
+ },
358
+ task: {
359
+ id: context.taskId,
360
+ title: context.taskTitle,
361
+ kind: context.kind,
362
+ },
363
+ };
364
+ return Object.entries(context.presetPackage?.metadata || {}).map(([name, declaration]) => {
365
+ const label = declaration.label || name;
366
+ let value = "";
367
+ if (Object.prototype.hasOwnProperty.call(declaration, "from")) {
368
+ value = getPath(base, declaration.from);
369
+ } else if (Object.prototype.hasOwnProperty.call(declaration, "value")) {
370
+ value = declaration.value;
371
+ } else if (Object.prototype.hasOwnProperty.call(declaration, "default")) {
372
+ value = declaration.default;
373
+ }
374
+ return value == null || value === "" ? "" : `${label}: ${value}`;
375
+ });
376
+ }
377
+
378
+ function migrationSession(context) {
379
+ const session = Object.values(context.resolvedInputs || {}).find((value) => value && typeof value === "object" && value.operation === "migrate-run");
380
+ if (!session) throw new Error("Preset evidence requires migrate-run session input");
381
+ return session;
382
+ }
383
+
384
+ function migrationLedger({ session, preset, verifyResult }) {
385
+ const summary = session.plan?.summary || {};
386
+ return {
387
+ schemaVersion: "legacy-migration-ledger/v2",
388
+ preset: preset.id,
389
+ presetVersion: preset.version,
390
+ staticDashboardRole: "evidence-snapshot",
391
+ workbenchRole: "human-confirmation-control-plane",
392
+ phases: [
393
+ { id: "baseline", state: verifyResult.status === "pass" ? "done" : "blocked", evidence: ["session.json", "migrate-plan.json", "normal-check.json", "strict-check.json", "migrate-verify.json"] },
394
+ {
395
+ id: "mechanical-scaffold",
396
+ state: "planned",
397
+ automationAllowed: true,
398
+ outputPolicy: "May add missing task contract files and placeholders, but must not mark semantic reconstruction complete.",
399
+ counters: {
400
+ taskActions: Number(summary.taskActions || 0),
401
+ reviewSchemaGaps: Number(summary.reviewSchemaGaps || 0),
402
+ legacyReferenceGaps: Number(summary.legacyReferenceGaps || 0),
403
+ },
404
+ },
405
+ {
406
+ id: "semantic-reconstruction",
407
+ state: "planned",
408
+ automationAllowed: false,
409
+ evidenceLedgerRequired: true,
410
+ requiredEvidenceSources: ["task_plan.md", "progress.md", "review.md", "walkthrough", "Harness-Ledger", "git"],
411
+ completionRule: "Each task needs explicit evidenceSources and reviewState before semantic completion.",
412
+ },
413
+ { id: "cutover-review", state: "planned", humanConfirmationRequired: true, workbenchQueueRequired: true, staticDashboardRole: "evidence-snapshot" },
414
+ ],
415
+ counters: {
416
+ warnings: Number(summary.warnings || 0),
417
+ taskActions: Number(summary.taskActions || 0),
418
+ reviewSchemaGaps: Number(summary.reviewSchemaGaps || 0),
419
+ legacyReferenceGaps: Number(summary.legacyReferenceGaps || 0),
420
+ legacyResiduals: Number(summary.legacyResiduals || 0),
421
+ fullCutoverEligible: summary.fullCutoverEligible === true,
422
+ },
423
+ queue: [],
424
+ };
425
+ }
426
+
427
+ function presetManifestSnapshot(preset) {
428
+ return {
429
+ id: preset.id,
430
+ version: preset.version,
431
+ manifestPath: preset.manifestRelativePath,
432
+ manifestSha256: preset.manifestSha256,
433
+ compatibleBudgets: preset.compatibleBudgets,
434
+ entrypoints: preset.entrypoints,
435
+ audit: preset.audit,
436
+ writeScopes: preset.writeScopes,
437
+ inputs: preset.inputs,
438
+ templateValues: preset.templateValues,
439
+ metadata: preset.metadata,
440
+ resources: preset.resources,
441
+ context: preset.context,
442
+ };
443
+ }
444
+
445
+ function matchesScope(scope, relativePath) {
446
+ const normalizedScope = toPosix(String(scope || ""));
447
+ if (normalizedScope.endsWith("/**")) {
448
+ const prefix = normalizedScope.slice(0, -3);
449
+ return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
450
+ }
451
+ return relativePath === normalizedScope;
452
+ }
453
+
454
+ function dashboardHash(indexPath) {
455
+ if (!indexPath || !fs.existsSync(indexPath)) return "missing";
456
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(indexPath)).digest("hex")}`;
457
+ }
458
+
459
+ function targetCommit(projectRoot) {
460
+ const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
461
+ return result.status === 0 ? result.stdout.trim() : "n/a";
462
+ }
463
+
464
+ function packageVersion() {
465
+ try {
466
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "unknown";
467
+ } catch {
468
+ return "unknown";
469
+ }
470
+ }
471
+
472
+ function getPath(values, key) {
473
+ if (!key) return values;
474
+ return String(key).split(".").reduce((cursor, part) => (cursor && Object.prototype.hasOwnProperty.call(cursor, part) ? cursor[part] : undefined), values);
475
+ }
476
+
477
+ function renderInline(value, values) {
478
+ return String(value || "").replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, key) => {
479
+ const result = getPath(values, key);
480
+ return result == null ? "" : String(result);
481
+ });
482
+ }
483
+
484
+ function markdownTableCell(value) {
485
+ return String(value || "").replace(/\r?\n/g, " ").replaceAll("|", "&#124;").trim();
486
+ }
487
+
488
+ function presetIndexSkeleton(kind) {
489
+ if (kind === "references") {
490
+ return "# References Index\n\n| ID | Type | Path | Summary | Used By |\n| --- | --- | --- | --- | --- |\n";
491
+ }
492
+ return "# Artifacts Index\n\n| ID | Type | Path | Summary | Produced By |\n| --- | --- | --- | --- | --- |\n";
493
+ }
494
+
495
+ function escapeRegExp(value) {
496
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
497
+ }