coding-agent-harness 1.0.2 → 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 (219) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/LICENSE +661 -21
  4. package/LICENSE-EXCEPTION.md +37 -0
  5. package/README.md +244 -87
  6. package/README.zh-CN.md +77 -35
  7. package/SKILL.md +32 -24
  8. package/docs-release/README.md +9 -5
  9. package/docs-release/architecture/overview.md +17 -5
  10. package/docs-release/architecture/overview.zh-CN.md +9 -5
  11. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  12. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  13. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  14. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  15. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  16. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  17. package/docs-release/architecture/system-explainer/README.md +67 -0
  18. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  19. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  20. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  21. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  22. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  23. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  24. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  25. package/docs-release/assets/dashboard-overview.png +0 -0
  26. package/docs-release/guides/agent-installation.en-US.md +39 -15
  27. package/docs-release/guides/agent-installation.md +43 -16
  28. package/docs-release/guides/contributing.md +100 -0
  29. package/docs-release/guides/contributing.zh-CN.md +99 -0
  30. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  31. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  32. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  33. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  34. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  35. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  36. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  37. package/docs-release/guides/migration-playbook.md +14 -15
  38. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  39. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  40. package/docs-release/guides/preset-development.md +238 -0
  41. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  42. package/docs-release/guides/repository-operating-models.md +5 -4
  43. package/docs-release/guides/task-state-machine.en-US.md +224 -0
  44. package/docs-release/guides/task-state-machine.md +231 -0
  45. package/docs-release/intl/en-US.md +1 -1
  46. package/docs-release/intl/zh-CN.md +1 -1
  47. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  48. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  49. package/package.json +10 -4
  50. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  51. package/presets/legacy-migration/preset.yaml +134 -0
  52. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  53. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  54. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  55. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  56. package/presets/legacy-migration/templates/review.seed.md +12 -0
  57. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  58. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  59. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  60. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  61. package/presets/lesson-sedimentation/preset.yaml +23 -0
  62. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  63. package/presets/module/preset.yaml +25 -0
  64. package/presets/module/templates/execution_strategy.append.md +8 -0
  65. package/presets/module/templates/task_plan.append.md +17 -0
  66. package/presets/standard-task/preset.yaml +31 -0
  67. package/presets/standard-task/templates/task_plan.append.md +7 -0
  68. package/references/adversarial-review-standard.md +2 -2
  69. package/references/agents-md-pattern.md +2 -2
  70. package/references/delivery-operating-model-standard.md +3 -3
  71. package/references/docs-directory-standard.md +6 -7
  72. package/references/harness-ledger.md +53 -96
  73. package/references/lessons-governance.md +88 -93
  74. package/references/module-parallel-standard.md +14 -14
  75. package/references/planning-loop.md +12 -6
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +11 -2
  78. package/references/review-routing-standard.md +7 -1
  79. package/references/ssot-governance.md +67 -59
  80. package/references/taskr-gap-analysis.md +600 -0
  81. package/references/walkthrough-closeout.md +7 -7
  82. package/scripts/check-harness.mjs +40 -301
  83. package/scripts/commands/dashboard-command.mjs +67 -0
  84. package/scripts/commands/migration-command.mjs +126 -0
  85. package/scripts/commands/preset-command.mjs +73 -0
  86. package/scripts/commands/task-command.mjs +328 -0
  87. package/scripts/harness.mjs +59 -260
  88. package/scripts/lib/capability-registry.mjs +82 -28
  89. package/scripts/lib/check-module-parallel.mjs +230 -0
  90. package/scripts/lib/check-profiles.mjs +90 -228
  91. package/scripts/lib/check-task-contracts.mjs +55 -0
  92. package/scripts/lib/core-shared.mjs +65 -2
  93. package/scripts/lib/dashboard-data.mjs +155 -24
  94. package/scripts/lib/dashboard-workbench.mjs +131 -12
  95. package/scripts/lib/dashboard-writer.mjs +20 -4
  96. package/scripts/lib/git-status-summary.mjs +46 -0
  97. package/scripts/lib/governance-index-generator.mjs +174 -0
  98. package/scripts/lib/governance-sync.mjs +611 -0
  99. package/scripts/lib/governance-table-boundary.mjs +175 -0
  100. package/scripts/lib/harness-core.mjs +6 -0
  101. package/scripts/lib/lesson-maintenance.mjs +36 -29
  102. package/scripts/lib/markdown-utils.mjs +33 -0
  103. package/scripts/lib/migration-planner.mjs +4 -6
  104. package/scripts/lib/migration-support.mjs +1 -1
  105. package/scripts/lib/phase-kind.mjs +50 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +494 -0
  108. package/scripts/lib/preset-registry.mjs +776 -0
  109. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  110. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  111. package/scripts/lib/status-builder.mjs +88 -0
  112. package/scripts/lib/status-dashboard-renderer.mjs +105 -0
  113. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  114. package/scripts/lib/task-audit-metadata.mjs +385 -0
  115. package/scripts/lib/task-audit-migration.mjs +350 -0
  116. package/scripts/lib/task-completion-consistency.mjs +26 -0
  117. package/scripts/lib/task-index.mjs +93 -0
  118. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  119. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  120. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  121. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  122. package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
  123. package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
  124. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  125. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  126. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  127. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  128. package/scripts/lib/task-lifecycle.mjs +338 -477
  129. package/scripts/lib/task-metadata.mjs +118 -0
  130. package/scripts/lib/task-review-model.mjs +455 -0
  131. package/scripts/lib/task-scanner.mjs +193 -372
  132. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  133. package/scripts/postinstall.mjs +14 -0
  134. package/skills/preset-creator/SKILL.md +179 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  137. package/skills/preset-creator/references/complex-task-skeleton/brief.md +43 -0
  138. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  139. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  140. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  141. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  142. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  143. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  144. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  145. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  146. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  147. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  148. package/templates/AGENTS.md.template +24 -18
  149. package/templates/dashboard/assets/app-src/00-state.js +13 -0
  150. package/templates/dashboard/assets/app-src/10-router.js +5 -1
  151. package/templates/dashboard/assets/app-src/20-overview.js +18 -8
  152. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  153. package/templates/dashboard/assets/app-src/35-task-detail.js +286 -0
  154. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  155. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  156. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  157. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  158. package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
  159. package/templates/dashboard/assets/app.css +1501 -376
  160. package/templates/dashboard/assets/app.css.manifest.json +10 -0
  161. package/templates/dashboard/assets/app.js +1240 -101
  162. package/templates/dashboard/assets/app.manifest.json +2 -0
  163. package/templates/dashboard/assets/css-src/00-foundation.css +346 -0
  164. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  165. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  166. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  167. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  168. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +489 -0
  169. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  170. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  171. package/templates/dashboard/assets/i18n.js +263 -23
  172. package/templates/ledger/Harness-Ledger.md +13 -25
  173. package/templates/lessons/lesson-arch-process-change.md +1 -1
  174. package/templates/lessons/lesson-new-doc.md +1 -1
  175. package/templates/lessons/lesson-ref-change.md +1 -1
  176. package/templates/planning/INDEX.md +87 -0
  177. package/templates/planning/brief.md +1 -1
  178. package/templates/planning/execution_strategy.md +31 -0
  179. package/templates/planning/lesson_candidates.md +18 -6
  180. package/templates/planning/module_session_prompt.md +1 -0
  181. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  182. package/templates/planning/optional/references/INDEX.md +3 -3
  183. package/templates/planning/review.md +41 -0
  184. package/templates/planning/task_plan.md +5 -21
  185. package/templates/planning/visual_map.md +13 -9
  186. package/templates/planning/visual_map.simple.md +52 -0
  187. package/templates/reference/execution-workflow-standard.md +31 -3
  188. package/templates/reference/pull-request-standard.md +80 -0
  189. package/templates/reference/repo-governance-standard.md +7 -6
  190. package/templates/reference/review-routing-standard.md +6 -0
  191. package/templates/reference/walkthrough-standard.md +2 -1
  192. package/templates/verifier/verifier-output.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +25 -19
  194. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  195. package/templates-zh-CN/planning/INDEX.md +87 -0
  196. package/templates-zh-CN/planning/brief.md +1 -1
  197. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  198. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  199. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  200. package/templates-zh-CN/planning/review.md +41 -1
  201. package/templates-zh-CN/planning/task_plan.md +4 -44
  202. package/templates-zh-CN/planning/visual_map.md +14 -7
  203. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  204. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  205. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  206. package/templates-zh-CN/reference/execution-workflow-standard.md +33 -7
  207. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  208. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  209. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  210. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  211. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  212. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  213. package/docs-release/assets/dashboard-overview-en.png +0 -0
  214. package/scripts/smoke-dashboard.mjs +0 -92
  215. package/scripts/test-harness.mjs +0 -1395
  216. package/templates/ssot/Feature-SSoT.md +0 -43
  217. package/templates/ssot/Lessons-SSoT.md +0 -44
  218. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  219. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -1,1395 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { spawn, spawnSync } from "node:child_process";
7
-
8
- const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
9
- const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
10
- const node = process.execPath;
11
- const cli = path.join(repoRoot, "scripts/harness.mjs");
12
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "harness-v1-"));
13
- const chineseCharacterPattern = /\p{Script=Han}/u;
14
- const brokenMechanicalTemplatePattern = /\bfill in(?:[A-Z]|\w)|(?:[a-z])fill in\b|TODO/;
15
- const staleDispositionPattern = /\b((?:open\s*\/\s*)?fixed\s*\/\s*accepted\s*\/\s*deferred\s*\/\s*n\/a|accepted[- ]residuals?|accepted\s+(?:with|as)\s+residual|accepted\s+by\s+owner|accepted\s+waiver)\b/i;
16
- const sampleOpenFindingPattern = /^\|\s*(?:F|R|SR|V|RR|HL)-\d+\s*\|.*\|\s*(?:open|yes\s*\|\s*open|yes\s*\/\s*no\s*\|\s*open)\s*\|?\s*$/im;
17
- const englishFirstZhHeadingPattern = /^#{1,6}\s+(?:Reviewer Identity|Confidence Challenge|Material Findings|Non-Material Notes|Evidence Checked|Final Confidence Basis|Follow-Up Routing|Phase Graph|Phase Table|Context Packet|Artifact Index|Stop Condition|Pause Conditions|Deliverables|Module Session Prompt|Subagent\s*\/\s*Worker|Coordinator|Worktree|Slice ID|Parent Phase|Inputs|Verifier\b|Harness\b|Closeout\b|Lessons\b)/m;
18
- const zhMechanicalEnglishWorkflowPattern = /^\s*\d+\.\s*(?:implement|run locally|self-review|rerun evidence)\b/im;
19
- const zhMechanicalEvidencePhrasePattern = /\b(?:local smoke|browser or UI inspection|live environment smoke|reviewer findings|PR checks\s*\/\s*workflow run)\b/i;
20
- const { taskMigrationClassification, requiresCanonicalVisualMap } = await import("./lib/harness-core.mjs");
21
- const todayLocal = (() => {
22
- const now = new Date();
23
- const y = now.getFullYear();
24
- const m = String(now.getMonth() + 1).padStart(2, "0");
25
- const d = String(now.getDate()).padStart(2, "0");
26
- return `${y}-${m}-${d}`;
27
- })();
28
-
29
- function run(args, options = {}) {
30
- const result = spawnSync(node, [cli, ...args], {
31
- cwd: repoRoot,
32
- encoding: "utf8",
33
- ...options,
34
- });
35
- return result;
36
- }
37
-
38
- function assert(condition, message) {
39
- if (!condition) throw new Error(message);
40
- }
41
-
42
- function expectPass(args) {
43
- const result = run(args);
44
- assert(result.status === 0, `${args.join(" ")} failed\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`);
45
- return result;
46
- }
47
-
48
- function expectJson(args) {
49
- const result = expectPass(args);
50
- return JSON.parse(result.stdout);
51
- }
52
-
53
- function waitForWorkbench(child) {
54
- return new Promise((resolve, reject) => {
55
- let stdout = "";
56
- let stderr = "";
57
- const timer = setTimeout(() => {
58
- child.kill("SIGTERM");
59
- reject(new Error(`workbench did not start\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
60
- }, 8000);
61
- child.stdout.on("data", (chunk) => {
62
- stdout += chunk.toString();
63
- const match = stdout.match(/(?:dashboard workbench|harness dev):\s+(http:\/\/127\.0\.0\.1:\d+\/)\s+csrf=([a-f0-9]+)/i);
64
- if (!match) return;
65
- clearTimeout(timer);
66
- resolve({ url: match[1], csrf: match[2], stdout, stderr });
67
- });
68
- child.stderr.on("data", (chunk) => {
69
- stderr += chunk.toString();
70
- });
71
- child.on("exit", (code) => {
72
- if (code !== null && code !== 0) {
73
- clearTimeout(timer);
74
- reject(new Error(`workbench exited with ${code}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
75
- }
76
- });
77
- });
78
- }
79
-
80
- async function waitForCondition(fn, message, { timeout = 8000, interval = 200 } = {}) {
81
- const started = Date.now();
82
- let lastValue;
83
- while (Date.now() - started < timeout) {
84
- lastValue = await fn();
85
- if (lastValue) return lastValue;
86
- await new Promise((resolve) => setTimeout(resolve, interval));
87
- }
88
- throw new Error(`${message}: ${JSON.stringify(lastValue)}`);
89
- }
90
-
91
- function commandExists(command) {
92
- const result = spawnSync(command, ["-v"], { encoding: "utf8" });
93
- return !result.error && result.status === 0;
94
- }
95
-
96
- function runInTty(args, options = {}) {
97
- const input = options.input || "";
98
- const timeout = options.timeout;
99
- const expectLines = [
100
- `set timeout ${Math.ceil((timeout || 5000) / 1000)}`,
101
- `spawn ${[node, cli, ...args].map(tclWord).join(" ")}`,
102
- ];
103
- if (input) {
104
- expectLines.push("expect -re {Language \\[1/2}");
105
- expectLines.push(`send -- ${tclWord(input.replace(/\n/g, "\r"))}`);
106
- }
107
- expectLines.push("expect eof");
108
- expectLines.push("catch wait result");
109
- expectLines.push("exit [lindex $result 3]");
110
- return spawnSync("expect", ["-c", expectLines.join("\n")], {
111
- cwd: repoRoot,
112
- encoding: "utf8",
113
- timeout,
114
- });
115
- }
116
-
117
- function expectTtyJson(args, options = {}) {
118
- const result = runInTty(args, options);
119
- assert(result.status === 0, `tty ${args.join(" ")} failed\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`);
120
- return parseJsonFromOutput(result.stdout);
121
- }
122
-
123
- function parseJsonFromOutput(output) {
124
- const start = output.indexOf("{");
125
- const end = output.lastIndexOf("}");
126
- assert(start >= 0 && end > start, `output did not contain JSON\n${output}`);
127
- return JSON.parse(output.slice(start, end + 1));
128
- }
129
-
130
- function tclWord(value) {
131
- return `{${String(value).replace(/\\/g, "\\\\").replace(/}/g, "\\}")}}`;
132
- }
133
-
134
- function acceptNoLessonCandidate(taskDir) {
135
- const candidatePath = path.join(taskDir, "lesson_candidates.md");
136
- let content = fs.readFileSync(candidatePath, "utf8");
137
- content = content
138
- .replace("| Task-level status | pending-review |", "| Task-level status | no-candidate-accepted |")
139
- .replace("| Review decision | pending-human-review |", "| Review decision | accepted-no-candidate |")
140
- .replace("| Closeout token | pending |", "| Closeout token | checked-candidate:LC-TEST-000 |")
141
- .replace(
142
- "Not decided yet. Fill this only when review accepts that the task produced no reusable lesson candidate.",
143
- "Human review accepted that this fixture produced no reusable lesson candidate.",
144
- )
145
- .replace("尚未判定。只有人工审查接受本任务没有可复用候选时,才填写这里。", "人工审查已接受该测试夹具没有可复用教训候选。");
146
- fs.writeFileSync(candidatePath, content);
147
- }
148
-
149
- const skillContent = fs.readFileSync(path.join(repoRoot, "SKILL.md"), "utf8");
150
- assert(!skillContent.includes("Historical 12-Phase Bootstrap"), "SKILL.md should not carry the legacy 12-phase reference body");
151
- assert(
152
- skillContent.includes("references/legacy-12-phase-bootstrap.md"),
153
- "SKILL.md should route legacy bootstrap details to the reference document",
154
- );
155
- assert(
156
- fs.readFileSync(path.join(repoRoot, "references/legacy-12-phase-bootstrap.md"), "utf8").includes("Historical 12-Phase Bootstrap"),
157
- "legacy 12-phase bootstrap reference should exist",
158
- );
159
-
160
- expectPass(["check", "--profile", "source-package", "."]);
161
- if (fs.existsSync(path.join(repoRoot, ".harness-private"))) {
162
- expectPass(["check", "--profile", "private-harness", ".harness-private"]);
163
- const privateStatus = expectJson(["status", "--json", ".harness-private"]);
164
- assert(privateStatus.tasks.length >= 1, "private-harness status JSON should be complete and parseable");
165
- }
166
-
167
- const sourceBoundaryTarget = path.join(tmpRoot, "source-boundary-target");
168
- fs.mkdirSync(path.join(sourceBoundaryTarget, "scripts"), { recursive: true });
169
- fs.mkdirSync(path.join(sourceBoundaryTarget, "templates/planning"), { recursive: true });
170
- fs.mkdirSync(path.join(sourceBoundaryTarget, "docs/private"), { recursive: true });
171
- fs.mkdirSync(path.join(sourceBoundaryTarget, ".harness-private"), { recursive: true });
172
- fs.writeFileSync(path.join(sourceBoundaryTarget, "package.json"), "{}\n");
173
- fs.writeFileSync(path.join(sourceBoundaryTarget, "scripts/harness.mjs"), "#!/usr/bin/env node\n");
174
- fs.writeFileSync(path.join(sourceBoundaryTarget, "scripts/check-harness.mjs"), "#!/usr/bin/env node\n");
175
- fs.writeFileSync(path.join(sourceBoundaryTarget, "templates/planning/task_plan.md"), "# Task\n");
176
- fs.writeFileSync(path.join(sourceBoundaryTarget, "AGENTS.md"), "# Local only\n");
177
- fs.writeFileSync(path.join(sourceBoundaryTarget, "docs/private/plan.md"), "# Private\n");
178
- fs.writeFileSync(path.join(sourceBoundaryTarget, ".harness-private/AGENTS.md"), "# Private harness\n");
179
- spawnSync("git", ["init"], { cwd: sourceBoundaryTarget, encoding: "utf8" });
180
- spawnSync("git", ["add", "-f", "AGENTS.md", "docs/private/plan.md", ".harness-private/AGENTS.md"], { cwd: sourceBoundaryTarget, encoding: "utf8" });
181
- const sourceBoundaryCheck = run(["check", "--profile", "source-package", sourceBoundaryTarget]);
182
- assert(sourceBoundaryCheck.status !== 0, "source-package check should reject staged local-only harness files");
183
- assert(sourceBoundaryCheck.stderr.includes("private local-only file staged: AGENTS.md"), "source-package check should report staged AGENTS.md");
184
- assert(sourceBoundaryCheck.stderr.includes("private local-only file staged: docs/private/plan.md"), "source-package check should report staged docs/");
185
- assert(sourceBoundaryCheck.stderr.includes("private local-only file staged: .harness-private/AGENTS.md"), "source-package check should report staged .harness-private/");
186
-
187
- const englishTemplateFiles = relativeFiles(path.join(repoRoot, "templates"));
188
- const chineseTemplateFiles = relativeFiles(path.join(repoRoot, "templates-zh-CN"));
189
- const englishNonDashboardTemplateFiles = englishTemplateFiles.filter((file) => !file.startsWith("dashboard/"));
190
- const chineseNonDashboardTemplateFiles = chineseTemplateFiles.filter((file) => !file.startsWith("dashboard/"));
191
- assert(englishTemplateFiles.length > 0, "templates/ should contain English templates");
192
- assert(chineseTemplateFiles.length > 0, "templates-zh-CN/ should contain Chinese templates");
193
- assert(
194
- JSON.stringify(englishNonDashboardTemplateFiles) === JSON.stringify(chineseNonDashboardTemplateFiles),
195
- "templates/ and templates-zh-CN/ should expose the same non-dashboard template file set",
196
- );
197
- assert(!chineseTemplateFiles.some((file) => file.startsWith("dashboard/")), "templates-zh-CN/dashboard should be removed; dashboard uses runtime i18n");
198
- for (const relativeFile of englishNonDashboardTemplateFiles) {
199
- const content = fs.readFileSync(path.join(repoRoot, "templates", relativeFile), "utf8");
200
- assert(!chineseCharacterPattern.test(content), `English template contains Chinese text: ${relativeFile}`);
201
- assert(!brokenMechanicalTemplatePattern.test(content), `English template contains mechanical placeholder text: ${relativeFile}`);
202
- assert(!staleDispositionPattern.test(content), `English template contains stale disposition vocabulary: ${relativeFile}`);
203
- assert(!sampleOpenFindingPattern.test(content), `English template contains a real open sample finding row: ${relativeFile}`);
204
- }
205
- assert(
206
- fs.readFileSync(path.join(repoRoot, "templates-zh-CN", "AGENTS.md.template"), "utf8").includes("项目概况"),
207
- "templates-zh-CN should provide Chinese AGENTS.md content",
208
- );
209
- for (const relativeFile of chineseNonDashboardTemplateFiles) {
210
- const content = fs.readFileSync(path.join(repoRoot, "templates-zh-CN", relativeFile), "utf8");
211
- assert(!brokenMechanicalTemplatePattern.test(content), `Chinese template contains mechanical placeholder text: ${relativeFile}`);
212
- assert(!staleDispositionPattern.test(content), `Chinese template contains stale disposition vocabulary: ${relativeFile}`);
213
- assert(!sampleOpenFindingPattern.test(content), `Chinese template contains a real open sample finding row: ${relativeFile}`);
214
- assert(!englishFirstZhHeadingPattern.test(content), `Chinese template contains English-first review heading: ${relativeFile}`);
215
- assert(!zhMechanicalEnglishWorkflowPattern.test(content), `Chinese template contains unlocalized workflow phrase: ${relativeFile}`);
216
- assert(!zhMechanicalEvidencePhrasePattern.test(content), `Chinese template contains unlocalized evidence phrase: ${relativeFile}`);
217
- }
218
-
219
- assert(taskMigrationClassification("unknown", "legacy-only") === "unknown-needs-human", "unknown legacy-only task should require human classification");
220
- assert(taskMigrationClassification("done", "not-needed") === "historical-no-map-needed", "done task with not-needed visual map should not require migration action");
221
- assert(taskMigrationClassification("active", "present") === "active", "active task with canonical visual map should remain active");
222
- assert(taskMigrationClassification("reopened", "missing") === "active", "reopened task should be treated as active migration work");
223
- assert(
224
- requiresCanonicalVisualMap({ migrationClassification: "historical-no-map-needed" }) === false,
225
- "historical-no-map-needed should not generate a canonical visual map action",
226
- );
227
-
228
- const exampleStatus = expectJson(["status", "--json", "examples/minimal-project"]);
229
- assert(exampleStatus.project.name === "minimal-project", "example status project name mismatch");
230
- assert(Array.isArray(exampleStatus.tasks), "example status missing tasks array");
231
- assert(exampleStatus.tasks[0].state === "in_progress", "task state was not normalized");
232
- assert(Array.isArray(exampleStatus.tasks[0].phases[0].requiredEvidence), "requiredEvidence must be an array");
233
- assert(exampleStatus.capabilities.some((capability) => capability.name === "core"), "example status missing core capability");
234
-
235
- const dashboardPath = path.join(tmpRoot, "dashboard.html");
236
- expectPass(["dashboard", "--out", dashboardPath, "examples/minimal-project"]);
237
- assert(fs.existsSync(dashboardPath), "dashboard file was not created");
238
- const dashboardHtml = fs.readFileSync(dashboardPath, "utf8");
239
- assert(dashboardHtml.includes("Harness Dashboard"), "dashboard HTML missing title");
240
- assert(dashboardHtml.includes("window.__HARNESS_DASHBOARD__"), "dashboard HTML missing inline data bundle");
241
- assert(dashboardHtml.includes("Human Visibility Dashboard"), "dashboard HTML missing v2 visibility copy");
242
- assert(dashboardHtml.includes("#/tasks"), "dashboard HTML missing task index route");
243
- assert(dashboardHtml.includes("#/review"), "dashboard HTML missing review queue route");
244
- assert(dashboardHtml.includes("function reviewQueue()"), "dashboard HTML missing review queue page implementation");
245
-
246
- const dashboardDir = path.join(tmpRoot, "dashboard-folder");
247
- expectPass(["dashboard", "--out-dir", dashboardDir, "examples/minimal-project"]);
248
- for (const required of [
249
- "index.html",
250
- "assets/app.css",
251
- "assets/app.js",
252
- "assets/i18n.js",
253
- "assets/markdown-reader.js",
254
- "assets/mermaid-renderer.js",
255
- "assets/dashboard-data.js",
256
- "data/status.json",
257
- "data/tables.json",
258
- "data/documents.json",
259
- "data/graph.json",
260
- "data/adoption.json",
261
- ]) {
262
- assert(fs.existsSync(path.join(dashboardDir, required)), `dashboard folder missing ${required}`);
263
- }
264
- const folderIndex = fs.readFileSync(path.join(dashboardDir, "index.html"), "utf8");
265
- assert(folderIndex.includes("dashboard-data.js"), "dashboard folder index missing embedded data script");
266
- assert(folderIndex.includes("rel=\"icon\""), "dashboard index should suppress favicon request");
267
- const folderStatus = JSON.parse(fs.readFileSync(path.join(dashboardDir, "data/status.json"), "utf8"));
268
- assert(folderStatus.tasks[0].visualMapSource === "canonical", "folder status should use canonical visual_map.md");
269
- assert(folderStatus.tasks[0].roadmapSource === "canonical", "folder status should preserve roadmapSource compatibility as canonical");
270
- assert(folderStatus.schemaVersion === 2, "dashboard folder status should expose schemaVersion 2");
271
- assert(folderStatus.summary.fullCutoverEligible === true, "minimal project should expose fullCutoverEligible=true");
272
- assert(folderStatus.summary.legacyVisualOnlyCount === 0, "minimal project should expose zero legacy visual-only tasks");
273
- assert(folderStatus.summary.weakBriefCount === 0, "minimal project should expose zero weak briefs");
274
- assert(folderStatus.summary.unknownClassificationCount === 0, "minimal project should expose zero unknown migration classifications");
275
- assert(folderStatus.summary.missingCanonicalVisualMapCount === 0, "minimal project should expose zero missing canonical visual maps");
276
- const documents = JSON.parse(fs.readFileSync(path.join(dashboardDir, "data/documents.json"), "utf8"));
277
- assert(documents.documents.some((doc) => doc.path.endsWith("/brief.md")), "documents should include task briefs");
278
- assert(documents.documents.some((doc) => doc.path.endsWith("/task_plan.md")), "documents should include task plan fallback");
279
- assert(documents.documents.some((doc) => doc.path.endsWith("execution_strategy.md")), "documents missing execution strategy");
280
- assert(documents.documents.some((doc) => doc.path.endsWith("visual_map.md")), "documents missing visual map");
281
- assert(documents.documents.some((doc) => doc.path.endsWith("lesson_candidates.md")), "documents missing lesson candidates");
282
- const tables = JSON.parse(fs.readFileSync(path.join(dashboardDir, "data/tables.json"), "utf8"));
283
- assert(tables.tables.some((table) => table.kind === "harness-ledger"), "documents missing harness ledger table");
284
- assert(JSON.stringify(tables).includes("alpha|beta"), "markdown table parser should preserve escaped pipes");
285
- const graph = JSON.parse(fs.readFileSync(path.join(dashboardDir, "data/graph.json"), "utf8"));
286
- assert(graph.edges.length > 0, "graph should include task/phase edges");
287
- assertGraphIntegrity(graph, "example graph");
288
- const dashboardApp = fs.readFileSync(path.join(dashboardDir, "assets/app.js"), "utf8");
289
- const dashboardMarkdown = fs.readFileSync(path.join(dashboardDir, "assets/markdown-reader.js"), "utf8");
290
- const dashboardMermaid = fs.readFileSync(path.join(dashboardDir, "assets/mermaid-renderer.js"), "utf8");
291
- assert(dashboardApp.includes("hashchange"), "dashboard should use hash routing");
292
- assert(dashboardApp.includes("taskDetail("), "dashboard should implement task detail route");
293
- assert(dashboardApp.includes("data-render-toggle"), "dashboard missing render/source toggle");
294
- assert(dashboardApp.includes("data-search"), "dashboard missing task search control");
295
- assert(dashboardApp.includes("taskGroupsPerPage"), "dashboard missing global task group paging");
296
- assert(dashboardApp.includes("taskStatsBar"), "dashboard missing task stats bar");
297
- assert(dashboardApp.includes("task-row-card"), "dashboard missing upgraded task row card");
298
- assert(dashboardApp.includes("fullCutoverEligible"), "dashboard missing full cutover summary field");
299
- assert(dashboardApp.includes("legacyVisualOnlyCount"), "dashboard missing legacy visual-only summary field");
300
- assert(dashboardApp.includes("weakBriefCount"), "dashboard missing weak brief summary field");
301
- assert(dashboardApp.includes("warningQueue()"), "dashboard missing warning queue workbench");
302
- assert(dashboardApp.includes("reviewWorkspace("), "dashboard missing review workspace route implementation");
303
- assert(dashboardApp.includes("[\"lessonCandidates\", \"lesson_candidates.md\"]"), "dashboard should expose lesson candidate documents");
304
- assert(dashboardApp.includes("migrationRunwayBreakdown"), "dashboard missing aggregate migration runway drilldown");
305
- assert(dashboardApp.includes("[\"brief\", \"brief.md\"]"), "dashboard should make brief.md the first task detail tab");
306
- assert(dashboardApp.includes("[\"visualMap\", \"visual_map.md\"]"), "dashboard should expose canonical visual_map.md tab");
307
- assert(dashboardApp.includes("projectMermaid"), "dashboard should render project flow from graph data");
308
- assert(dashboardApp.includes("escapeHtml(projectName())"), "dashboard project title must be escaped");
309
- assert(dashboardMarkdown.includes("rendered-table"), "dashboard missing rendered markdown table support");
310
- assert(dashboardMermaid.includes("mermaid-rendered"), "dashboard missing rendered mermaid output");
311
- const dashboardCss = fs.readFileSync(path.join(dashboardDir, "assets/app.css"), "utf8");
312
- assert(dashboardCss.includes(".runtime-banner"), "dashboard missing static read-only banner styling");
313
- assert(dashboardCss.includes("max-height: min(68vh, 620px)"), "dashboard missing mermaid viewport containment");
314
- assert(dashboardCss.includes(".review-workspace-grid"), "dashboard missing review workspace layout");
315
- for (const generated of ["data/status.json", "data/tables.json", "data/documents.json", "data/graph.json", "data/adoption.json", "assets/dashboard-data.js"]) {
316
- const content = fs.readFileSync(path.join(dashboardDir, generated), "utf8");
317
- assert(!content.includes(repoRoot), `${generated} leaked absolute repo path`);
318
- assert(!content.includes("file://"), `${generated} leaked file URL`);
319
- assert(!hasLocalAbsolutePath(content), `${generated} leaked local absolute path`);
320
- }
321
- assert(!JSON.stringify(documents.documents.map((doc) => doc.path)).includes("_task-template"), "documents included task template paths");
322
-
323
- const unsafeOut = run(["dashboard", "--out-dir", ".", "examples/minimal-project"]);
324
- assert(unsafeOut.status !== 0, "dashboard --out-dir . should be refused");
325
- const unsafeDocsOut = run(["dashboard", "--out-dir", "examples/minimal-project/docs", "examples/minimal-project"]);
326
- assert(unsafeDocsOut.status !== 0, "dashboard --out-dir target docs should be refused");
327
- const unsafeDocsChildOut = run(["dashboard", "--out-dir", "examples/minimal-project/docs/generated-dashboard", "examples/minimal-project"]);
328
- assert(unsafeDocsChildOut.status !== 0, "dashboard --out-dir inside target docs should be refused");
329
- const staticWorkbenchFlagDir = path.join(tmpRoot, "static-workbench-flag");
330
- expectPass(["dashboard", "--out-dir", staticWorkbenchFlagDir, "examples/minimal-project"]);
331
- assert(
332
- fs.readFileSync(path.join(staticWorkbenchFlagDir, "index.html"), "utf8").includes("__HARNESS_WORKBENCH__ = false"),
333
- "static dashboard folder should not enable workbench runtime",
334
- );
335
- assert(
336
- fs.readFileSync(path.join(staticWorkbenchFlagDir, "assets/app.js"), "utf8").includes("staticReadOnly"),
337
- "static dashboard app should render a visible read-only runtime boundary",
338
- );
339
- const helpOutput = expectPass(["help"]).stdout;
340
- assert(helpOutput.includes("harness dev"), "help should advertise harness dev as the daily dynamic workbench entry");
341
-
342
- const redactionTarget = path.join(tmpRoot, "redaction-target");
343
- fs.mkdirSync(path.join(redactionTarget, "docs/09-PLANNING/TASKS/path-check"), { recursive: true });
344
- fs.writeFileSync(path.join(redactionTarget, "AGENTS.md"), "# AGENTS\n");
345
- fs.writeFileSync(path.join(redactionTarget, "docs/09-PLANNING/TASKS/path-check/task_plan.md"), "# Path Check\n");
346
- fs.writeFileSync(
347
- path.join(redactionTarget, "docs/09-PLANNING/TASKS/path-check/progress.md"),
348
- "# Progress\n\n## Status\n\nin_progress\n\ncommand:TARGET:logs/check.txt: touched /tmp/secret and C:\\Users\\name\\secret\n",
349
- );
350
- const redactionDir = path.join(tmpRoot, "redaction-dashboard");
351
- expectPass(["dashboard", "--out-dir", redactionDir, redactionTarget]);
352
- const redactionData = fs.readFileSync(path.join(redactionDir, "assets/dashboard-data.js"), "utf8");
353
- assert(redactionData.includes("LOCAL_PATH_REDACTED"), "dashboard data should include redacted local paths");
354
- assert(!hasLocalAbsolutePath(redactionData), "dashboard data leaked generic local path");
355
-
356
- const dryRunTarget = path.join(tmpRoot, "dry-run-target");
357
- fs.mkdirSync(dryRunTarget);
358
- const dryRun = expectJson(["init", "--dry-run", "--locale", "zh-CN", "--capabilities", "core,dashboard", dryRunTarget]);
359
- assert(dryRun.dryRun === true, "init dry-run did not report dryRun true");
360
- assert(dryRun.locale === "zh-CN", "init dry-run did not preserve zh-CN locale");
361
- assert(dryRun.nextCommands?.some((command) => command.includes("coding-agent-harness dev")), "init output should recommend harness dev as the next human dashboard command");
362
- assert(
363
- dryRun.changes.filter((change) => change.destination.startsWith("docs/11-REFERENCE/")).every((change) => change.destination === "docs/11-REFERENCE/external-source-intake-standard.md"),
364
- "init scaffold should only copy the external source intake standard as a core reference",
365
- );
366
- assert(
367
- dryRun.changes.some((change) => change.source === "templates-zh-CN/planning/task_plan.md"),
368
- "init zh-CN dry-run should use localized task_plan template when available",
369
- );
370
- assert(!fs.existsSync(path.join(dryRunTarget, "AGENTS.md")), "init dry-run mutated target");
371
-
372
- const nonInteractiveDefaultTarget = path.join(tmpRoot, "non-interactive-default-target");
373
- fs.mkdirSync(nonInteractiveDefaultTarget);
374
- const nonInteractiveDefault = expectJson(["init", "--dry-run", "--capabilities", "core", nonInteractiveDefaultTarget]);
375
- assert(nonInteractiveDefault.locale === "en-US", "non-interactive init without --locale should default to en-US");
376
-
377
- if (commandExists("expect")) {
378
- const interactiveZhTarget = path.join(tmpRoot, "interactive-zh-target");
379
- fs.mkdirSync(interactiveZhTarget);
380
- const interactiveZh = expectTtyJson(["init", "--dry-run", "--capabilities", "core,dashboard", interactiveZhTarget], { input: "1\n", timeout: 5000 });
381
- assert(interactiveZh.locale === "zh-CN", "interactive init option 1 should select zh-CN");
382
- assert(
383
- interactiveZh.changes.some((change) => change.source === "templates-zh-CN/planning/task_plan.md"),
384
- "interactive zh-CN init should use localized templates",
385
- );
386
-
387
- const ttyExplicitTarget = path.join(tmpRoot, "tty-explicit-target");
388
- fs.mkdirSync(ttyExplicitTarget);
389
- const ttyExplicit = expectTtyJson(["init", "--dry-run", "--locale", "en-US", "--capabilities", "core", ttyExplicitTarget], { timeout: 5000 });
390
- assert(ttyExplicit.locale === "en-US", "explicit --locale should win in TTY init");
391
- } else {
392
- console.log("Skipping TTY init tests: expect command is unavailable");
393
- }
394
-
395
- const zhInitTarget = path.join(tmpRoot, "zh-init-target");
396
- fs.mkdirSync(zhInitTarget);
397
- const zhInit = expectJson(["init", "--locale", "zh-CN", "--capabilities", "core,dashboard", zhInitTarget]);
398
- assert(zhInit.report?.locale === "zh-CN", "init output should include install report locale");
399
- assert(zhInit.report?.capabilities?.some((capability) => capability.name === "core" && capability.default === true), "install report should explain core as default");
400
- assert(zhInit.report?.capabilities?.some((capability) => capability.name === "dashboard" && capability.selected === true), "install report should mark selected capabilities");
401
- assert(zhInit.report?.agentInstructions?.some((item) => item.includes("--locale")), "install report should remind agents to pass --locale explicitly");
402
- assert(zhInit.nextCommands?.some((command) => command.includes("coding-agent-harness dev")), "init should print a dev workbench next command");
403
- const zhRegistry = JSON.parse(fs.readFileSync(path.join(zhInitTarget, ".harness-capabilities.json"), "utf8"));
404
- assert(zhRegistry.locale === "zh-CN", "init should persist zh-CN locale");
405
- assert(fs.readFileSync(path.join(zhInitTarget, "AGENTS.md"), "utf8").includes("项目概况"), "zh-CN init should write Chinese AGENTS.md");
406
- assert(fs.existsSync(path.join(zhInitTarget, "docs/11-REFERENCE/external-source-intake-standard.md")), "zh-CN init should create external source intake standard");
407
- assert(
408
- fs.readFileSync(path.join(zhInitTarget, "docs/04-DEVELOPMENT/external-source-packs/README.md"), "utf8").includes("外部资料包索引"),
409
- "zh-CN init should create localized external source pack registry",
410
- );
411
- const zhReviewTemplate = fs.readFileSync(path.join(zhInitTarget, "docs/09-PLANNING/TASKS/_task-template/review.md"), "utf8");
412
- assert(zhReviewTemplate.includes("| ID | Severity | Finding | Evidence Checked | Required Action | Open | Disposition | Blocks Release | Follow-up |"), "zh-CN review template should preserve checker table headers");
413
- const zhInitCheck = expectJson(["status", "--json", zhInitTarget]);
414
- assert(zhInitCheck.checkState.status === "pass", "core+dashboard init should pass status check without safe-adoption");
415
- assert(zhInitCheck.checkState.warnings === 0, "core+dashboard init should not warn about safe-adoption orphan artifacts");
416
- const zhDashboardDir = path.join(tmpRoot, "zh-dashboard");
417
- expectPass(["dashboard", "--out-dir", zhDashboardDir, zhInitTarget]);
418
- const zhDashboardIndex = fs.readFileSync(path.join(zhDashboardDir, "index.html"), "utf8");
419
- const zhDashboardApp = fs.readFileSync(path.join(zhDashboardDir, "assets/app.js"), "utf8");
420
- const zhDashboardI18n = fs.readFileSync(path.join(zhDashboardDir, "assets/i18n.js"), "utf8");
421
- assert(zhDashboardIndex.includes("Harness 控制台"), "zh-CN dashboard should use localized index template");
422
- assert(zhDashboardApp.includes("projectCockpit"), "zh-CN dashboard should render through localized labels");
423
- assert(zhDashboardI18n.includes("控制台"), "zh-CN dashboard should include localized app labels");
424
- assert(zhDashboardApp.includes("data-language-toggle"), "dashboard should expose runtime language toggle");
425
- assert(zhDashboardIndex.includes("__HARNESS_LOCALE__"), "dashboard should bootstrap locale explicitly");
426
-
427
- const packageScriptTarget = path.join(tmpRoot, "package-script-target");
428
- fs.mkdirSync(packageScriptTarget);
429
- fs.writeFileSync(path.join(packageScriptTarget, "package.json"), JSON.stringify({ scripts: { test: "node --version" } }, null, 2));
430
- const packageScriptInit = expectJson(["init", "--locale", "en-US", "--capabilities", "core,dashboard", "--add-npm-scripts", packageScriptTarget]);
431
- assert(packageScriptInit.changes.some((change) => change.destination === "package.json" && change.action === "update-scripts"), "init --add-npm-scripts should report package.json script update");
432
- const packageScripts = JSON.parse(fs.readFileSync(path.join(packageScriptTarget, "package.json"), "utf8")).scripts;
433
- assert(packageScripts.test === "node --version", "init --add-npm-scripts should preserve existing scripts");
434
- assert(packageScripts["harness:dev"] === "coding-agent-harness dev .", "init --add-npm-scripts should add harness:dev");
435
- assert(packageScripts["harness:dashboard"] === "coding-agent-harness dashboard --out-dir tmp/harness-dashboard .", "init --add-npm-scripts should add static dashboard script");
436
- const noPackageScriptTarget = path.join(tmpRoot, "no-package-script-target");
437
- fs.mkdirSync(noPackageScriptTarget);
438
- const noPackageScripts = run(["init", "--dry-run", "--locale", "en-US", "--capabilities", "core", "--add-npm-scripts", noPackageScriptTarget]);
439
- assert(noPackageScripts.status !== 0, "init --add-npm-scripts should require an existing package.json");
440
-
441
- const enRunTarget = path.join(tmpRoot, "en-run-target");
442
- fs.mkdirSync(enRunTarget);
443
- const enRun = expectJson(["init", "--dry-run", "--locale", "en-US", "--capabilities", "core", enRunTarget]);
444
- assert(enRun.locale === "en-US", "init dry-run did not preserve en-US locale");
445
- assert(
446
- enRun.changes.some((change) => change.source === "templates/planning/task_plan.md"),
447
- "init en-US dry-run should use default English task_plan template",
448
- );
449
- const enInitTarget = path.join(tmpRoot, "en-init-target");
450
- fs.mkdirSync(enInitTarget);
451
- expectJson(["init", "--locale", "en-US", "--capabilities", "core,dashboard", enInitTarget]);
452
- assert(
453
- fs.readFileSync(path.join(enInitTarget, "docs/11-REFERENCE/external-source-intake-standard.md"), "utf8").includes("External Source Intake Standard"),
454
- "en-US init should create English external source intake standard",
455
- );
456
- assert(
457
- fs.readFileSync(path.join(enInitTarget, "docs/04-DEVELOPMENT/external-source-packs/README.md"), "utf8").includes("External Source Packs"),
458
- "en-US init should create English external source pack registry",
459
- );
460
- const enInitStatus = expectJson(["status", "--json", enInitTarget]);
461
- assert(enInitStatus.checkState.status === "pass", "en-US core+dashboard init should pass status check");
462
- assert(enInitStatus.checkState.warnings === 0, "en-US core+dashboard init should not warn about safe-adoption");
463
-
464
- const lifecycleTarget = path.join(tmpRoot, "lifecycle-target");
465
- fs.mkdirSync(lifecycleTarget);
466
- expectJson(["init", "--locale", "zh-CN", "--capabilities", "core,dashboard", lifecycleTarget]);
467
- const lifecycleDryRun = expectJson(["new-task", "phase-2-lifecycle", "--title", "阶段二任务生命周期", "--locale", "zh-CN", "--dry-run", lifecycleTarget]);
468
- assert(lifecycleDryRun.dryRun === true, "new-task dry-run should report dryRun true");
469
- assert(
470
- lifecycleDryRun.changes.some((change) => change.destination.endsWith("brief.md") && change.action === "would-create"),
471
- "new-task dry-run should plan brief.md",
472
- );
473
- assert(!fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle`)), "new-task dry-run should not mutate target");
474
- const lifecycleCreate = expectJson(["new-task", "phase-2-lifecycle", "--title", "阶段二任务生命周期", "--locale", "zh-CN", lifecycleTarget]);
475
- assert(lifecycleCreate.task?.shortId === `${todayLocal}-phase-2-lifecycle`, "new-task should report normalized short task id");
476
- assert(lifecycleCreate.task?.id === `TASKS/${todayLocal}-phase-2-lifecycle`, "new-task should report relative task id");
477
- for (const required of ["brief.md", "task_plan.md", "execution_strategy.md", "visual_map.md", "findings.md", "lesson_candidates.md", "progress.md", "review.md"]) {
478
- assert(
479
- fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle`, required)),
480
- `new-task should create ${required}`,
481
- );
482
- }
483
- assert(
484
- fs.readFileSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/brief.md`), "utf8").includes("阶段二任务生命周期"),
485
- "new-task should render the requested title into brief.md",
486
- );
487
- assert(
488
- fs.readFileSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/task_plan.md`), "utf8").includes("Task Contract: harness-task/v1"),
489
- "new-task should render the durable task contract marker",
490
- );
491
- const duplicateLifecycle = run(["new-task", `${todayLocal}-phase-2-lifecycle`, "--title", "duplicate", lifecycleTarget]);
492
- assert(duplicateLifecycle.status !== 0, "new-task should refuse to overwrite an existing task directory");
493
- const simpleLifecycle = expectJson(["new-task", "simple-lifecycle", "--budget", "simple", "--title", "简单任务", "--locale", "zh-CN", lifecycleTarget]);
494
- assert(simpleLifecycle.task?.budget === "simple", "new-task --budget simple should report simple budget");
495
- for (const required of ["brief.md", "task_plan.md", "visual_map.md", "progress.md"]) {
496
- assert(
497
- fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-simple-lifecycle`, required)),
498
- `simple task should create ${required}`,
499
- );
500
- }
501
- for (const omitted of ["execution_strategy.md", "findings.md", "review.md", "lesson_candidates.md"]) {
502
- assert(
503
- !fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-simple-lifecycle`, omitted)),
504
- `simple task should not create ${omitted}`,
505
- );
506
- }
507
- const simpleTaskPlan = fs.readFileSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-simple-lifecycle/task_plan.md`), "utf8");
508
- assert(/Selected budget\s*:\s*simple/i.test(simpleTaskPlan) || /选择预算\s*[::]\s*simple/i.test(simpleTaskPlan), "simple task should persist selected budget");
509
- const longRunningLifecycle = expectJson(["new-task", "long-running-lifecycle", "--long-running", "--title", "长程任务", "--locale", "zh-CN", lifecycleTarget]);
510
- assert(longRunningLifecycle.task?.longRunning === true, "new-task --long-running should report longRunning true");
511
- assert(
512
- fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-long-running-lifecycle/long-running-task-contract.md`)),
513
- "new-task --long-running should create long-running-task-contract.md",
514
- );
515
- const legacyPresetSessionDir = path.join(tmpRoot, "legacy-preset-session");
516
- fs.mkdirSync(path.join(legacyPresetSessionDir, "dashboard"), { recursive: true });
517
- fs.writeFileSync(path.join(legacyPresetSessionDir, "dashboard/index.html"), "<html>legacy migration dashboard</html>\n");
518
- fs.writeFileSync(path.join(legacyPresetSessionDir, "migrate-plan.json"), JSON.stringify({ operation: "migrate-plan", summary: { warnings: 2, legacyResiduals: 1 } }, null, 2));
519
- const legacyPresetSessionPath = path.join(legacyPresetSessionDir, "session.json");
520
- fs.writeFileSync(
521
- legacyPresetSessionPath,
522
- JSON.stringify(
523
- {
524
- operation: "migrate-run",
525
- schemaVersion: 1,
526
- generatedAt: "2026-05-22T00:00:00.000Z",
527
- result: "adopted-with-strict-deferred",
528
- target: lifecycleTarget,
529
- sessionDir: legacyPresetSessionDir,
530
- planOnly: false,
531
- dashboard: { dir: path.join(legacyPresetSessionDir, "dashboard"), indexPath: path.join(legacyPresetSessionDir, "dashboard/index.html"), kind: "html-folder" },
532
- plan: {
533
- mode: "legacy-compat",
534
- summary: {
535
- warnings: 2,
536
- visualMapActions: 0,
537
- legacyVisualOnly: 0,
538
- unknownClassification: 0,
539
- weakBrief: 0,
540
- missingCanonicalVisualMap: 0,
541
- taskActions: 0,
542
- reviewSchemaGaps: 0,
543
- legacyReferenceGaps: 0,
544
- legacyResiduals: 1,
545
- fullCutoverEligible: false,
546
- recommendedCapabilities: ["safe-adoption"],
547
- },
548
- },
549
- checks: {
550
- normal: { status: "warn", failures: 0, warnings: 2, failureDetails: [], warningDetails: ["legacy residual"] },
551
- strict: { status: "fail", failures: 1, warnings: 2, failureDetails: ["strict residual"], warningDetails: [] },
552
- },
553
- strictDeferred: {
554
- owner: "migration-owner",
555
- trigger: "strict-cutover",
556
- nextAction: "Assign real owner before full cutover.",
557
- reason: "Historical residual remains.",
558
- failureCount: 1,
559
- failures: ["strict residual"],
560
- },
561
- git: {
562
- before: { inGit: false, branch: "", entries: [], staged: [], dirty: false },
563
- after: { inGit: false, branch: "", entries: [], staged: [], dirty: false },
564
- },
565
- },
566
- null,
567
- 2,
568
- ),
569
- );
570
- const legacyPresetDryRun = expectJson(["new-task", "--budget", "complex", "--preset", "legacy-migration", "--from-session", legacyPresetSessionPath, "--dry-run"]);
571
- assert(legacyPresetDryRun.task?.preset === "legacy-migration", "new-task legacy-migration dry-run should report preset");
572
- assert(!fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-harness-v1-migration`)), "legacy-migration dry-run should not mutate target");
573
- const legacyPresetTask = expectJson(["new-task", "--budget", "complex", "--preset", "legacy-migration", "--from-session", legacyPresetSessionPath]);
574
- assert(legacyPresetTask.task?.id === `TASKS/${todayLocal}-harness-v1-migration`, "legacy-migration preset should derive a default task id");
575
- assert(legacyPresetTask.task?.kind === "project-migration", "legacy-migration preset should report project-migration kind");
576
- assert(legacyPresetTask.task?.preset === "legacy-migration", "legacy-migration preset should report preset");
577
- assert(legacyPresetTask.task?.evidenceBundle, "legacy-migration preset should report evidence bundle");
578
- const legacyPresetTaskDir = path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-harness-v1-migration`);
579
- const legacyPresetTaskPlan = fs.readFileSync(path.join(legacyPresetTaskDir, "task_plan.md"), "utf8");
580
- assert(legacyPresetTaskPlan.includes("Task Preset: legacy-migration"), "legacy-migration task plan should persist preset metadata");
581
- assert(legacyPresetTaskPlan.includes("Migration Achieved Level: migration-deferred"), "strict-deferred session should start as migration-deferred");
582
- for (const required of ["session.json", "migrate-plan.json", "normal-check.json", "strict-check.json", "migrate-verify.json", "dashboard.hash.txt", "target-git-status.txt", "target-commit.txt", "harness-version.txt", "generated-at.txt"]) {
583
- assert(
584
- fs.existsSync(path.join(lifecycleTarget, legacyPresetTask.task.evidenceBundle, required)),
585
- `legacy-migration preset should copy evidence file ${required}`,
586
- );
587
- }
588
- const legacyPresetStatus = expectJson(["status", "--json", lifecycleTarget]);
589
- const legacyPresetStatusTask = legacyPresetStatus.tasks.find((task) => task.id === `TASKS/${todayLocal}-harness-v1-migration`);
590
- assert(legacyPresetStatusTask?.taskKind === "project-migration", "status should expose taskKind");
591
- assert(legacyPresetStatusTask?.taskPreset === "legacy-migration", "status should expose taskPreset");
592
- assert(legacyPresetStatusTask?.migrationSnapshot?.strictDeferred === true, "status should expose migration snapshot strictDeferred");
593
- const legacyPresetDashboardDir = path.join(tmpRoot, "legacy-preset-dashboard");
594
- expectPass(["dashboard", "--out-dir", legacyPresetDashboardDir, lifecycleTarget]);
595
- const legacyPresetDashboardData = fs.readFileSync(path.join(legacyPresetDashboardDir, "assets/dashboard-data.js"), "utf8");
596
- assert(legacyPresetDashboardData.includes("migrationSnapshot"), "dashboard bundle should expose migrationSnapshot");
597
- fs.writeFileSync(
598
- path.join(legacyPresetTaskDir, "task_plan.md"),
599
- legacyPresetTaskPlan.replace("Migration Achieved Level: migration-deferred", "Migration Achieved Level: migration-full-cutover"),
600
- );
601
- const falseFullCutoverCheck = run(["check", "--profile", "target-project", lifecycleTarget]);
602
- assert(falseFullCutoverCheck.status !== 0, "check should reject migration-full-cutover when evidence still has residuals");
603
- assert(falseFullCutoverCheck.stderr.includes("migration-full-cutover"), "full-cutover preset failure should explain achieved level");
604
- fs.writeFileSync(path.join(legacyPresetTaskDir, "task_plan.md"), legacyPresetTaskPlan);
605
- const promotableCandidatePath = path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-long-running-lifecycle/lesson_candidates.md`);
606
- fs.writeFileSync(
607
- promotableCandidatePath,
608
- fs.readFileSync(promotableCandidatePath, "utf8")
609
- .replace("| Task-level status | pending-review |", "| Task-level status | needs-promotion |")
610
- .replace("| Review decision | pending-human-review |", "| Review decision | accepted-for-promotion |")
611
- .replace("| Promotion state | not-promoted |", "| Promotion state | queued |")
612
- .replace("| Closeout token | pending |", "| Closeout token | queued-promotion:LC-20260521-001 |")
613
- .replace(
614
- "| --- | --- | --- | --- | --- | --- |",
615
- "| --- | --- | --- | --- | --- | --- |\n| LC-20260521-001 | needs-promotion | Commit contract must be explicit | Agents forget proactive commits when contracts are implicit | accepted-for-promotion | references/execution-workflow-standard.md |",
616
- ),
617
- );
618
- const promoteDryRun = expectJson(["lesson-promote", "long-running-lifecycle", "LC-20260521-001", "--dry-run", lifecycleTarget]);
619
- assert(promoteDryRun.dryRun === true && promoteDryRun.lessonId === "L-2026-05-21-001", "lesson-promote --dry-run should derive the lesson id");
620
- const promoteRun = expectJson(["lesson-promote", "long-running-lifecycle", "LC-20260521-001", lifecycleTarget]);
621
- assert(promoteRun.lessonId === "L-2026-05-21-001", "lesson-promote should return the created lesson id");
622
- assert(
623
- fs.existsSync(path.join(lifecycleTarget, "docs/01-GOVERNANCE/lessons/L-2026-05-21-001-commit-contract-must-be-explicit.md")),
624
- "lesson-promote should create a detail document",
625
- );
626
- assert(
627
- fs.readFileSync(path.join(lifecycleTarget, "docs/01-GOVERNANCE/Lessons-SSoT.md"), "utf8").includes("L-2026-05-21-001"),
628
- "lesson-promote should append Lessons SSoT",
629
- );
630
- assert(fs.readFileSync(promotableCandidatePath, "utf8").includes("| LC-20260521-001 | promoted |"), "lesson-promote should mark the candidate row promoted");
631
- const promoteAgain = expectJson(["lesson-promote", "long-running-lifecycle", "LC-20260521-001", lifecycleTarget]);
632
- assert(promoteAgain.changes.length === 0, "lesson-promote should be idempotent after promotion");
633
- expectPass(["check", "--profile", "target-project", lifecycleTarget]);
634
- expectJson(["task-start", "simple-lifecycle", "--message", "开始简单任务", lifecycleTarget]);
635
- const simpleComplete = expectJson(["task-complete", "simple-lifecycle", "--message", "简单任务完成", lifecycleTarget]);
636
- assert(simpleComplete.task?.state === "done", "simple task should be able to complete without review");
637
- const earlyReview = run(["task-review", "review-too-early", lifecycleTarget]);
638
- assert(earlyReview.status !== 0, "task-review should reject unknown tasks");
639
- const tooEarlyTask = expectJson(["new-task", "review-too-early", "--title", "Too early review", "--locale", "zh-CN", lifecycleTarget]);
640
- assert(tooEarlyTask.task?.id === `TASKS/${todayLocal}-review-too-early`, "new-task should create review-too-early fixture");
641
- const tooEarlyReview = run(["task-review", "review-too-early", "--message", "too early", lifecycleTarget]);
642
- assert(tooEarlyReview.status !== 0, "task-review should reject tasks that are not in_progress");
643
- assert(tooEarlyReview.stderr.includes("in_progress"), "task-review invalid transition should explain required state");
644
- expectJson(["task-start", "phase-2-lifecycle", "--message", "开始实现生命周期切片", lifecycleTarget]);
645
- expectJson(["task-log", "phase-2-lifecycle", "--message", "补齐 CLI 与模板", "--evidence", "command:TARGET:npm-test:passed", lifecycleTarget]);
646
- const noPhaseProgressReview = run(["task-review", "phase-2-lifecycle", "--message", "阶段表尚未更新", lifecycleTarget]);
647
- assert(noPhaseProgressReview.status !== 0, "task-review should reject standard tasks whose visual map has no recorded phase progress");
648
- assert(
649
- noPhaseProgressReview.stderr.includes("task-phase"),
650
- "task-review phase-progress failure should tell the agent to run task-phase",
651
- );
652
- const lifecycleBlocked = expectJson(["task-block", "phase-2-lifecycle", "--message", "等待旧项目迁移验证", lifecycleTarget]);
653
- assert(lifecycleBlocked.task?.state === "blocked", "task-block should report blocked state");
654
- const lifecyclePhase = expectJson(["task-phase", "phase-2-lifecycle", "PH-01", "--state", "done", "--completion", "100", "--evidence", "present", lifecycleTarget]);
655
- assert(lifecyclePhase.task?.phases?.some((phase) => phase.id === "PH-01" && phase.state === "done" && phase.completion === 100), "task-phase should update visual map row");
656
- assert(
657
- fs.readFileSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/visual_map.md`), "utf8").includes("Visual Map Contract: v1.0"),
658
- "new-task should render canonical visual map contract",
659
- );
660
- expectJson(["task-phase", "phase-2-lifecycle", "PH-01", "--state", "done", "--completion", "100", "--evidence", "present", lifecycleTarget]);
661
- const missingPhase = run(["task-phase", "phase-2-lifecycle", "NO_SUCH_PHASE", "--state", "done", lifecycleTarget]);
662
- assert(missingPhase.status !== 0, "task-phase should fail for unknown phase id");
663
- assert(missingPhase.stderr.includes("Phase not found"), "task-phase unknown phase should explain missing phase");
664
- const directComplete = run(["task-complete", "phase-2-lifecycle", "--message", "跳过审查完成", lifecycleTarget]);
665
- assert(directComplete.status !== 0, "standard task-complete should require review state");
666
- assert(directComplete.stderr.includes("task-review"), "standard task-complete failure should tell the user to run task-review first");
667
- expectJson(["task-start", "phase-2-lifecycle", "--message", "恢复执行生命周期切片", lifecycleTarget]);
668
- const lifecycleReview = expectJson(["task-review", "phase-2-lifecycle", "--message", "进入执行审查", lifecycleTarget]);
669
- assert(lifecycleReview.task?.state === "review", "task-review should report review state");
670
- const lifecycleReviewPath = path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/review.md`);
671
- fs.writeFileSync(
672
- lifecycleReviewPath,
673
- fs.readFileSync(lifecycleReviewPath, "utf8").replace(
674
- "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
675
- `| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| RR-001 | P1 | Human review is still pending | TARGET:docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/progress.md | confirm in dashboard | yes | open | yes | dashboard |`,
676
- ),
677
- );
678
- const blockedComplete = run(["task-complete", "phase-2-lifecycle", "--message", "带阻塞审查项完成", lifecycleTarget]);
679
- assert(blockedComplete.status !== 0, "task-complete should reject open blocking review findings");
680
- assert(blockedComplete.stderr.includes("Open blocking review findings"), "task-complete blocked review failure should explain open findings");
681
- const blockedConfirm = run(["review-confirm", `TASKS/${todayLocal}-phase-2-lifecycle`, "--reviewer", "Human Reviewer", "--confirm", `${todayLocal}-phase-2-lifecycle`, lifecycleTarget]);
682
- assert(blockedConfirm.status !== 0, "review-confirm should reject tasks with open blocking review findings");
683
- assert(blockedConfirm.stderr.includes("Open blocking review findings"), "review-confirm blocked failure should explain open findings");
684
- fs.writeFileSync(
685
- lifecycleReviewPath,
686
- fs.readFileSync(lifecycleReviewPath, "utf8").replace(`| RR-001 | P1 | Human review is still pending | TARGET:docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/progress.md | confirm in dashboard | yes | open | yes | dashboard |`, `| RR-001 | P1 | Human review is closed | TARGET:docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/progress.md | confirmed in dashboard | no | closed | no | none |`),
687
- );
688
- const unconfirmedComplete = run(["task-complete", "phase-2-lifecycle", "--message", "未确认审查完成", lifecycleTarget]);
689
- assert(unconfirmedComplete.status !== 0, "task-complete should require human review confirmation");
690
- assert(unconfirmedComplete.stderr.includes("review-confirm"), "unconfirmed review failure should tell the user to run review-confirm");
691
- const missingWalkthroughConfirm = run(["review-confirm", `TASKS/${todayLocal}-phase-2-lifecycle`, "--reviewer", "Human Reviewer", "--message", "walkthrough reviewed", "--confirm", `${todayLocal}-phase-2-lifecycle`, lifecycleTarget]);
692
- assert(missingWalkthroughConfirm.status !== 0, "review-confirm should require a walkthrough before human confirmation");
693
- assert(missingWalkthroughConfirm.stderr.includes("walkthrough"), "missing walkthrough confirmation failure should explain the walkthrough requirement");
694
- const lifecycleWalkthrough = path.join(lifecycleTarget, `docs/10-WALKTHROUGH/${todayLocal}-phase-2-lifecycle-walkthrough.md`);
695
- fs.writeFileSync(
696
- lifecycleWalkthrough,
697
- "# Walkthrough: Phase 2 lifecycle\n\n## Summary\n\nHuman-readable walkthrough for review before completion.\n",
698
- );
699
- fs.appendFileSync(
700
- path.join(lifecycleTarget, "docs/10-WALKTHROUGH/Closeout-SSoT.md"),
701
- `\n| CL-PHASE-2-LIFECYCLE | 2026-05-21 | Phase 2 lifecycle | \`docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/task_plan.md\` | \`docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/review.md\` | \`docs/10-WALKTHROUGH/${todayLocal}-phase-2-lifecycle-walkthrough.md\` | pending human review | none | checked-none | pending |\n`,
702
- );
703
- acceptNoLessonCandidate(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle`));
704
- const preCompleteStatus = expectJson(["status", "--json", lifecycleTarget]);
705
- const preCompleteTask = preCompleteStatus.tasks.find((task) => task.id === `TASKS/${todayLocal}-phase-2-lifecycle`);
706
- assert(preCompleteTask?.walkthroughPath?.endsWith(`docs/10-WALKTHROUGH/${todayLocal}-phase-2-lifecycle-walkthrough.md`), "status should expose walkthrough before human review confirmation");
707
- const preCompleteConfirm = expectJson(["review-confirm", `TASKS/${todayLocal}-phase-2-lifecycle`, "--reviewer", "Human Reviewer", "--message", "walkthrough reviewed", "--confirm", `${todayLocal}-phase-2-lifecycle`, lifecycleTarget]);
708
- assert(preCompleteConfirm.task?.reviewStatus === "confirmed", "review-confirm should confirm review before task-complete");
709
- const lifecycleComplete = expectJson(["task-complete", "phase-2-lifecycle", "--message", "生命周期闭环完成", lifecycleTarget]);
710
- assert(lifecycleComplete.task?.state === "done", "task-complete should report done state");
711
- const lifecycleTasks = expectJson(["task-list", "--json", lifecycleTarget]);
712
- assert(lifecycleTasks.tasks.some((task) => task.id === `TASKS/${todayLocal}-phase-2-lifecycle` && task.state === "done"), "task-list should include completed task");
713
- assert(lifecycleTasks.tasks.some((task) => task.id === `TASKS/${todayLocal}-simple-lifecycle` && task.budget === "simple"), "task-list should expose parsed task budget");
714
- const doneLifecycleTasks = expectJson(["task-list", "--json", "--state", "done", lifecycleTarget]);
715
- assert(doneLifecycleTasks.tasks.every((task) => task.state === "done"), "task-list --state should filter states");
716
- const lifecycleStatus = expectJson(["status", "--json", lifecycleTarget]);
717
- assert(lifecycleStatus.schemaVersion === 2, "status should expose dashboard schemaVersion 2");
718
- const lifecycleTask = lifecycleStatus.tasks.find((task) => task.id === `TASKS/${todayLocal}-phase-2-lifecycle`);
719
- assert(lifecycleTask?.briefSource === "standalone", "status should expose standalone task brief");
720
- assert(lifecycleTask?.briefPath?.endsWith("/brief.md"), "status should expose the task brief path");
721
- assert(lifecycleTask?.classificationBucket === "current", "new v1 tasks should not be classified as legacy");
722
- assert(lifecycleStatus.summary?.briefCoverage?.missing === 0, "status should expose explicit brief coverage summary");
723
- assert(lifecycleTask?.state === "done", "status should read lifecycle task state from progress.md");
724
- assert(lifecycleTask?.lifecycleState === "closing", "done task with pending closeout should remain in closing lifecycle state");
725
- assert(lifecycleTask?.evidence?.some((item) => item.summary.includes("passed")), "status should collect task-log evidence");
726
- const confirmedStatus = expectJson(["status", "--json", lifecycleTarget]);
727
- const confirmedTask = confirmedStatus.tasks.find((task) => task.id === `TASKS/${todayLocal}-phase-2-lifecycle`);
728
- assert(confirmedTask?.reviewStatus === "confirmed", "status should expose confirmed review status");
729
- assert(confirmedTask?.closeoutStatus === "pending", "status should keep pending closeout separate from review confirmation");
730
- assert(fs.readFileSync(lifecycleReviewPath, "utf8").includes("Human Review Confirmation"), "review-confirm should write a human review confirmation block");
731
- assert(fs.readFileSync(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/progress.md`), "utf8").includes("review-confirm"), "review-confirm should append a progress log entry");
732
- const moduleLifecycle = expectJson(["new-task", "module-lifecycle", "--module", "auth", "--budget", "complex", "--title", "模块生命周期", "--locale", "zh-CN", lifecycleTarget]);
733
- assert(moduleLifecycle.task?.id === `MODULES/auth/${todayLocal}-module-lifecycle`, "new-task --module should create a module task id");
734
- assert(fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle/references/INDEX.md`)), "complex module task should create references index");
735
- assert(fs.existsSync(path.join(lifecycleTarget, `docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle/artifacts/INDEX.md`)), "complex module task should create artifacts index");
736
- assert(fs.existsSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/brief.md")), "new-task --module should create a module brief when missing");
737
- assert(fs.existsSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/module_plan.md")), "new-task --module should create a module plan when missing");
738
- assert(fs.existsSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/execution_strategy.md")), "new-task --module should create module-level execution strategy when missing");
739
- assert(fs.existsSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/visual_map.md")), "new-task --module should create module-level visual map when missing");
740
- assert(fs.existsSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/session_prompt.md")), "new-task --module should create a module session prompt when missing");
741
- fs.writeFileSync(
742
- path.join(lifecycleTarget, "docs/09-PLANNING/Module-Registry.md"),
743
- "# Module Registry\n\n## Active Modules\n\n| ID | Module | Path Scope | Owner | Status | Branch or Worktree | Task Plan | Shared Files | Depends On | Handoff Evidence | Residual | Updated |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| M-AUTH | Auth | src/auth/** | coordinator | reserved | n/a | docs/09-PLANNING/MODULES/auth/module_plan.md | none | none | pending | none | 2026-05-19 |\n",
744
- );
745
- fs.writeFileSync(
746
- path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/module_plan.md"),
747
- `# Auth Module Plan\n\n## Steps\n\n| Step ID | Name | Status | Task Plan | Depends On |\n| --- | --- | --- | --- | --- |\n| AUTH-01 | Setup | planned | docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle/task_plan.md | none |\n`,
748
- );
749
- const moduleStep = expectJson(["module-step", "auth", "AUTH-01", "--state", "done", lifecycleTarget]);
750
- assert(moduleStep.moduleKey === "auth" && moduleStep.stepId === "AUTH-01", "module-step should report updated module step");
751
- assert(fs.readFileSync(path.join(lifecycleTarget, "docs/09-PLANNING/MODULES/auth/module_plan.md"), "utf8").includes("| AUTH-01 | Setup | done |"), "module-step should update module_plan status");
752
- assert(fs.readFileSync(path.join(lifecycleTarget, "docs/09-PLANNING/Module-Registry.md"), "utf8").includes("| M-AUTH | Auth | src/auth/** | coordinator | merged |"), "module-step should update module registry status when done");
753
- expectJson(["module-step", "auth", "AUTH-01", "--state", "done", lifecycleTarget]);
754
- const missingModuleStep = run(["module-step", "auth", "NO_SUCH_STEP", "--state", "done", lifecycleTarget]);
755
- assert(missingModuleStep.status !== 0, "module-step should fail for unknown step id");
756
- assert(missingModuleStep.stderr.includes("Module step not found"), "module-step unknown step should explain missing step");
757
- expectJson(["task-start", `MODULES/auth/${todayLocal}-module-lifecycle`, "--message", "开始模块任务审查夹具", lifecycleTarget]);
758
- expectJson(["task-phase", `MODULES/auth/${todayLocal}-module-lifecycle`, "PH-01", "--state", "done", "--completion", "100", "--evidence", "present", lifecycleTarget]);
759
- expectJson(["task-review", `MODULES/auth/${todayLocal}-module-lifecycle`, "--message", "模块任务进入审查", lifecycleTarget]);
760
- const moduleWalkthrough = path.join(lifecycleTarget, `docs/10-WALKTHROUGH/${todayLocal}-module-lifecycle-walkthrough.md`);
761
- fs.writeFileSync(
762
- moduleWalkthrough,
763
- "# Walkthrough: Module lifecycle\n\n## Summary\n\nHuman-readable module walkthrough for review confirmation.\n",
764
- );
765
- fs.appendFileSync(
766
- path.join(lifecycleTarget, "docs/10-WALKTHROUGH/Closeout-SSoT.md"),
767
- `\n| CL-MODULE-LIFECYCLE | 2026-05-21 | Module lifecycle | \`docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle/task_plan.md\` | \`docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle/review.md\` | \`docs/10-WALKTHROUGH/${todayLocal}-module-lifecycle-walkthrough.md\` | pending human review | none | checked-none | pending |\n`,
768
- );
769
- acceptNoLessonCandidate(path.join(lifecycleTarget, `docs/09-PLANNING/MODULES/auth/${todayLocal}-module-lifecycle`));
770
- const moduleConfirm = expectJson(["review-confirm", `MODULES/auth/${todayLocal}-module-lifecycle`, "--reviewer", "Human Reviewer", "--confirm", `${todayLocal}-module-lifecycle`, lifecycleTarget]);
771
- assert(moduleConfirm.task?.id === `MODULES/auth/${todayLocal}-module-lifecycle`, "review-confirm should accept full module task ids");
772
- const workbenchReviewTask = expectJson(["new-task", "workbench-review", "--title", "Workbench review gate", "--locale", "zh-CN", lifecycleTarget]);
773
- assert(workbenchReviewTask.task?.id === `TASKS/${todayLocal}-workbench-review`, "new-task should create workbench review gate fixture");
774
- const workbenchReviewProgress = path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-workbench-review/progress.md`);
775
- const workbenchClosedReviewTask = expectJson(["new-task", "workbench-closed-review", "--title", "Closed review debt", "--locale", "zh-CN", lifecycleTarget]);
776
- assert(workbenchClosedReviewTask.task?.id === `TASKS/${todayLocal}-workbench-closed-review`, "new-task should create closed review debt fixture");
777
- const workbenchClosedReviewProgress = path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-workbench-closed-review/progress.md`);
778
- fs.writeFileSync(
779
- workbenchClosedReviewProgress,
780
- fs.readFileSync(workbenchClosedReviewProgress, "utf8").replace(/^## 状态:.*$/m, "## 状态:done"),
781
- );
782
- const closedReviewWalkthrough = path.join(lifecycleTarget, "docs/10-WALKTHROUGH/workbench-closed-walkthrough.md");
783
- fs.writeFileSync(
784
- closedReviewWalkthrough,
785
- "# Walkthrough: Closed review debt\n\n## Summary\n\nHuman-readable closeout walkthrough for dashboard review.\n",
786
- );
787
- fs.appendFileSync(
788
- path.join(lifecycleTarget, "docs/10-WALKTHROUGH/Closeout-SSoT.md"),
789
- `\n| CL-WORKBENCH-CLOSED | 2026-05-21 | Closed review debt | \`docs/09-PLANNING/TASKS/${todayLocal}-workbench-closed-review/task_plan.md\` | \`docs/09-PLANNING/TASKS/${todayLocal}-workbench-closed-review/review.md\` | \`docs/10-WALKTHROUGH/workbench-closed-walkthrough.md\` | test evidence | none | checked-none | closed |\n`,
790
- );
791
- const closedReviewStatus = expectJson(["status", "--json", lifecycleTarget]);
792
- const closedReviewTask = closedReviewStatus.tasks.find((task) => task.id === `TASKS/${todayLocal}-workbench-closed-review`);
793
- assert(closedReviewTask?.walkthroughPath?.endsWith("docs/10-WALKTHROUGH/workbench-closed-walkthrough.md"), "status should expose task walkthrough path from Closeout SSoT");
794
- const workbenchDir = path.join(tmpRoot, "review-workbench");
795
- const workbench = spawn(node, [cli, "dashboard", "--workbench", "--out-dir", workbenchDir, "--host", "127.0.0.1", "--port", "0", lifecycleTarget], {
796
- cwd: repoRoot,
797
- stdio: ["ignore", "pipe", "pipe"],
798
- });
799
- const runtime = await waitForWorkbench(workbench);
800
- try {
801
- const runtimeResponse = await fetch(new URL("api/runtime", runtime.url));
802
- assert(runtimeResponse.status === 200, "workbench should expose runtime metadata");
803
- const runtimePayload = await runtimeResponse.json();
804
- assert(runtimePayload.mode === "workbench" && runtimePayload.csrfToken === runtime.csrf, "workbench runtime should expose mode and csrf token");
805
- const dashboardData = fs.readFileSync(path.join(workbenchDir, "assets/dashboard-data.js"), "utf8");
806
- assert(dashboardData.includes("Walkthrough: Closed review debt"), "dashboard data should include closeout walkthrough documents");
807
- const badOrigin = await fetch(new URL("api/tasks/review-complete", runtime.url), {
808
- method: "POST",
809
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: "http://127.0.0.1:9" },
810
- body: JSON.stringify({ taskId: `MODULES/auth/${todayLocal}-module-lifecycle`, confirmText: `${todayLocal}-module-lifecycle`, reviewer: "Human Reviewer" }),
811
- });
812
- assert(badOrigin.status === 403, "workbench should reject mismatched origins");
813
- const badTask = await fetch(new URL("api/tasks/review-complete", runtime.url), {
814
- method: "POST",
815
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: runtime.url.replace(/\/$/, "") },
816
- body: JSON.stringify({ taskId: "../bad", confirmText: "bad", reviewer: "Human Reviewer" }),
817
- });
818
- assert(badTask.status === 404, "workbench should reject unknown task ids");
819
- const plannedReview = await fetch(new URL("api/tasks/review-complete", runtime.url), {
820
- method: "POST",
821
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: runtime.url.replace(/\/$/, "") },
822
- body: JSON.stringify({ taskId: `TASKS/${todayLocal}-workbench-review`, confirmText: `${todayLocal}-workbench-review`, reviewer: "Human Reviewer" }),
823
- });
824
- assert(plannedReview.status === 409, "workbench review completion should reject tasks that are not in review state");
825
- fs.writeFileSync(
826
- workbenchReviewProgress,
827
- fs.readFileSync(workbenchReviewProgress, "utf8").replace(/^## 状态:.*$/m, "## 状态:review"),
828
- );
829
- const missingWalkthroughResponse = await fetch(new URL("api/tasks/review-complete", runtime.url), {
830
- method: "POST",
831
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: runtime.url.replace(/\/$/, "") },
832
- body: JSON.stringify({ taskId: `TASKS/${todayLocal}-workbench-review`, confirmText: `${todayLocal}-workbench-review`, reviewer: "Human Reviewer", message: "confirmed without walkthrough" }),
833
- });
834
- const missingWalkthroughText = await missingWalkthroughResponse.text();
835
- assert(missingWalkthroughResponse.status === 400, `workbench review completion should require walkthrough, got ${missingWalkthroughResponse.status}: ${missingWalkthroughText}`);
836
- assert(missingWalkthroughText.includes("walkthrough"), "workbench missing walkthrough rejection should explain walkthrough requirement");
837
- const workbenchReviewWalkthrough = path.join(lifecycleTarget, "docs/10-WALKTHROUGH/workbench-review-walkthrough.md");
838
- fs.writeFileSync(
839
- workbenchReviewWalkthrough,
840
- "# Walkthrough: Workbench review gate\n\n## Summary\n\nHuman-readable walkthrough for dashboard review confirmation.\n",
841
- );
842
- fs.appendFileSync(
843
- path.join(lifecycleTarget, "docs/10-WALKTHROUGH/Closeout-SSoT.md"),
844
- `\n| CL-WORKBENCH-REVIEW | 2026-05-21 | Workbench review gate | \`docs/09-PLANNING/TASKS/${todayLocal}-workbench-review/task_plan.md\` | \`docs/09-PLANNING/TASKS/${todayLocal}-workbench-review/review.md\` | \`docs/10-WALKTHROUGH/workbench-review-walkthrough.md\` | pending human review | none | checked-none | pending |\n`,
845
- );
846
- acceptNoLessonCandidate(path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-workbench-review`));
847
- const okResponse = await fetch(new URL("api/tasks/review-complete", runtime.url), {
848
- method: "POST",
849
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: runtime.url.replace(/\/$/, "") },
850
- body: JSON.stringify({ taskId: `TASKS/${todayLocal}-workbench-review`, confirmText: `${todayLocal}-workbench-review`, reviewer: "Human Reviewer", message: "confirmed from workbench" }),
851
- });
852
- const okText = await okResponse.text();
853
- assert(okResponse.status === 200, `workbench review completion should pass, got ${okResponse.status}: ${okText}`);
854
- const okPayload = JSON.parse(okText);
855
- assert(okPayload.task?.reviewStatus === "confirmed", "workbench review completion should return confirmed task status");
856
- const closedReviewResponse = await fetch(new URL("api/tasks/review-complete", runtime.url), {
857
- method: "POST",
858
- headers: { "content-type": "application/json", "x-harness-csrf": runtime.csrf, origin: runtime.url.replace(/\/$/, "") },
859
- body: JSON.stringify({ taskId: `TASKS/${todayLocal}-workbench-closed-review`, confirmText: `${todayLocal}-workbench-closed-review`, reviewer: "Human Reviewer", message: "closed debt confirmed from workbench" }),
860
- });
861
- const closedReviewText = await closedReviewResponse.text();
862
- assert(closedReviewResponse.status === 409, `workbench review completion should reject closed closeout tasks, got ${closedReviewResponse.status}: ${closedReviewText}`);
863
- assert(closedReviewText.includes("review"), "workbench closed closeout rejection should explain review stage boundary");
864
- } finally {
865
- workbench.kill("SIGTERM");
866
- }
867
- const devDir = path.join(tmpRoot, "dev-workbench");
868
- const dev = spawn(node, [cli, "dev", "--no-open", "--out-dir", devDir, "--host", "127.0.0.1", "--port", "0", lifecycleTarget], {
869
- cwd: repoRoot,
870
- stdio: ["ignore", "pipe", "pipe"],
871
- });
872
- const devRuntime = await waitForWorkbench(dev);
873
- try {
874
- assert(devRuntime.stdout.includes("outDir="), "harness dev should print the generated outDir");
875
- const initialRuntime = await (await fetch(new URL("api/runtime", devRuntime.url))).json();
876
- assert(initialRuntime.mode === "workbench" && initialRuntime.autoRefresh === true, "harness dev should start auto-refreshing workbench runtime");
877
- assert(fs.readFileSync(path.join(devDir, "index.html"), "utf8").includes("__HARNESS_WORKBENCH__ = true"), "harness dev should enable workbench runtime in generated index");
878
- const marker = `dev-refresh-${Date.now()}`;
879
- fs.appendFileSync(
880
- path.join(lifecycleTarget, `docs/09-PLANNING/TASKS/${todayLocal}-phase-2-lifecycle/progress.md`),
881
- `\n\n## Dev Refresh Marker\n\n${marker}\n`,
882
- );
883
- await waitForCondition(async () => {
884
- const runtimePayload = await (await fetch(new URL("api/runtime", devRuntime.url))).json();
885
- if (runtimePayload.snapshotVersion === initialRuntime.snapshotVersion) return false;
886
- const dashboardData = fs.readFileSync(path.join(devDir, "assets/dashboard-data.js"), "utf8");
887
- return dashboardData.includes(marker) ? runtimePayload : false;
888
- }, "harness dev should regenerate dashboard data after docs changes");
889
- } finally {
890
- dev.kill("SIGTERM");
891
- }
892
-
893
- const zhRegistryTarget = path.join(tmpRoot, "zh-module-registry-target");
894
- fs.mkdirSync(zhRegistryTarget);
895
- expectJson(["init", "--locale", "zh-CN", "--capabilities", "core,module-parallel", zhRegistryTarget]);
896
- assert(fs.existsSync(path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/Session-Prompt-Pack.md")), "module-parallel init should create a session prompt pack");
897
- assert(fs.existsSync(path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/_module-template/module_plan.md")), "module-parallel init should create a module plan template");
898
- assert(fs.existsSync(path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/_module-template/session_prompt.md")), "module-parallel init should create a module session prompt template");
899
- assert(fs.existsSync(path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/_task-template/review.md")), "module-parallel init should create complete module task templates");
900
- expectJson(["new-task", "zh-task", "--module", "example", "--title", "中文模块任务", "--locale", "zh-CN", zhRegistryTarget]);
901
- fs.mkdirSync(path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/example"), { recursive: true });
902
- fs.writeFileSync(
903
- path.join(zhRegistryTarget, "docs/09-PLANNING/MODULES/example/module_plan.md"),
904
- `# 示例模块计划\n\n## 步骤\n\n| 步骤 ID | 名称 | 状态 | 任务计划 | 依赖 |\n| --- | --- | --- | --- | --- |\n| EXM-01 | 启动 | planned | docs/09-PLANNING/MODULES/example/${todayLocal}-zh-task/task_plan.md | none |\n`,
905
- );
906
- expectJson(["module-step", "example", "EXM-01", "--state", "done", zhRegistryTarget]);
907
- const zhRegistryContent = fs.readFileSync(path.join(zhRegistryTarget, "docs/09-PLANNING/Module-Registry.md"), "utf8");
908
- assert(zhRegistryContent.includes("| example | 示例模块 | EXM | `codex/example` | EXM-01 | completed |"), "module-step should update zh-CN module registry status/current step");
909
- const zhGraphDir = path.join(tmpRoot, "zh-module-dashboard");
910
- expectPass(["dashboard", "--out-dir", zhGraphDir, zhRegistryTarget]);
911
- const zhGraph = JSON.parse(fs.readFileSync(path.join(zhGraphDir, "data/graph.json"), "utf8"));
912
- assert(zhGraph.nodes.some((node) => node.type === "module" && node.id === "module:example" && node.state === "completed" && node.currentStep === "EXM-01"), "zh-CN module registry should populate dashboard graph");
913
- assert(zhGraph.nodes.some((node) => node.type === "step" && node.id === "step:EXM-01" && node.state === "done"), "zh-CN module plan should populate step graph");
914
- const moduleFiltered = expectJson(["task-list", "--json", "--module", "auth", lifecycleTarget]);
915
- assert(moduleFiltered.tasks.length === 1 && moduleFiltered.tasks[0].id === `MODULES/auth/${todayLocal}-module-lifecycle`, "task-list --module should filter module tasks");
916
- expectJson(["new-task", "module-lifecycle", "--title", "同名根任务", "--locale", "zh-CN", lifecycleTarget]);
917
- const ambiguousTask = run(["task-start", "module-lifecycle", "--message", "ambiguous", lifecycleTarget]);
918
- assert(ambiguousTask.status !== 0, "ambiguous task short name should fail");
919
- assert(ambiguousTask.stderr.includes("Ambiguous task reference"), "ambiguous task error should explain ambiguity");
920
- assert(ambiguousTask.stderr.includes(`TASKS/${todayLocal}-module-lifecycle`) && ambiguousTask.stderr.includes(`MODULES/auth/${todayLocal}-module-lifecycle`), "ambiguous task error should list candidate task paths");
921
-
922
- const capTarget = path.join(tmpRoot, "cap-target");
923
- fs.mkdirSync(capTarget);
924
- expectPass(["add-capability", "dashboard", capTarget]);
925
- const registry = JSON.parse(fs.readFileSync(path.join(capTarget, ".harness-capabilities.json"), "utf8"));
926
- assert(registry.locale === "en-US", "add-capability registry missing default locale");
927
- assert(registry.capabilities.some((capability) => capability.name === "dashboard"), "add-capability missing dashboard");
928
- assert(registry.capabilities.some((capability) => capability.name === "core"), "add-capability missing dependency core");
929
- const addReport = expectJson(["add-capability", "dashboard", "--dry-run", capTarget]);
930
- assert(addReport.report?.capabilities?.some((capability) => capability.name === "dashboard"), "add-capability output should include install report");
931
-
932
- const userInstallHome = path.join(tmpRoot, "user-install-home");
933
- const userInstallDryRun = expectJson(["install-user", "--agent", "codex", "--home", userInstallHome, "--dry-run"]);
934
- assert(userInstallDryRun.operation === "install-user", "install-user dry-run should report operation");
935
- assert(userInstallDryRun.targets?.[0]?.agent === "codex", "install-user dry-run should target codex");
936
- assert(userInstallDryRun.targets?.[0]?.changes?.some((change) => change.destination.endsWith("SKILL.md") && change.action === "would-create"), "install-user dry-run should plan SKILL.md");
937
- assert(!fs.existsSync(path.join(userInstallHome, ".codex")), "install-user dry-run should not mutate home");
938
- const userInstall = expectJson(["install-user", "--agent", "codex", "--home", userInstallHome, "--yes"]);
939
- const codexSkillRoot = path.join(userInstallHome, ".codex/skills/coding-agent-harness");
940
- assert(userInstall.status === "installed", "install-user should install skill");
941
- assert(fs.existsSync(path.join(codexSkillRoot, "SKILL.md")), "install-user should copy SKILL.md");
942
- assert(fs.existsSync(path.join(codexSkillRoot, "templates-zh-CN/AGENTS.md.template")), "install-user should copy Chinese templates");
943
- assert(fs.existsSync(path.join(codexSkillRoot, "scripts/harness.mjs")), "install-user should copy CLI scripts");
944
- assert(fs.existsSync(path.join(codexSkillRoot, "docs-release/guides/agent-installation.md")), "install-user should copy agent guide");
945
- const userInstallAgain = expectJson(["install-user", "--agent", "codex", "--home", userInstallHome, "--yes"]);
946
- assert(userInstallAgain.targets?.[0]?.changes?.some((change) => change.action === "skip-existing"), "install-user should not overwrite existing files by default");
947
- const userDoctor = expectJson(["doctor-user", "--agent", "codex", "--home", userInstallHome]);
948
- assert(userDoctor.status === "pass", "doctor-user should pass for installed codex skill");
949
- assert(userDoctor.targets?.[0]?.version === packageVersion, "doctor-user should report installed package version");
950
- const missingDoctor = run(["doctor-user", "--agent", "gemini", "--home", userInstallHome]);
951
- assert(missingDoctor.status !== 0, "doctor-user should fail for missing agent install");
952
-
953
- const zhCapTarget = path.join(tmpRoot, "zh-cap-target");
954
- fs.mkdirSync(zhCapTarget);
955
- expectPass(["add-capability", "dashboard", "--locale", "zh-CN", zhCapTarget]);
956
- const zhCapRegistry = JSON.parse(fs.readFileSync(path.join(zhCapTarget, ".harness-capabilities.json"), "utf8"));
957
- assert(zhCapRegistry.locale === "zh-CN", "add-capability should support zh-CN locale for legacy targets");
958
- assert(fs.readFileSync(path.join(zhCapTarget, "AGENTS.md"), "utf8").includes("项目概况"), "zh-CN add-capability should write Chinese templates");
959
-
960
- const mismatch = run(["init", "--capabilities", "core,module-parallel", capTarget]);
961
- assert(mismatch.status !== 0, "init with mismatched existing capabilities should fail");
962
-
963
- const invalidReviewTarget = path.join(tmpRoot, "invalid-review");
964
- fs.mkdirSync(path.join(invalidReviewTarget, "docs/09-PLANNING/TASKS/bad"), { recursive: true });
965
- fs.writeFileSync(
966
- path.join(invalidReviewTarget, ".harness-capabilities.json"),
967
- JSON.stringify({ version: 1, locale: "en-US", capabilities: [{ name: "core", state: "configured" }, { name: "adversarial-review", state: "configured" }] }, null, 2),
968
- );
969
- fs.writeFileSync(path.join(invalidReviewTarget, "docs/09-PLANNING/TASKS/bad/task_plan.md"), "# Bad\n");
970
- fs.writeFileSync(
971
- path.join(invalidReviewTarget, "docs/09-PLANNING/TASKS/bad/review.md"),
972
- "# Review\n\n## Findings\n\n| ID | Severity | Finding | Evidence Checked | Required Action | Open | Disposition | Blocks Release | Follow-up |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| R-001 | P1 | Missing sections | none | fix | no | mitigated | no | next |\n",
973
- );
974
- const invalidReview = run(["check", "--profile", "target-project", invalidReviewTarget]);
975
- assert(invalidReview.status !== 0, "declared review missing required sections should fail");
976
-
977
- const invalidVerifierTarget = path.join(tmpRoot, "invalid-verifier");
978
- fs.mkdirSync(path.join(invalidVerifierTarget, "docs/09-PLANNING/TASKS/bad"), { recursive: true });
979
- fs.writeFileSync(
980
- path.join(invalidVerifierTarget, ".harness-capabilities.json"),
981
- JSON.stringify({ version: 1, locale: "en-US", capabilities: [{ name: "core", state: "configured" }, { name: "adversarial-review", state: "configured" }] }, null, 2),
982
- );
983
- fs.writeFileSync(path.join(invalidVerifierTarget, "docs/09-PLANNING/TASKS/bad/task_plan.md"), "# Bad\n");
984
- fs.writeFileSync(
985
- path.join(invalidVerifierTarget, "docs/09-PLANNING/TASKS/bad/review.md"),
986
- "# Review\n\n## Reviewer Identity\n\n| Reviewer | Type | Scope |\n| --- | --- | --- |\n| v1 | verifier | task |\n\n## Confidence Challenge\n\nVerifier reviewed this.\n\n## Evidence Checked\n\n| Evidence ID | Type | Path | Summary |\n| --- | --- | --- | --- |\n| E-001 | review | TARGET:docs/09-PLANNING/TASKS/bad/task_plan.md | checked |\n\n## Findings\n\n| ID | Severity | Finding | Evidence Checked | Required Action | Open | Disposition | Blocks Release | Follow-up |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| R-001 | P3 | Missing verifier schema | E-001 | fix | no | mitigated | no | next |\n\n## Final Confidence Basis\n\nexternal verifier reviewed this.\n",
987
- );
988
- const invalidVerifier = run(["check", "--profile", "target-project", invalidVerifierTarget]);
989
- assert(invalidVerifier.status !== 0, "verifier review without template_id/verdict should fail");
990
-
991
- const legacyContractTarget = path.join(tmpRoot, "legacy-contract");
992
- fs.mkdirSync(path.join(legacyContractTarget, "docs/09-PLANNING/TASKS/old"), { recursive: true });
993
- fs.writeFileSync(path.join(legacyContractTarget, "AGENTS.md"), "# AGENTS\n");
994
- fs.writeFileSync(path.join(legacyContractTarget, "docs/09-PLANNING/TASKS/old/task_plan.md"), "# Old\n");
995
- fs.writeFileSync(path.join(legacyContractTarget, "docs/09-PLANNING/TASKS/old/progress.md"), "# Progress\n\n## Status\n\nplanned\n");
996
- const legacyLoose = run(["check", "--profile", "target-project", legacyContractTarget]);
997
- assert(legacyLoose.status === 0, "legacy contract gaps should be advisory without strict");
998
- const legacyStrict = run(["check", "--profile", "target-project", "--strict", legacyContractTarget]);
999
- assert(legacyStrict.status !== 0, "strict legacy contract gaps should fail");
1000
-
1001
- const invalidTaskStateTarget = path.join(tmpRoot, "invalid-task-state");
1002
- fs.mkdirSync(path.join(invalidTaskStateTarget, "docs/09-PLANNING/TASKS/bad-state"), { recursive: true });
1003
- fs.writeFileSync(path.join(invalidTaskStateTarget, "AGENTS.md"), "# AGENTS\n");
1004
- fs.writeFileSync(
1005
- path.join(invalidTaskStateTarget, ".harness-capabilities.json"),
1006
- JSON.stringify({ version: 1, locale: "en-US", capabilities: [{ name: "core", state: "configured" }] }, null, 2),
1007
- );
1008
- fs.writeFileSync(path.join(invalidTaskStateTarget, "docs/09-PLANNING/TASKS/bad-state/task_plan.md"), "# Bad State\n");
1009
- fs.writeFileSync(path.join(invalidTaskStateTarget, "docs/09-PLANNING/TASKS/bad-state/progress.md"), "# Progress\n\n## Status\n\nin progresss\n");
1010
- const invalidTaskState = run(["check", "--profile", "target-project", invalidTaskStateTarget]);
1011
- assert(invalidTaskState.status !== 0, "invalid explicit task state should fail for declared v1 targets");
1012
- assert(invalidTaskState.stderr.includes("invalid task state"), "invalid task state failure should be explicit");
1013
- fs.writeFileSync(path.join(invalidTaskStateTarget, "docs/09-PLANNING/TASKS/bad-state/progress.md"), "# Progress\n\n## Status\n\nunknown\n");
1014
- const explicitUnknownTaskState = run(["check", "--profile", "target-project", invalidTaskStateTarget]);
1015
- assert(explicitUnknownTaskState.status !== 0, "explicit unknown task state should fail for declared v1 targets");
1016
- assert(explicitUnknownTaskState.stderr.includes("invalid task state"), "explicit unknown state failure should be explicit");
1017
-
1018
- const legacyPhaseTableTarget = path.join(tmpRoot, "legacy-phase-table");
1019
- fs.mkdirSync(path.join(legacyPhaseTableTarget, "docs/09-PLANNING/TASKS/table-active"), { recursive: true });
1020
- fs.writeFileSync(path.join(legacyPhaseTableTarget, "AGENTS.md"), "# Legacy Agents\n");
1021
- fs.writeFileSync(path.join(legacyPhaseTableTarget, "docs/09-PLANNING/TASKS/table-active/task_plan.md"), "# Table Active\n");
1022
- fs.writeFileSync(
1023
- path.join(legacyPhaseTableTarget, "docs/09-PLANNING/TASKS/table-active/progress.md"),
1024
- "# Progress\n\n## 阶段状态表\n| Phase | Status | Notes |\n| --- | --- | --- |\n| Phase 1 | Done | ok |\n| Phase 2 | In Progress | active |\n| Phase 3 | Pending | next |\n",
1025
- );
1026
- const legacyPhaseStatus = expectJson(["status", "--json", legacyPhaseTableTarget]);
1027
- assert(legacyPhaseStatus.tasks[0].state === "in_progress", "Agora-style legacy phase table should infer active task state");
1028
-
1029
- const legacyChineseTarget = path.join(tmpRoot, "legacy-chinese");
1030
- fs.mkdirSync(path.join(legacyChineseTarget, "docs/09-PLANNING/TASKS/old"), { recursive: true });
1031
- fs.writeFileSync(path.join(legacyChineseTarget, "AGENTS.md"), "# 中文项目\n\n这是旧 harness 项目。\n");
1032
- fs.writeFileSync(path.join(legacyChineseTarget, "docs/09-PLANNING/TASKS/old/task_plan.md"), "# 旧任务\n");
1033
- const legacyChinesePlan = expectJson(["migrate-plan", "--json", legacyChineseTarget]);
1034
- assert(legacyChinesePlan.locale === "zh-CN", "migrate-plan should infer zh-CN from Chinese legacy project text");
1035
- assert(
1036
- legacyChinesePlan.nextCommands.some((command) => command.includes("migrate-run --locale zh-CN")),
1037
- "migrate-plan should recommend zh-CN migration run for Chinese legacy projects",
1038
- );
1039
- assert(
1040
- legacyChinesePlan.nextCommands.some((command) => command.includes(legacyChineseTarget)),
1041
- "migrate-plan should keep executable target paths in CLI output",
1042
- );
1043
-
1044
- const legacyAdoptionTarget = path.join(tmpRoot, "legacy-adoption");
1045
- fs.mkdirSync(path.join(legacyAdoptionTarget, "docs/09-PLANNING/TASKS/old"), { recursive: true });
1046
- const legacyAgents = "# Legacy Agents\n\nLEGACY_DO_NOT_OVERWRITE\n";
1047
- const legacyClaude = "# Legacy Claude\n\nLEGACY_CLAUDE_DO_NOT_OVERWRITE\n";
1048
- const legacyLedger = "# Legacy Ledger\n\nLEGACY_LEDGER_DO_NOT_OVERWRITE\n";
1049
- const legacyTaskPlan = "# Legacy Task\n\nLEGACY_TASK_DO_NOT_OVERWRITE\n";
1050
- fs.writeFileSync(path.join(legacyAdoptionTarget, "AGENTS.md"), legacyAgents);
1051
- fs.writeFileSync(path.join(legacyAdoptionTarget, "CLAUDE.md"), legacyClaude);
1052
- fs.mkdirSync(path.join(legacyAdoptionTarget, "docs"), { recursive: true });
1053
- fs.writeFileSync(path.join(legacyAdoptionTarget, "docs/Harness-Ledger.md"), legacyLedger);
1054
- fs.writeFileSync(path.join(legacyAdoptionTarget, "docs/09-PLANNING/TASKS/old/task_plan.md"), legacyTaskPlan);
1055
- fs.writeFileSync(path.join(legacyAdoptionTarget, "docs/09-PLANNING/TASKS/old/progress.md"), "# Progress\n\n## Status\n\nplanned\n");
1056
- const legacyAdoption = expectJson(["add-capability", "safe-adoption", "--locale", "zh-CN", legacyAdoptionTarget]);
1057
- assert(legacyAdoption.report?.operation === "add-capability", "safe-adoption output should include add-capability report");
1058
- assert(
1059
- legacyAdoption.report?.capabilities?.some((capability) => capability.name === "safe-adoption" && capability.selected === true),
1060
- "safe-adoption report should mark safe-adoption selected",
1061
- );
1062
- assert(
1063
- legacyAdoption.report?.skipped?.includes("AGENTS.md") &&
1064
- legacyAdoption.report?.skipped?.includes("CLAUDE.md") &&
1065
- legacyAdoption.report?.skipped?.includes("docs/Harness-Ledger.md"),
1066
- "safe-adoption report should show skipped legacy files",
1067
- );
1068
- const legacyAdoptionRegistry = JSON.parse(fs.readFileSync(path.join(legacyAdoptionTarget, ".harness-capabilities.json"), "utf8"));
1069
- assert(legacyAdoptionRegistry.locale === "zh-CN", "safe-adoption should persist requested locale");
1070
- assert(legacyAdoptionRegistry.capabilities.some((capability) => capability.name === "core"), "safe-adoption should include core dependency");
1071
- assert(legacyAdoptionRegistry.capabilities.some((capability) => capability.name === "safe-adoption"), "safe-adoption registry missing capability");
1072
- assert(fs.readFileSync(path.join(legacyAdoptionTarget, "AGENTS.md"), "utf8") === legacyAgents, "safe-adoption should not overwrite legacy AGENTS.md");
1073
- assert(fs.readFileSync(path.join(legacyAdoptionTarget, "CLAUDE.md"), "utf8") === legacyClaude, "safe-adoption should not overwrite legacy CLAUDE.md");
1074
- assert(fs.readFileSync(path.join(legacyAdoptionTarget, "docs/Harness-Ledger.md"), "utf8") === legacyLedger, "safe-adoption should not overwrite legacy ledger");
1075
- assert(fs.readFileSync(path.join(legacyAdoptionTarget, "docs/09-PLANNING/TASKS/old/task_plan.md"), "utf8") === legacyTaskPlan, "safe-adoption should not overwrite old task plans");
1076
- assert(
1077
- fs.readFileSync(path.join(legacyAdoptionTarget, "docs/09-PLANNING/TASKS/_task-template/review.md"), "utf8").includes("审查者身份"),
1078
- "safe-adoption should add missing localized v1 templates",
1079
- );
1080
- const adoptedStatus = expectJson(["status", "--json", legacyAdoptionTarget]);
1081
- assert(adoptedStatus.checkState.status === "warn", "safe-adoption should warn on historical contract gaps without failing");
1082
- assert(
1083
- adoptedStatus.checkState.details.warnings.some((warning) => warning.includes("adoption-needed")),
1084
- "safe-adoption warnings should be routed as adoption-needed",
1085
- );
1086
- assert(adoptedStatus.tasks[0].inferredModule, "legacy task status should expose inferred module classification");
1087
- assert(adoptedStatus.tasks[0].classificationBucket, "legacy task status should expose classification bucket");
1088
- const legacyAdoptionDashboard = path.join(tmpRoot, "legacy-adoption-dashboard");
1089
- expectPass(["dashboard", "--out-dir", legacyAdoptionDashboard, legacyAdoptionTarget]);
1090
- const legacyAdoptionWarnings = JSON.parse(fs.readFileSync(path.join(legacyAdoptionDashboard, "data/adoption.json"), "utf8"));
1091
- const firstAdoptionWarning = legacyAdoptionWarnings.warnings?.[0];
1092
- assert(firstAdoptionWarning?.type, "adoption warning should expose stable type");
1093
- assert(firstAdoptionWarning?.scope, "adoption warning should expose scope");
1094
- assert(firstAdoptionWarning?.priority, "adoption warning should expose priority");
1095
- assert(firstAdoptionWarning?.phase, "adoption warning should expose migration phase");
1096
- assert(firstAdoptionWarning?.fixability, "adoption warning should expose fixability");
1097
- assert(firstAdoptionWarning?.status, "adoption warning should expose queue status");
1098
- assert(firstAdoptionWarning?.confidence, "adoption warning should expose confidence");
1099
- assert(Array.isArray(firstAdoptionWarning?.affectedPaths), "adoption warning should expose affectedPaths array");
1100
- assert(firstAdoptionWarning?.affected && firstAdoptionWarning?.requiredAction, "adoption warning should preserve affected and requiredAction fields");
1101
- const migrationPlan = expectJson(["migrate-plan", "--json", "--limit", "5", legacyAdoptionTarget]);
1102
- assert(migrationPlan.operation === "migrate-plan", "migrate-plan should report its operation");
1103
- assert(migrationPlan.compatibility?.preserves?.some((item) => item.includes("AGENTS.md")), "migrate-plan should state preservation rules");
1104
- assert(migrationPlan.phases?.some((phase) => phase.id === "MP-03"), "migrate-plan should include active task migration phase");
1105
- assert(migrationPlan.summary?.missingExecutionStrategy >= 1, "migrate-plan should count missing execution strategies");
1106
- assert(migrationPlan.summary?.missingVisualMap >= 1, "migrate-plan should count missing canonical visual maps");
1107
- assert(migrationPlan.summary?.visualMapActions >= 1, "migrate-plan should expose visual map action count");
1108
- assert(migrationPlan.summary?.legacyVisualOnly >= 1, "migrate-plan should expose legacy visual-only count");
1109
- assert(migrationPlan.summary?.weakBrief >= 1, "migrate-plan should expose weak brief count");
1110
- assert(migrationPlan.summary?.missingCanonicalVisualMap >= 1, "migrate-plan should expose missing canonical visual map count");
1111
- assert(migrationPlan.summary?.fullCutoverEligible === false, "legacy migrate-plan should not be full-cutover eligible");
1112
- assert(migrationPlan.taskActions?.some((action) => action.taskId === "old" && action.files.includes("execution_strategy.md")), "migrate-plan should include task-level file actions");
1113
- assert(migrationPlan.taskActions?.some((action) => action.taskId === "old" && action.files.includes("visual_map.md")), "migrate-plan should include canonical visual map action");
1114
- assert(migrationPlan.taskActions?.some((action) => action.taskId === "old" && action.files.includes("brief.md")), "migrate-plan should include active brief migration action");
1115
- assert(migrationPlan.visualMapActions?.some((action) => action.taskId === "old"), "migrate-plan should expose visual map actions separately");
1116
- assert(migrationPlan.legacyVisualOnlyTasks?.some((action) => action.taskId === "old"), "migrate-plan should expose legacy visual-only tasks separately");
1117
- assert(migrationPlan.weakBriefTasks?.some((action) => action.taskId === "old"), "migrate-plan should expose weak brief tasks separately");
1118
- assert(migrationPlan.taskActions?.some((action) => action.commands.some((command) => command.includes("_task-template/brief.md"))), "migrate-plan should emit a command per active task file");
1119
- assert(migrationPlan.nextCommands?.some((command) => command.includes("migrate-run")), "migrate-plan should include migrate-run command");
1120
- assert(migrationPlan.nextCommands?.some((command) => command.includes("migrate-verify --full-cutover")), "migrate-plan should include full-cutover verify command");
1121
- const migrationPlanText = expectPass(["migrate-plan", "--limit", "3", legacyAdoptionTarget]).stdout;
1122
- assert(migrationPlanText.includes("Migration Plan"), "migrate-plan text output should have a readable heading");
1123
- assert(migrationPlanText.includes("legacy residuals:"), "migrate-plan text output should show residual counts");
1124
- assert(migrationPlanText.includes("full cutover eligible:"), "migrate-plan text output should show full cutover eligibility");
1125
- const adoptedStrict = run(["status", "--json", "--strict", legacyAdoptionTarget]);
1126
- assert(adoptedStrict.status !== 0, "safe-adoption strict status should still fail on historical contract gaps");
1127
-
1128
- const migrationRunTarget = path.join(tmpRoot, "migration-run");
1129
- fs.mkdirSync(path.join(migrationRunTarget, "docs/09-PLANNING/TASKS/old"), { recursive: true });
1130
- fs.writeFileSync(path.join(migrationRunTarget, "AGENTS.md"), "# 旧项目 Agents\n\nLegacy English notes are still present.\n");
1131
- fs.writeFileSync(path.join(migrationRunTarget, "docs/09-PLANNING/TASKS/old/task_plan.md"), "# Old Task\n\nThis active task predates v1.\n");
1132
- fs.writeFileSync(path.join(migrationRunTarget, "docs/09-PLANNING/TASKS/old/progress.md"), "# Progress\n\n## Status\n\nplanned\n");
1133
- spawnSync("git", ["init"], { cwd: migrationRunTarget, encoding: "utf8" });
1134
- spawnSync("git", ["add", "."], { cwd: migrationRunTarget, encoding: "utf8" });
1135
- spawnSync("git", ["-c", "user.name=Harness Test", "-c", "user.email=harness@example.test", "commit", "-m", "legacy baseline"], {
1136
- cwd: migrationRunTarget,
1137
- encoding: "utf8",
1138
- });
1139
- const migrationSessionDir = path.join(tmpRoot, "migration-session");
1140
- const migrationDashboardDir = path.join(tmpRoot, "migration-dashboard");
1141
- const migrationRun = expectJson([
1142
- "migrate-run",
1143
- "--locale",
1144
- "zh-CN",
1145
- "--session-dir",
1146
- migrationSessionDir,
1147
- "--out-dir",
1148
- migrationDashboardDir,
1149
- migrationRunTarget,
1150
- ]);
1151
- assert(migrationRun.operation === "migrate-run", "migrate-run should report its operation");
1152
- assert(migrationRun.result === "adopted-with-strict-deferred", "legacy migrate-run should keep strict cutover deferred");
1153
- assert(migrationRun.checks.normal.status !== "fail", "legacy migrate-run should keep normal check usable");
1154
- assert(migrationRun.checks.strict.status === "fail", "legacy migrate-run should record strict failure");
1155
- assert(migrationRun.strictDeferred?.owner && migrationRun.strictDeferred?.trigger && migrationRun.strictDeferred?.nextAction, "strict-deferred migration should include owner, trigger, and nextAction");
1156
- assert(fs.existsSync(migrationRun.sessionPath), "migrate-run should write session.json");
1157
- assert(fs.existsSync(migrationRun.reportPath), "migrate-run should write report.md");
1158
- assert(fs.existsSync(path.join(migrationDashboardDir, "index.html")), "migrate-run should generate an HTML dashboard folder");
1159
- const migrationRegistry = JSON.parse(fs.readFileSync(path.join(migrationRunTarget, ".harness-capabilities.json"), "utf8"));
1160
- assert(migrationRegistry.locale === "zh-CN", "migrate-run should persist selected locale");
1161
- assert(migrationRegistry.capabilities.some((capability) => capability.name === "safe-adoption"), "migrate-run should declare safe-adoption");
1162
- assert(migrationRegistry.capabilities.some((capability) => capability.name === "dashboard"), "migrate-run should declare dashboard");
1163
- assert(!migrationRun.git.after.staged.length, "migrate-run should not stage target files");
1164
- assert(
1165
- spawnSync("git", ["-C", migrationRunTarget, "diff", "--cached", "--name-only"], { encoding: "utf8" }).stdout.trim() === "",
1166
- "migrate-run should leave the target git index untouched",
1167
- );
1168
- const migrationVerify = expectJson(["migrate-verify", "--json", migrationRun.sessionPath]);
1169
- assert(migrationVerify.status === "pass", "migrate-verify should pass for migrate-run output");
1170
- assert(migrationVerify.dashboard?.indexPath?.endsWith("index.html"), "migrate-verify should preserve HTML dashboard evidence");
1171
- const migrationFullCutover = run(["migrate-verify", "--json", "--full-cutover", migrationRun.sessionPath]);
1172
- assert(migrationFullCutover.status !== 0, "full cutover verify should reject baseline legacy-only migration output");
1173
-
1174
- const falseSessionPath = path.join(tmpRoot, "false-session.json");
1175
- fs.writeFileSync(
1176
- falseSessionPath,
1177
- JSON.stringify(
1178
- {
1179
- ...JSON.parse(fs.readFileSync(migrationRun.sessionPath, "utf8")),
1180
- dashboard: { dir: migrationRunTarget, indexPath: path.join(migrationRunTarget, "docs/Harness-Ledger.md"), kind: "html-folder" },
1181
- },
1182
- null,
1183
- 2,
1184
- ),
1185
- );
1186
- const falseVerify = run(["migrate-verify", "--json", falseSessionPath]);
1187
- assert(falseVerify.status !== 0, "migrate-verify should reject non-HTML dashboard evidence");
1188
-
1189
- const mixedLocaleTarget = path.join(tmpRoot, "mixed-locale");
1190
- fs.mkdirSync(path.join(mixedLocaleTarget, "docs/09-PLANNING/TASKS/mixed"), { recursive: true });
1191
- fs.writeFileSync(path.join(mixedLocaleTarget, "AGENTS.md"), "# 中文入口\n\n这是一个中文项目,迁移时需要选择中文或英文模板。\n");
1192
- fs.writeFileSync(
1193
- path.join(mixedLocaleTarget, "docs/09-PLANNING/TASKS/mixed/task_plan.md"),
1194
- "# Legacy task\n\nThis English task plan intentionally contains enough words to make the language decision ambiguous for migration.\n",
1195
- );
1196
- const mixedLocaleFail = run(["migrate-run", "--plan-only", mixedLocaleTarget]);
1197
- assert(mixedLocaleFail.status !== 0, "migrate-run should require --locale for mixed-language targets");
1198
- assert(mixedLocaleFail.stderr.includes("--locale zh-CN"), "mixed-language failure should tell agents how to choose locale");
1199
- const mixedLocalePlan = expectJson(["migrate-run", "--plan-only", "--locale", "zh-CN", "--session-dir", path.join(tmpRoot, "mixed-locale-session"), mixedLocaleTarget]);
1200
- assert(mixedLocalePlan.result === "plan-only", "migrate-run --plan-only should produce a plan-only session");
1201
- assert(mixedLocalePlan.localeDecision.selected === "zh-CN", "migrate-run --locale should resolve mixed-language decision");
1202
- const planOnlyVerify = run(["migrate-verify", "--json", mixedLocalePlan.sessionPath]);
1203
- assert(planOnlyVerify.status !== 0, "migrate-verify should reject plan-only sessions as completion evidence");
1204
-
1205
- const dirtyMigrationTarget = path.join(tmpRoot, "dirty-migration");
1206
- fs.mkdirSync(dirtyMigrationTarget);
1207
- fs.writeFileSync(path.join(dirtyMigrationTarget, "AGENTS.md"), "# Legacy\n");
1208
- spawnSync("git", ["init"], { cwd: dirtyMigrationTarget, encoding: "utf8" });
1209
- spawnSync("git", ["add", "."], { cwd: dirtyMigrationTarget, encoding: "utf8" });
1210
- spawnSync("git", ["-c", "user.name=Harness Test", "-c", "user.email=harness@example.test", "commit", "-m", "baseline"], {
1211
- cwd: dirtyMigrationTarget,
1212
- encoding: "utf8",
1213
- });
1214
- fs.writeFileSync(path.join(dirtyMigrationTarget, "unreviewed.txt"), "dirty\n");
1215
- const dirtyMigration = run(["migrate-run", "--locale", "en-US", dirtyMigrationTarget]);
1216
- assert(dirtyMigration.status !== 0, "migrate-run should stop on dirty git targets by default");
1217
- assert(dirtyMigration.stderr.includes("--allow-dirty"), "dirty guard should explain --allow-dirty escape hatch");
1218
-
1219
- const forgedStrictSessionPath = path.join(tmpRoot, "forged-strict-session.json");
1220
- const forgedStrictSession = {
1221
- ...JSON.parse(fs.readFileSync(migrationRun.sessionPath, "utf8")),
1222
- result: "complete",
1223
- checks: { ...migrationRun.checks, strict: { status: "pass", failures: 0, warnings: 0 } },
1224
- strictDeferred: null,
1225
- };
1226
- fs.writeFileSync(forgedStrictSessionPath, `${JSON.stringify(forgedStrictSession, null, 2)}\n`);
1227
- const forgedStrictVerify = run(["migrate-verify", "--json", forgedStrictSessionPath]);
1228
- assert(forgedStrictVerify.status !== 0, "migrate-verify should rerun strict and reject forged strict pass sessions");
1229
-
1230
- const fakeDashboardDir = path.join(tmpRoot, "fake-dashboard");
1231
- const fakeDashboardPath = path.join(fakeDashboardDir, "index.html");
1232
- fs.mkdirSync(path.join(fakeDashboardDir, "assets"), { recursive: true });
1233
- fs.mkdirSync(path.join(fakeDashboardDir, "data"), { recursive: true });
1234
- fs.writeFileSync(fakeDashboardPath, '<html><script src="assets/dashboard-data.js"></script></html>\n');
1235
- fs.writeFileSync(path.join(fakeDashboardDir, "assets/dashboard-data.js"), 'window.__HARNESS_DASHBOARD__ = {"status":{"schemaVersion":2,"project":{"name":"WrongProject"},"checkState":{}},"adoption":{"warnings":[]}};\n');
1236
- fs.writeFileSync(path.join(fakeDashboardDir, "data/status.json"), "{}\n");
1237
- fs.writeFileSync(path.join(fakeDashboardDir, "data/adoption.json"), "{}\n");
1238
- const fakeDashboardSessionPath = path.join(tmpRoot, "fake-dashboard-session.json");
1239
- const fakeDashboardSession = {
1240
- ...JSON.parse(fs.readFileSync(migrationRun.sessionPath, "utf8")),
1241
- dashboard: { dir: fakeDashboardDir, indexPath: fakeDashboardPath, kind: "html-folder" },
1242
- };
1243
- fs.writeFileSync(fakeDashboardSessionPath, `${JSON.stringify(fakeDashboardSession, null, 2)}\n`);
1244
- const fakeDashboardVerify = run(["migrate-verify", "--json", fakeDashboardSessionPath]);
1245
- assert(fakeDashboardVerify.status !== 0, "migrate-verify should reject arbitrary HTML as dashboard evidence");
1246
-
1247
- const missingGitSessionPath = path.join(tmpRoot, "missing-git-session.json");
1248
- const missingGitSession = {
1249
- ...JSON.parse(fs.readFileSync(migrationRun.sessionPath, "utf8")),
1250
- git: undefined,
1251
- };
1252
- fs.writeFileSync(missingGitSessionPath, `${JSON.stringify(missingGitSession, null, 2)}\n`);
1253
- const missingGitVerify = run(["migrate-verify", "--json", missingGitSessionPath]);
1254
- assert(missingGitVerify.status !== 0, "migrate-verify should require git audit metadata");
1255
-
1256
- const legacyCheckerOnlyTarget = path.join(tmpRoot, "legacy-checker-only");
1257
- fs.mkdirSync(legacyCheckerOnlyTarget);
1258
- expectPass(["add-capability", "safe-adoption", "--locale", "en-US", legacyCheckerOnlyTarget]);
1259
- const legacyCheckerOnly = expectJson(["status", "--json", legacyCheckerOnlyTarget]);
1260
- assert(legacyCheckerOnly.checkState.status === "warn", "safe-adoption should surface legacy checker gaps as warnings");
1261
- assert(legacyCheckerOnly.checkState.legacy.status === "fail", "safe-adoption should keep legacy checker signal after registry creation");
1262
- const legacyCheckerOnlyStrictStatus = run(["status", "--json", "--strict", legacyCheckerOnlyTarget]);
1263
- assert(legacyCheckerOnlyStrictStatus.status !== 0, "safe-adoption strict status should fail when legacy checker fails even if v1 validators are clean");
1264
- const legacyCheckerOnlyStrictCheck = run(["check", "--profile", "target-project", "--strict", legacyCheckerOnlyTarget]);
1265
- assert(legacyCheckerOnlyStrictCheck.status !== 0, "safe-adoption strict check should fail when legacy checker fails even if v1 validators are clean");
1266
-
1267
- const mingjingDocs = "/Users/lizeyu/Projects/mingjing-app/docs";
1268
- if (fs.existsSync(mingjingDocs)) {
1269
- const mingjingRepo = path.dirname(mingjingDocs);
1270
- const before = spawnSync("git", ["-C", mingjingRepo, "status", "--short", "--", "docs"], { encoding: "utf8" }).stdout;
1271
- const mingjing = run(["status", "--json", mingjingDocs]);
1272
- assert(mingjing.status === 0, "mingjing legacy status should be a safe-adoption warning, not a failure");
1273
- const status = JSON.parse(mingjing.stdout);
1274
- assert(status.project.docsOnly === true, "mingjing docs target was not detected as docsOnly");
1275
- assert(status.mode === "legacy-compat", "mingjing docs should be legacy-compat without capability registry");
1276
- assert(status.checkState.status === "warn", "mingjing legacy status should warn");
1277
- expectPass(["check", "--profile", "target-project", mingjingDocs]);
1278
- const strictStatus = run(["status", "--json", "--strict", mingjingDocs]);
1279
- const strictCheck = run(["check", "--profile", "target-project", "--strict", mingjingDocs]);
1280
- assert(strictStatus.status !== 0, "mingjing strict status should fail on legacy checker failures");
1281
- assert(strictCheck.status !== 0, "mingjing strict check should fail on legacy checker failures");
1282
- const mingjingDashboard = path.join(tmpRoot, "mingjing-dashboard.html");
1283
- expectPass(["dashboard", "--out", mingjingDashboard, mingjingDocs]);
1284
- assert(fs.existsSync(mingjingDashboard), "mingjing dashboard file was not created");
1285
- const mingjingDashboardDir = path.join(tmpRoot, "mingjing-dashboard-folder");
1286
- expectPass(["dashboard", "--out-dir", mingjingDashboardDir, mingjingDocs]);
1287
- assert(fs.existsSync(path.join(mingjingDashboardDir, "index.html")), "mingjing dashboard folder index was not created");
1288
- for (const generated of ["data/status.json", "data/tables.json", "data/documents.json", "data/graph.json", "data/adoption.json", "assets/dashboard-data.js"]) {
1289
- const content = fs.readFileSync(path.join(mingjingDashboardDir, generated), "utf8");
1290
- assert(!content.includes("/Users/lizeyu"), `mingjing ${generated} leaked local user path`);
1291
- assert(!content.includes("file://"), `mingjing ${generated} leaked file URL`);
1292
- }
1293
- const mingjingDocuments = JSON.parse(fs.readFileSync(path.join(mingjingDashboardDir, "data/documents.json"), "utf8"));
1294
- const mingjingTables = JSON.parse(fs.readFileSync(path.join(mingjingDashboardDir, "data/tables.json"), "utf8"));
1295
- assert(!JSON.stringify(mingjingDocuments.documents.map((doc) => doc.path)).includes("_task-template"), "mingjing documents included task template paths");
1296
- assert(!JSON.stringify(mingjingTables.tables.map((table) => table.source)).includes("_task-template"), "mingjing tables included task template sources");
1297
- const mingjingGraph = JSON.parse(fs.readFileSync(path.join(mingjingDashboardDir, "data/graph.json"), "utf8"));
1298
- assert(mingjingGraph.nodes.some((node) => node.type === "module"), "mingjing graph missing module nodes");
1299
- assert(mingjingGraph.edges.length > 0, "mingjing graph missing dependency edges");
1300
- assertGraphIntegrity(mingjingGraph, "mingjing graph");
1301
- const after = spawnSync("git", ["-C", mingjingRepo, "status", "--short", "--", "docs"], { encoding: "utf8" }).stdout;
1302
- assert(before === after, "mingjing docs changed during status/check/dashboard smoke");
1303
- }
1304
-
1305
- // --- Date prefix auto-generation tests ---
1306
- const datePrefixTarget = path.join(tmpRoot, "date-prefix-target");
1307
- fs.mkdirSync(datePrefixTarget);
1308
- expectJson(["init", "--locale", "en-US", "--capabilities", "core,dashboard", datePrefixTarget]);
1309
-
1310
- // 1. Bare slug gets auto-prefixed with local date
1311
- const datePrefixCreate = expectJson(["new-task", "my-feature", "--title", "My Feature", datePrefixTarget]);
1312
- assert(datePrefixCreate.task?.shortId === `${todayLocal}-my-feature`, "new-task bare slug should auto-prefix local date");
1313
- assert(datePrefixCreate.task?.id === `TASKS/${todayLocal}-my-feature`, "new-task bare slug task id should include date prefix");
1314
- assert(datePrefixCreate.task?.title === "My Feature", "new-task should use explicit title, not dated id");
1315
- assert(
1316
- fs.existsSync(path.join(datePrefixTarget, `docs/09-PLANNING/TASKS/${todayLocal}-my-feature/task_plan.md`)),
1317
- "new-task bare slug should create dated directory",
1318
- );
1319
-
1320
- // 2. Already-dated slug should NOT double-prefix
1321
- const alreadyDated = expectJson(["new-task", `${todayLocal}-existing-date`, "--title", "Already Dated", datePrefixTarget]);
1322
- assert(alreadyDated.task?.shortId === `${todayLocal}-existing-date`, "new-task already-dated slug should not double-prefix");
1323
- assert(
1324
- fs.existsSync(path.join(datePrefixTarget, `docs/09-PLANNING/TASKS/${todayLocal}-existing-date/task_plan.md`)),
1325
- "new-task already-dated slug should create directory without double date",
1326
- );
1327
- assert(
1328
- !fs.existsSync(path.join(datePrefixTarget, `docs/09-PLANNING/TASKS/${todayLocal}-${todayLocal}-existing-date`)),
1329
- "new-task already-dated slug must not create double-dated directory",
1330
- );
1331
-
1332
- // 3. Module task also gets date prefix
1333
- const moduleWithDate = expectJson(["new-task", "module-feat", "--module", "payments", "--title", "Module Feature", datePrefixTarget]);
1334
- assert(moduleWithDate.task?.shortId === `${todayLocal}-module-feat`, "new-task --module bare slug should auto-prefix local date");
1335
- assert(moduleWithDate.task?.id === `MODULES/payments/${todayLocal}-module-feat`, "new-task --module task id should include date prefix");
1336
- assert(
1337
- fs.existsSync(path.join(datePrefixTarget, `docs/09-PLANNING/MODULES/payments/${todayLocal}-module-feat/task_plan.md`)),
1338
- "new-task --module should create dated directory under module",
1339
- );
1340
-
1341
- // 4. Bare slug lifecycle resolution: task-start resolves "my-feature" to dated directory
1342
- const startByBareSlug = expectJson(["task-start", "my-feature", "--message", "start via bare slug", datePrefixTarget]);
1343
- assert(startByBareSlug.task?.id === `TASKS/${todayLocal}-my-feature`, "task-start should resolve bare slug to dated directory");
1344
- assert(startByBareSlug.task?.state === "in_progress", "task-start via bare slug should transition to in_progress");
1345
-
1346
- // 5. task-log also resolves bare slug
1347
- expectJson(["task-log", "my-feature", "--message", "log via bare slug", "--evidence", "command:TARGET:test:passed", datePrefixTarget]);
1348
-
1349
- // 6. Ambiguous multi-match: create a second dated directory with same bare slug
1350
- fs.mkdirSync(path.join(datePrefixTarget, "docs/09-PLANNING/TASKS/2025-01-01-my-feature"), { recursive: true });
1351
- fs.writeFileSync(path.join(datePrefixTarget, "docs/09-PLANNING/TASKS/2025-01-01-my-feature/task_plan.md"), "# Old\n");
1352
- const ambiguousBareSlug = run(["task-log", "my-feature", "--message", "ambiguous", datePrefixTarget]);
1353
- assert(ambiguousBareSlug.status !== 0, "bare slug matching multiple dated directories should fail");
1354
- assert(ambiguousBareSlug.stderr.includes("Ambiguous task reference"), "ambiguous bare slug should report ambiguity");
1355
- assert(ambiguousBareSlug.stderr.includes(`${todayLocal}-my-feature`) && ambiguousBareSlug.stderr.includes("2025-01-01-my-feature"), "ambiguous error should list both dated candidates");
1356
-
1357
- // 7. Title preservation: title should be the semantic slug, not the date-id
1358
- const noTitleCreate = expectJson(["new-task", "auto-title-check", datePrefixTarget]);
1359
- assert(noTitleCreate.task?.title === "auto-title-check", "new-task without --title should use semantic slug as display title, not dated id");
1360
- assert(noTitleCreate.task?.shortId === `${todayLocal}-auto-title-check`, "new-task without --title should still date-prefix the shortId");
1361
-
1362
- console.log("Harness v1 tests passed");
1363
-
1364
- function hasLocalAbsolutePath(content) {
1365
- return /(?:^|[\s"'(])(?:\/Users\/|\/Volumes\/|\/tmp\/|\/private\/tmp\/|\/var\/folders\/|\/home\/|[A-Za-z]:\\)/.test(content);
1366
- }
1367
-
1368
- function assertGraphIntegrity(graph, label) {
1369
- const nodes = new Set((graph.nodes || []).map((node) => node.id));
1370
- for (const edge of graph.edges || []) {
1371
- assert(nodes.has(edge.from), `${label} has dangling edge source ${edge.from}`);
1372
- assert(nodes.has(edge.to), `${label} has dangling edge target ${edge.to}`);
1373
- }
1374
- }
1375
-
1376
- function relativeFiles(root) {
1377
- const results = [];
1378
- function walk(dir) {
1379
- for (const entry of fs.readdirSync(dir)) {
1380
- const full = path.join(dir, entry);
1381
- const stat = fs.statSync(full);
1382
- if (stat.isDirectory()) {
1383
- walk(full);
1384
- } else {
1385
- results.push(toPosix(path.relative(root, full)));
1386
- }
1387
- }
1388
- }
1389
- walk(root);
1390
- return results.sort();
1391
- }
1392
-
1393
- function toPosix(value) {
1394
- return value.split(path.sep).join("/");
1395
- }