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