coding-agent-harness 1.0.1 → 1.0.2

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 (159) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en-US.md +14 -0
  3. package/README.md +111 -86
  4. package/README.zh-CN.md +270 -0
  5. package/SKILL.md +116 -189
  6. package/docs-release/README.md +72 -5
  7. package/docs-release/architecture/overview.md +286 -28
  8. package/docs-release/architecture/overview.zh-CN.md +288 -0
  9. package/docs-release/assets/dashboard-overview-en.png +0 -0
  10. package/docs-release/assets/harness-architecture.svg +163 -0
  11. package/docs-release/assets/harness-workflow.svg +64 -0
  12. package/docs-release/guides/agent-installation.en-US.md +214 -0
  13. package/docs-release/guides/agent-installation.md +123 -26
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +112 -0
  15. package/docs-release/guides/document-audience-and-surfaces.md +112 -0
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +384 -0
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +361 -0
  20. package/docs-release/guides/migration-playbook.en-US.md +325 -0
  21. package/docs-release/guides/migration-playbook.md +329 -0
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +252 -0
  23. package/docs-release/guides/parent-control-repository-pattern.md +252 -0
  24. package/docs-release/guides/repository-operating-models.en-US.md +196 -0
  25. package/docs-release/guides/repository-operating-models.md +196 -0
  26. package/docs-release/intl/README.md +15 -0
  27. package/docs-release/intl/de-DE.md +18 -0
  28. package/docs-release/intl/en-US.md +18 -0
  29. package/docs-release/intl/es-ES.md +18 -0
  30. package/docs-release/intl/fr-FR.md +18 -0
  31. package/docs-release/intl/ja-JP.md +18 -0
  32. package/docs-release/intl/ko-KR.md +18 -0
  33. package/docs-release/intl/zh-CN.md +18 -0
  34. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  35. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  36. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  37. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  38. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  39. package/package.json +3 -1
  40. package/references/agents-md-pattern.md +3 -3
  41. package/references/docs-directory-standard.md +47 -3
  42. package/references/external-source-intake-standard.md +75 -0
  43. package/references/harness-ledger.md +5 -3
  44. package/references/legacy-12-phase-bootstrap.md +41 -0
  45. package/references/lessons-governance.md +23 -6
  46. package/references/planning-loop.md +41 -3
  47. package/references/project-onboarding-audit.md +10 -0
  48. package/references/repo-governance-standard.md +2 -0
  49. package/references/testing-standard.md +50 -0
  50. package/references/walkthrough-closeout.md +6 -5
  51. package/scripts/check-harness.mjs +76 -35
  52. package/scripts/harness.mjs +303 -12
  53. package/scripts/lib/capability-registry.mjs +533 -0
  54. package/scripts/lib/check-profiles.mjs +510 -0
  55. package/scripts/lib/core-shared.mjs +186 -0
  56. package/scripts/lib/dashboard-data.mjs +389 -0
  57. package/scripts/lib/dashboard-workbench.mjs +217 -0
  58. package/scripts/lib/dashboard-writer.mjs +93 -2
  59. package/scripts/lib/harness-core.mjs +10 -1318
  60. package/scripts/lib/lesson-maintenance.mjs +145 -0
  61. package/scripts/lib/markdown-utils.mjs +158 -0
  62. package/scripts/lib/migration-planner.mjs +478 -0
  63. package/scripts/lib/migration-support.mjs +312 -0
  64. package/scripts/lib/task-lifecycle.mjs +755 -0
  65. package/scripts/lib/task-scanner.mjs +682 -0
  66. package/scripts/smoke-dashboard.mjs +22 -0
  67. package/scripts/test-harness.mjs +926 -14
  68. package/templates/AGENTS.md.template +41 -30
  69. package/templates/architecture/Architecture-SSoT.md +21 -0
  70. package/templates/architecture/README.md +49 -0
  71. package/templates/architecture/critical-flows.md +22 -0
  72. package/templates/architecture/local-repo-context.md +20 -0
  73. package/templates/architecture/service-catalog.md +17 -0
  74. package/templates/architecture/services/service-template.md +31 -0
  75. package/templates/architecture/system-map.md +22 -0
  76. package/templates/dashboard/assets/app-src/00-state.js +41 -0
  77. package/templates/dashboard/assets/app-src/10-router.js +76 -0
  78. package/templates/dashboard/assets/app-src/20-overview.js +235 -0
  79. package/templates/dashboard/assets/app-src/30-tasks.js +563 -0
  80. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  81. package/templates/dashboard/assets/app-src/45-review.js +128 -0
  82. package/templates/dashboard/assets/app-src/50-migration.js +169 -0
  83. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  84. package/templates/dashboard/assets/app-src/90-bindings.js +382 -0
  85. package/templates/dashboard/assets/app.css +2575 -310
  86. package/templates/dashboard/assets/app.js +1498 -307
  87. package/templates/dashboard/assets/app.manifest.json +11 -0
  88. package/templates/dashboard/assets/i18n.js +429 -44
  89. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  90. package/templates/development/README.md +52 -0
  91. package/templates/development/codebase-map.md +11 -0
  92. package/templates/development/cross-repo-debugging.md +18 -0
  93. package/templates/development/external-context/service-template.md +33 -0
  94. package/templates/development/external-source-packs/README.md +24 -0
  95. package/templates/development/external-source-packs/digest-template.md +28 -0
  96. package/templates/development/local-setup.md +16 -0
  97. package/templates/development/stubs-and-mocks.md +11 -0
  98. package/templates/integrations/README.md +40 -0
  99. package/templates/integrations/api-contract.md +42 -0
  100. package/templates/integrations/event-contract.md +46 -0
  101. package/templates/integrations/third-party/vendor-template.md +42 -0
  102. package/templates/integrations/webhook-contract.md +41 -0
  103. package/templates/planning/brief.md +32 -0
  104. package/templates/planning/lesson_candidates.md +58 -0
  105. package/templates/planning/long-running-task-contract.md +7 -0
  106. package/templates/planning/module_brief.md +25 -0
  107. package/templates/planning/module_session_prompt.md +6 -0
  108. package/templates/planning/task_plan.md +7 -5
  109. package/templates/planning/{visual_roadmap.md → visual_map.md} +24 -2
  110. package/templates/reference/docs-library-standard.md +31 -0
  111. package/templates/reference/execution-workflow-standard.md +4 -2
  112. package/templates/reference/external-source-intake-standard.md +82 -0
  113. package/templates/reference/harness-ledger-standard.md +1 -0
  114. package/templates/reference/repo-governance-standard.md +6 -4
  115. package/templates/reference/walkthrough-standard.md +2 -1
  116. package/templates/walkthrough/walkthrough-template.md +2 -2
  117. package/templates-zh-CN/AGENTS.md.template +69 -70
  118. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  119. package/templates-zh-CN/architecture/README.md +51 -0
  120. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  121. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  122. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  123. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  124. package/templates-zh-CN/architecture/system-map.md +22 -0
  125. package/templates-zh-CN/development/README.md +54 -0
  126. package/templates-zh-CN/development/codebase-map.md +11 -0
  127. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  128. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  129. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  130. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  131. package/templates-zh-CN/development/local-setup.md +16 -0
  132. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  133. package/templates-zh-CN/integrations/README.md +42 -0
  134. package/templates-zh-CN/integrations/api-contract.md +42 -0
  135. package/templates-zh-CN/integrations/event-contract.md +46 -0
  136. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  137. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  138. package/templates-zh-CN/planning/brief.md +32 -0
  139. package/templates-zh-CN/planning/lesson_candidates.md +58 -0
  140. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  141. package/templates-zh-CN/planning/module_brief.md +25 -0
  142. package/templates-zh-CN/planning/module_plan.md +2 -2
  143. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  144. package/templates-zh-CN/planning/task_plan.md +10 -4
  145. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  146. package/templates-zh-CN/reference/docs-library-standard.md +35 -0
  147. package/templates-zh-CN/reference/execution-workflow-standard.md +9 -2
  148. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  149. package/templates-zh-CN/reference/harness-ledger-standard.md +5 -2
  150. package/templates-zh-CN/reference/repo-governance-standard.md +2 -0
  151. package/templates-zh-CN/reference/walkthrough-standard.md +4 -4
  152. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  153. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  154. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  155. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  156. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  157. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  158. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  159. package/templates-zh-CN/dashboard/index.html +0 -18
@@ -0,0 +1,755 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import {
6
+ repoRoot,
7
+ visualMapFile,
8
+ legacyVisualRoadmapFile,
9
+ lessonCandidatesFile,
10
+ longRunningTaskContractFile,
11
+ allowedTaskStates,
12
+ allowedTaskBudgets,
13
+ allowedPhaseStates,
14
+ allowedEvidenceStatus,
15
+ normalizeTarget,
16
+ normalizeLocale,
17
+ toPosix,
18
+ readFileSafe,
19
+ readBundledTemplate,
20
+ localizedTemplateSource,
21
+ todayDate,
22
+ localDate,
23
+ datePrefix,
24
+ nowTimestamp,
25
+ normalizeTaskId,
26
+ renderTaskTemplate,
27
+ } from "./core-shared.mjs";
28
+ import { verifyMigrationSession } from "./migration-planner.mjs";
29
+ import { readCapabilityRegistry } from "./capability-registry.mjs";
30
+ import {
31
+ collectTasks,
32
+ collectReviewRisks,
33
+ isBlockingReviewRisk,
34
+ listTaskPlanPaths,
35
+ parsePhases,
36
+ parseTaskBudget,
37
+ parseLessonCandidateStatus,
38
+ isLessonCandidateDecisionComplete,
39
+ parseReviewConfirmation,
40
+ readVisualMapContractFile,
41
+ taskIdForDirectory,
42
+ } from "./task-scanner.mjs";
43
+ import {
44
+ getColumn,
45
+ firstColumn,
46
+ updateMarkdownTableRow,
47
+ } from "./markdown-utils.mjs";
48
+
49
+ function taskTemplateFiles({ locale = "en-US" } = {}) {
50
+ return [
51
+ ["brief.md", "templates/planning/brief.md"],
52
+ ["task_plan.md", "templates/planning/task_plan.md"],
53
+ ["execution_strategy.md", "templates/planning/execution_strategy.md"],
54
+ [visualMapFile, "templates/planning/visual_map.md"],
55
+ ["findings.md", "templates/planning/findings.md"],
56
+ [lessonCandidatesFile, "templates/planning/lesson_candidates.md"],
57
+ ["progress.md", "templates/planning/progress.md"],
58
+ ["review.md", "templates/planning/review.md"],
59
+ ].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
60
+ }
61
+
62
+ function simpleTaskTemplateFiles({ locale = "en-US" } = {}) {
63
+ return [
64
+ ["brief.md", "templates/planning/brief.md"],
65
+ ["task_plan.md", "templates/planning/task_plan.md"],
66
+ [visualMapFile, "templates/planning/visual_map.md"],
67
+ ["progress.md", "templates/planning/progress.md"],
68
+ ].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
69
+ }
70
+
71
+ function optionalTaskTemplateFiles({ locale = "en-US" } = {}) {
72
+ return [
73
+ ["references/INDEX.md", "templates/planning/optional/references/INDEX.md"],
74
+ ["artifacts/INDEX.md", "templates/planning/optional/artifacts/INDEX.md"],
75
+ ].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
76
+ }
77
+
78
+ function moduleTemplateFiles({ locale = "en-US" } = {}) {
79
+ return [
80
+ ["brief.md", "templates/planning/module_brief.md"],
81
+ ["module_plan.md", "templates/planning/module_plan.md"],
82
+ ["execution_strategy.md", "templates/planning/execution_strategy.md"],
83
+ [visualMapFile, "templates/planning/visual_map.md"],
84
+ ["session_prompt.md", "templates/planning/module_session_prompt.md"],
85
+ ].map(([destination, source]) => [destination, localizedTemplateSource(source, locale)]);
86
+ }
87
+
88
+ function taskRoot(target, taskId, { moduleKey = "" } = {}) {
89
+ const normalizedTaskId = normalizeTaskId(taskId);
90
+ if (moduleKey) return path.join(target.docsRoot, "09-PLANNING/MODULES", normalizeTaskId(moduleKey), normalizedTaskId);
91
+ return path.join(target.docsRoot, "09-PLANNING/TASKS", normalizedTaskId);
92
+ }
93
+
94
+ function resolveTaskDirectory(target, taskRef) {
95
+ const raw = String(taskRef || "").replace(/^docs\/09-PLANNING\//, "").replace(/^\/+/, "");
96
+ if (!raw) throw new Error("Missing task id");
97
+ const direct = raw.startsWith("TASKS/") || raw.startsWith("MODULES/") ? path.join(target.docsRoot, "09-PLANNING", raw) : "";
98
+ if (direct && fs.existsSync(path.join(direct, "task_plan.md"))) return direct;
99
+ const normalized = normalizeTaskId(raw);
100
+ const candidates = listTaskPlanPaths(target)
101
+ .map((taskPlanPath) => path.dirname(taskPlanPath))
102
+ .filter((taskDir) => {
103
+ const id = taskIdForDirectory(target, taskDir);
104
+ const dirName = path.basename(taskDir);
105
+ return id === raw || id.endsWith(`/${raw}`) || dirName === normalized;
106
+ });
107
+ if (candidates.length === 1) return candidates[0];
108
+ if (candidates.length > 1) {
109
+ const options = candidates.map((taskDir) => `- ${taskIdForDirectory(target, taskDir)}`).join("\n");
110
+ throw new Error(`Ambiguous task reference: ${taskRef}\n${options}`);
111
+ }
112
+ // Try bare slug resolution: match normalized slug against dated directories
113
+ if (!datePrefix.test(normalized)) {
114
+ const datedCandidates = listTaskPlanPaths(target)
115
+ .map((taskPlanPath) => path.dirname(taskPlanPath))
116
+ .filter((taskDir) => {
117
+ const dirName = path.basename(taskDir);
118
+ return datePrefix.test(dirName) && dirName.replace(datePrefix, "") === normalized;
119
+ });
120
+ if (datedCandidates.length === 1) return datedCandidates[0];
121
+ if (datedCandidates.length > 1) {
122
+ const options = datedCandidates.map((taskDir) => `- ${taskIdForDirectory(target, taskDir)}`).join("\n");
123
+ throw new Error(`Ambiguous task reference: ${taskRef}\n${options}`);
124
+ }
125
+ }
126
+ const legacy = taskRoot(target, normalized);
127
+ if (fs.existsSync(path.join(legacy, "task_plan.md"))) return legacy;
128
+ throw new Error(`Task not found: ${taskRef}`);
129
+ }
130
+
131
+ function findTaskByDirectory(target, taskDir) {
132
+ const id = taskIdForDirectory(target, taskDir);
133
+ return collectTasks(target).find((task) => task.id === id) || null;
134
+ }
135
+
136
+ function stateLabel(state, locale) {
137
+ if (normalizeLocale(locale) !== "zh-CN") return state;
138
+ return (
139
+ {
140
+ not_started: "未开始",
141
+ planned: "未开始",
142
+ in_progress: "进行中",
143
+ review: "审查中",
144
+ blocked: "已阻塞",
145
+ done: "已完成",
146
+ }[state] || state
147
+ );
148
+ }
149
+
150
+ function normalizeTaskBudgetInput(budget) {
151
+ const normalized = String(budget || "standard").trim().toLowerCase().replaceAll("_", "-");
152
+ if (allowedTaskBudgets.has(normalized)) return normalized;
153
+ throw new Error(`Invalid task budget: ${budget}. Expected one of: simple, standard, complex`);
154
+ }
155
+
156
+ function normalizeTaskPresetInput(preset) {
157
+ const normalized = String(preset || "none").trim().toLowerCase().replaceAll("_", "-");
158
+ if (!normalized || normalized === "none") return "none";
159
+ if (normalized === "legacy-migration") return normalized;
160
+ throw new Error(`Invalid task preset: ${preset}. Expected one of: legacy-migration`);
161
+ }
162
+
163
+ function taskFilesForBudget({ budget, locale }) {
164
+ if (budget === "simple") return simpleTaskTemplateFiles({ locale });
165
+ if (budget === "complex") return [...taskTemplateFiles({ locale }), ...optionalTaskTemplateFiles({ locale })];
166
+ return taskTemplateFiles({ locale });
167
+ }
168
+
169
+ function appendLongRunningContractFile(files, { locale, longRunning }) {
170
+ if (!longRunning) return files;
171
+ return [...files, [longRunningTaskContractFile, localizedTemplateSource("templates/planning/long-running-task-contract.md", locale)]];
172
+ }
173
+
174
+ function validateLifecycleTransition({ event, currentState, budget, reviewContent = "" }) {
175
+ if (event === "task-review" && currentState !== "in_progress") {
176
+ throw new Error(`task-review requires current state in_progress; current state is ${currentState || "unknown"}`);
177
+ }
178
+ if (event === "task-complete" && budget !== "simple" && currentState !== "review") {
179
+ throw new Error(`task-complete for ${budget} tasks requires current state review. Run task-review first.`);
180
+ }
181
+ if (event === "task-complete" && budget !== "simple") {
182
+ const blockingRisks = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk);
183
+ if (blockingRisks.length > 0) {
184
+ const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
185
+ throw new Error(`Open blocking review findings must be closed before task-complete: ${ids}`);
186
+ }
187
+ if (!parseReviewConfirmation(reviewContent)?.confirmed) {
188
+ throw new Error("Human review must be confirmed before task-complete. Run review-confirm first.");
189
+ }
190
+ }
191
+ }
192
+
193
+ function validateReviewEntryGate(taskDir, budget) {
194
+ if (budget === "simple") return;
195
+ const candidatePath = path.join(taskDir, lessonCandidatesFile);
196
+ if (!fs.existsSync(candidatePath)) {
197
+ throw new Error(`task-review requires ${lessonCandidatesFile} before entering human review.`);
198
+ }
199
+ const phases = parsePhases(readVisualMapContractFile(taskDir).content);
200
+ const actionablePhases = phases.filter((phase) => phase.state !== "skipped");
201
+ const hasRecordedPhaseProgress = actionablePhases.some(
202
+ (phase) =>
203
+ phase.completion > 0 ||
204
+ ["in_progress", "review", "blocked", "done"].includes(phase.state) ||
205
+ ["partial", "present", "waived"].includes(phase.evidenceStatus),
206
+ );
207
+ if (actionablePhases.length > 0 && !hasRecordedPhaseProgress) {
208
+ throw new Error("task-review requires at least one Visual Map phase progress update. Run task-phase before entering human review.");
209
+ }
210
+ }
211
+
212
+ function validateHumanReviewConfirmation({ task, budget }) {
213
+ if (budget === "simple") return;
214
+ const state = task?.state || "unknown";
215
+ const lifecycle = task?.lifecycleState || "";
216
+ if (state !== "review" && !["in_review", "review-blocked"].includes(lifecycle)) {
217
+ throw new Error(`Human review confirmation requires current state review; current state is ${state}. Run task-review first.`);
218
+ }
219
+ if (!task?.walkthroughPath) {
220
+ throw new Error("Human review confirmation requires a walkthrough linked from Closeout SSoT before review-confirm.");
221
+ }
222
+ if (!task?.lessonCandidateDecisionComplete) {
223
+ const status = task?.lessonCandidateStatus || "missing";
224
+ throw new Error(`Human review confirmation requires lesson candidate decision complete; current status is ${status}.`);
225
+ }
226
+ }
227
+
228
+ function updateProgressState(content, state, locale) {
229
+ const label = stateLabel(state, locale);
230
+ if (/^##\s*状态[::][^\n]*/im.test(content)) {
231
+ return content.replace(/^##\s*状态[::][^\n]*/im, `## 状态:${label}`);
232
+ }
233
+ if (/^##\s*(?:Current Status|Status)\s*\n+\s*[^\n]+/im.test(content)) {
234
+ return content.replace(/^##\s*(Current Status|Status)\s*\n+\s*[^\n]+/im, `## $1\n\n${label}`);
235
+ }
236
+ return `${content.trimEnd()}\n\n## Status\n\n${label}\n`;
237
+ }
238
+
239
+ function appendProgressLog(content, { event, message, evidence, actor = "coordinator" }) {
240
+ const timestamp = nowTimestamp();
241
+ const safeMessage = String(message || event).replace(/\r?\n/g, " ").trim();
242
+ const safeEvidence = String(evidence || "n/a").replace(/\r?\n/g, " ").trim();
243
+ if (/^##\s*Log\s*$/im.test(content)) {
244
+ return content.replace(
245
+ /(^##\s*Log\s*$[\s\S]*?\| --- \| --- \| --- \| --- \| --- \|\n)/im,
246
+ `$1| ${timestamp} | ${actor} | ${event}: ${safeMessage} | ${safeEvidence} | ${event === "task-complete" ? "done" : "continue"} |\n`,
247
+ );
248
+ }
249
+ if (/^##\s*进度记录\s*$/im.test(content)) {
250
+ return `${content.trimEnd()}\n\n### [${timestamp}] - ${event}\n\n- 做了什么:${safeMessage}\n- 验证结果:已记录\n- 下一步:${event === "task-complete" ? "完成" : "继续执行"}\n- 证据:${safeEvidence}\n`;
251
+ }
252
+ return `${content.trimEnd()}\n\n## Log\n\n| Time | Actor | Action | Evidence | Next |\n| --- | --- | --- | --- | --- |\n| ${timestamp} | ${actor} | ${event}: ${safeMessage} | ${safeEvidence} | ${event === "task-complete" ? "done" : "continue"} |\n`;
253
+ }
254
+
255
+ function ensureDatePrefix(slug) {
256
+ if (datePrefix.test(slug)) return slug;
257
+ return `${localDate()}-${slug}`;
258
+ }
259
+
260
+ function bareSlug(datedId) {
261
+ if (datePrefix.test(datedId)) return datedId.replace(datePrefix, "");
262
+ return datedId;
263
+ }
264
+
265
+ export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "" } = {}) {
266
+ const normalizedPreset = normalizeTaskPresetInput(preset);
267
+ const migrationSession = fromSession ? readMigrationSession(fromSession) : null;
268
+ const target = migrationSession ? normalizeTarget(migrationSession.target) : normalizeTarget(targetInput);
269
+ if (migrationSession && targetInput && targetInput !== "." && path.resolve(targetInput) !== path.resolve(migrationSession.target)) {
270
+ throw new Error(`--from-session target mismatch: session target is ${migrationSession.target}`);
271
+ }
272
+ if (normalizedPreset !== "none" && normalizedPreset !== "legacy-migration") throw new Error(`Unsupported task preset: ${normalizedPreset}`);
273
+ if (normalizedPreset === "legacy-migration" && budget !== "complex") throw new Error("legacy-migration preset requires --budget complex");
274
+ if (normalizedPreset === "legacy-migration" && moduleKey) throw new Error("legacy-migration preset is project-level and cannot be combined with --module");
275
+ if (normalizedPreset === "legacy-migration" && !migrationSession) throw new Error("legacy-migration preset requires --from-session");
276
+ const rawNormalized = normalizeTaskId(taskId || (normalizedPreset === "legacy-migration" ? "harness-v1-migration" : ""));
277
+ const normalizedTaskId = ensureDatePrefix(rawNormalized);
278
+ if (!normalizedTaskId) throw new Error("Missing task id");
279
+ const semanticSlug = bareSlug(normalizedTaskId);
280
+ const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
281
+ const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);
282
+ const normalizedBudget = normalizeTaskBudgetInput(budget);
283
+ const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);
284
+ const directory = taskRoot(target, normalizedTaskId, { moduleKey: normalizedModuleKey });
285
+ if (fs.existsSync(directory)) throw new Error(`Task already exists: ${normalizedTaskId}`);
286
+ const presetContext = normalizedPreset === "legacy-migration"
287
+ ? legacyMigrationPresetContext({ target, taskDir: directory, taskId: normalizedTaskId, session: migrationSession })
288
+ : null;
289
+ const changes = [];
290
+ if (normalizedModuleKey) {
291
+ const moduleDirectory = path.dirname(directory);
292
+ for (const [destination, source] of moduleTemplateFiles({ locale: normalizedLocale })) {
293
+ const destinationPath = path.join(moduleDirectory, destination);
294
+ if (fs.existsSync(destinationPath)) continue;
295
+ const sourcePath = path.join(repoRoot, source);
296
+ changes.push({
297
+ destination: toPosix(path.relative(target.projectRoot, destinationPath)),
298
+ source,
299
+ action: dryRun ? "would-create" : "create",
300
+ });
301
+ if (dryRun) continue;
302
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
303
+ fs.writeFileSync(
304
+ destinationPath,
305
+ renderTaskTemplate(readBundledTemplate(source), {
306
+ taskId: normalizedModuleKey,
307
+ title: normalizedModuleKey,
308
+ locale: normalizedLocale,
309
+ budget: normalizedBudget,
310
+ }),
311
+ );
312
+ }
313
+ }
314
+ const files = appendLongRunningContractFile(taskFilesForBudget({ budget: normalizedBudget, locale: normalizedLocale }), {
315
+ locale: normalizedLocale,
316
+ longRunning,
317
+ });
318
+ for (const [destination, source] of files) {
319
+ const destinationPath = path.join(directory, destination);
320
+ const sourcePath = path.join(repoRoot, source);
321
+ changes.push({
322
+ destination: toPosix(path.relative(target.projectRoot, destinationPath)),
323
+ source,
324
+ action: dryRun ? "would-create" : "create",
325
+ });
326
+ if (dryRun) continue;
327
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
328
+ fs.writeFileSync(
329
+ destinationPath,
330
+ renderPresetTaskTemplate(destination, renderTaskTemplate(readBundledTemplate(source), {
331
+ taskId: normalizedTaskId,
332
+ title: taskTitle,
333
+ locale: normalizedLocale,
334
+ budget: normalizedBudget,
335
+ }), presetContext),
336
+ );
337
+ }
338
+ if (presetContext) {
339
+ for (const evidence of presetContext.evidenceFiles) {
340
+ const destinationPath = path.join(target.projectRoot, evidence.relativePath);
341
+ changes.push({
342
+ destination: toPosix(evidence.relativePath),
343
+ source: evidence.source,
344
+ action: dryRun ? "would-create" : "create",
345
+ });
346
+ if (dryRun) continue;
347
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
348
+ fs.writeFileSync(destinationPath, evidence.content);
349
+ }
350
+ }
351
+ return {
352
+ dryRun,
353
+ task: {
354
+ id: taskIdForDirectory(target, directory),
355
+ shortId: normalizedTaskId,
356
+ title: taskTitle,
357
+ module: normalizedModuleKey || null,
358
+ path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
359
+ locale: normalizedLocale,
360
+ budget: normalizedBudget,
361
+ kind: presetContext?.kind || "general",
362
+ preset: normalizedPreset,
363
+ presetVersion: presetContext?.presetVersion || "",
364
+ migrationTargetLevel: presetContext?.migrationTargetLevel || "",
365
+ migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
366
+ evidenceBundle: presetContext?.evidenceBundle || "",
367
+ longRunning,
368
+ },
369
+ changes,
370
+ };
371
+ }
372
+
373
+ function readMigrationSession(fromSession) {
374
+ const sessionPath = path.resolve(fromSession || "");
375
+ if (!sessionPath || !fs.existsSync(sessionPath)) throw new Error(`Migration session not found: ${fromSession}`);
376
+ let session;
377
+ try {
378
+ session = JSON.parse(fs.readFileSync(sessionPath, "utf8"));
379
+ } catch (error) {
380
+ throw new Error(`Invalid migration session JSON: ${error.message}`);
381
+ }
382
+ if (session.operation !== "migrate-run") throw new Error("legacy-migration preset requires a migrate-run session");
383
+ if (session.planOnly) throw new Error("legacy-migration preset cannot use plan-only session evidence");
384
+ if (!session.target || !fs.existsSync(session.target)) throw new Error(`Migration session target missing: ${session.target || "(none)"}`);
385
+ return { ...session, sourcePath: sessionPath };
386
+ }
387
+
388
+ function legacyMigrationPresetContext({ target, taskDir, taskId, session }) {
389
+ const stamp = String(session.generatedAt || new Date().toISOString()).replace(/[^0-9A-Za-z-]+/g, "-").replace(/-+$/g, "");
390
+ const evidenceBundle = toPosix(path.relative(target.projectRoot, path.join(taskDir, "evidence", stamp || "session")));
391
+ const targetLevel = "migration-baseline";
392
+ const achievedLevel = session.strictDeferred ? "migration-deferred" : session.result === "complete" ? "migration-full-cutover" : "migration-baseline";
393
+ const verifyResult = verifyMigrationSession(session.sourcePath, { fullCutover: false });
394
+ return {
395
+ kind: "project-migration",
396
+ preset: "legacy-migration",
397
+ presetVersion: "v1",
398
+ migrationTargetLevel: targetLevel,
399
+ migrationAchievedLevel: achievedLevel,
400
+ evidenceBundle,
401
+ session,
402
+ evidenceFiles: legacyMigrationEvidenceFiles({ target, session, evidenceBundle, verifyResult }),
403
+ };
404
+ }
405
+
406
+ function legacyMigrationEvidenceFiles({ target, session, evidenceBundle, verifyResult }) {
407
+ const files = [];
408
+ const addJson = (name, value, source = "session") => files.push({
409
+ relativePath: path.join(evidenceBundle, name),
410
+ source,
411
+ content: `${JSON.stringify(value, null, 2)}\n`,
412
+ });
413
+ const addText = (name, value, source = "generated") => files.push({
414
+ relativePath: path.join(evidenceBundle, name),
415
+ source,
416
+ content: `${String(value || "").trim()}\n`,
417
+ });
418
+ addJson("session.json", session, "session.json");
419
+ addJson("migrate-plan.json", session.plan || {}, "migrate-plan.json");
420
+ addJson("normal-check.json", session.checks?.normal || {}, "session.checks.normal");
421
+ addJson("strict-check.json", session.checks?.strict || {}, "session.checks.strict");
422
+ addJson("migrate-verify.json", verifyResult, "migrate-verify");
423
+ addText("dashboard.hash.txt", dashboardHash(session.dashboard?.indexPath || ""), "dashboard");
424
+ addText("target-git-status.txt", JSON.stringify(session.git?.after || {}, null, 2), "session.git.after");
425
+ addText("target-commit.txt", targetCommit(target.projectRoot), "git");
426
+ addText("harness-version.txt", packageVersion(), "package.json");
427
+ addText("generated-at.txt", new Date().toISOString(), "generated");
428
+ return files;
429
+ }
430
+
431
+ function dashboardHash(indexPath) {
432
+ if (!indexPath || !fs.existsSync(indexPath)) return "missing";
433
+ const hash = crypto.createHash("sha256").update(fs.readFileSync(indexPath)).digest("hex");
434
+ return `sha256:${hash}`;
435
+ }
436
+
437
+ function targetCommit(projectRoot) {
438
+ const result = spawnSync("git", ["-C", projectRoot, "rev-parse", "HEAD"], { encoding: "utf8" });
439
+ return result.status === 0 ? result.stdout.trim() : "n/a";
440
+ }
441
+
442
+ function packageVersion() {
443
+ try {
444
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "unknown";
445
+ } catch {
446
+ return "unknown";
447
+ }
448
+ }
449
+
450
+ function renderPresetTaskTemplate(destination, content, presetContext) {
451
+ if (!presetContext) return content;
452
+ if (destination === "task_plan.md") return renderLegacyMigrationTaskPlan(content, presetContext);
453
+ if (destination === "execution_strategy.md") return `${content.trimEnd()}\n\n${legacyMigrationExecutionStrategy(presetContext)}\n`;
454
+ if (destination === "findings.md") return `${content.trimEnd()}\n\n${legacyMigrationFindings(presetContext)}\n`;
455
+ if (destination === "review.md") return `${content.trimEnd()}\n\n${legacyMigrationReview(presetContext)}\n`;
456
+ if (destination === visualMapFile) return `${content.trimEnd()}\n\n${legacyMigrationVisualMap()}\n`;
457
+ return content;
458
+ }
459
+
460
+ function renderLegacyMigrationTaskPlan(content, context) {
461
+ const metadata = [
462
+ `Selected budget: complex`,
463
+ `Task Kind: ${context.kind}`,
464
+ `Task Preset: ${context.preset}`,
465
+ `Preset Version: ${context.presetVersion}`,
466
+ `Migration Target Level: ${context.migrationTargetLevel}`,
467
+ `Migration Achieved Level: ${context.migrationAchievedLevel}`,
468
+ `Evidence Bundle: ${context.evidenceBundle}`,
469
+ ].join("\n");
470
+ let next = String(content).replace(/^(Task Contract:\s*harness-task\/v1\s*)$/im, `$1\n${metadata}`);
471
+ next = next.replace("[State the outcome this task must deliver in one sentence.]", "Create a controlled Harness v1 migration task from the recorded migrate-run session without rewriting history automatically.");
472
+ next = next.replace("[用一句话说明本任务完成后应达到的状态。]", "基于已记录的 migrate-run session 创建受控的 Harness v1 迁移任务,不自动改写历史材料。");
473
+ return `${next.trimEnd()}\n\n## Legacy Migration Preset\n\nThis Complex Task uses the legacy-migration preset. The preset only scaffolds the task and records evidence. It does not run migration, rewrite historical task bodies, stage files, or commit changes.\n\n- Baseline session: \`${context.evidenceBundle}/session.json\`\n- Migration plan: \`${context.evidenceBundle}/migrate-plan.json\`\n- Strict deferred: ${context.session.strictDeferred ? "yes" : "no"}\n- Full-cutover claim allowed now: ${context.migrationAchievedLevel === "migration-full-cutover" ? "yes" : "no"}\n`;
474
+ }
475
+
476
+ function legacyMigrationExecutionStrategy(context) {
477
+ return `## Legacy Migration Preset Strategy
478
+
479
+ This preset keeps migration inside the Complex Task contract.
480
+
481
+ | Area | Rule |
482
+ | --- | --- |
483
+ | Write boundary | Do not rewrite historical task bodies unless the user explicitly confirms that phase. |
484
+ | Evidence source | Use \`${context.evidenceBundle}/\` as the handoff bundle. Absolute session paths are origin data only. |
485
+ | Target level | \`${context.migrationTargetLevel}\` |
486
+ | Achieved level | \`${context.migrationAchievedLevel}\` |
487
+
488
+ ## Subagent Lane Table
489
+
490
+ Declare lanes before dispatching workers.
491
+
492
+ | Lane ID | Allowed globs | Forbidden globs | Shared file owner | Worktree / branch | Handoff path | Merge order | Verification command |
493
+ | --- | --- | --- | --- | --- | --- | --- | --- |
494
+ | coordinator | docs/09-PLANNING/TASKS/** | AGENTS.md, CLAUDE.md, docs/Harness-Ledger.md until closeout | coordinator | current | progress.md | 1 | harness check --profile target-project . |
495
+ `;
496
+ }
497
+
498
+ function legacyMigrationFindings(context) {
499
+ return `## Legacy Migration Action Buckets
500
+
501
+ | Bucket | Count | Owner | Status | Next Action |
502
+ | --- | ---: | --- | --- | --- |
503
+ | warnings | ${context.session.plan?.summary?.warnings || 0} | coordinator | open | Triage before increasing target level |
504
+ | taskActions | ${context.session.plan?.summary?.taskActions || 0} | coordinator | open | Upgrade only current/reopened/current-evidence tasks |
505
+ | legacyResiduals | ${context.session.plan?.summary?.legacyResiduals || 0} | coordinator | open | Assign real owner before full cutover |
506
+
507
+ ## Residual Policy
508
+
509
+ Residuals require reason, owner, trigger, next action, and reviewer. Placeholder owner \`migration-owner\` is not a real owner.
510
+
511
+ ## Status Conflict Table
512
+
513
+ | Item | Competing Evidence | Chosen Classification | Confidence | Human Needed |
514
+ | --- | --- | --- | --- | --- |
515
+ | pending | session / SSoT / progress / git | pending | medium | yes |
516
+ `;
517
+ }
518
+
519
+ function legacyMigrationReview(context) {
520
+ return `## Legacy Migration Preset Gate
521
+
522
+ \`migration-full-cutover\` can only be claimed when the final session proves all gates:
523
+
524
+ - final session result is \`complete\`
525
+ - strict check passes
526
+ - \`migrate-verify --full-cutover\` passes
527
+ - warnings/actions/residuals/strictDeferred are zero
528
+ - dashboard evidence is readable
529
+ - review has no open P0/P1/P2 blocker
530
+
531
+ Current achieved level: \`${context.migrationAchievedLevel}\`.
532
+ `;
533
+ }
534
+
535
+ function legacyMigrationVisualMap() {
536
+ return `## Legacy Migration Preset Flow
537
+
538
+ \`\`\`mermaid
539
+ flowchart TD
540
+ A["Recorded migrate-run session"] --> B["Create Complex Task preset"]
541
+ B --> C["Baseline usable"]
542
+ C --> D{"User confirms deeper cutover?"}
543
+ D -- no --> E["Keep residuals owned"]
544
+ D -- yes --> F["Current work cutover"]
545
+ F --> G["Historical consolidation"]
546
+ G --> H["Strict / full-cutover verify"]
547
+ \`\`\`
548
+ `;
549
+ }
550
+
551
+ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {
552
+ const target = normalizeTarget(targetInput);
553
+ const taskDir = resolveTaskDirectory(target, taskId);
554
+ const progressPath = path.join(taskDir, "progress.md");
555
+ const registry = readCapabilityRegistry(target);
556
+ const normalizedState = state ? String(state).toLowerCase().replaceAll("-", "_") : "";
557
+ if (normalizedState && !allowedTaskStates.has(normalizedState)) throw new Error(`Invalid task state: ${state}`);
558
+ const currentTask = findTaskByDirectory(target, taskDir);
559
+ const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
560
+ validateLifecycleTransition({
561
+ event,
562
+ currentState: currentTask?.state || "unknown",
563
+ budget,
564
+ reviewContent: readFileSafe(path.join(taskDir, "review.md")),
565
+ });
566
+ if (event === "task-review") validateReviewEntryGate(taskDir, budget);
567
+ let content = readFileSafe(progressPath);
568
+ if (normalizedState) content = updateProgressState(content, normalizedState, registry.locale);
569
+ content = appendProgressLog(content, { event, message, evidence });
570
+ fs.writeFileSync(progressPath, content.endsWith("\n") ? content : `${content}\n`);
571
+ return {
572
+ event,
573
+ task: findTaskByDirectory(target, taskDir) || { id: taskIdForDirectory(target, taskDir), state: normalizedState || "unknown" },
574
+ };
575
+ }
576
+
577
+ export function confirmTaskReview(targetInput, taskId, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {
578
+ const target = normalizeTarget(targetInput);
579
+ const taskDir = resolveTaskDirectory(target, taskId);
580
+ assertTaskDirectoryInsidePlanning(target, taskDir);
581
+ const canonicalTaskId = taskIdForDirectory(target, taskDir);
582
+ const shortId = path.basename(taskDir);
583
+ if (confirmText && ![shortId, canonicalTaskId].includes(confirmText)) {
584
+ throw new Error(`Review confirmation text must match task id: ${shortId}`);
585
+ }
586
+ if (!confirmText) throw new Error(`Missing review confirmation text: ${shortId}`);
587
+
588
+ const reviewPath = path.join(taskDir, "review.md");
589
+ const progressPath = path.join(taskDir, "progress.md");
590
+ const reviewContent = readFileSafe(reviewPath);
591
+ const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
592
+ const candidateStatus = parseLessonCandidateStatus(readFileSafe(path.join(taskDir, lessonCandidatesFile)));
593
+ const blockingRisks = collectReviewRisks(reviewContent).filter(isBlockingReviewRisk);
594
+ if (blockingRisks.length > 0) {
595
+ const ids = blockingRisks.map((risk) => risk.id || risk.severity).join(", ");
596
+ throw new Error(`Open blocking review findings must be closed before confirmation: ${ids}`);
597
+ }
598
+ validateHumanReviewConfirmation({
599
+ task: findTaskByDirectory(target, taskDir),
600
+ budget,
601
+ });
602
+ if (budget !== "simple" && !isLessonCandidateDecisionComplete(candidateStatus)) {
603
+ throw new Error(`Human review confirmation requires lesson candidate decision complete; current status is ${candidateStatus.status}.`);
604
+ }
605
+
606
+ const timestamp = nowTimestamp();
607
+ const safeReviewer = markdownCell(reviewer || "Human Reviewer");
608
+ const safeMessage = markdownCell(message || "Human review confirmed");
609
+ const safeEvidence = markdownCell(evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`);
610
+ const confirmationBlock = [
611
+ "## Human Review Confirmation",
612
+ "",
613
+ `Reviewer: ${safeReviewer}`,
614
+ "",
615
+ "| Confirmed At | Reviewer | Message | Evidence |",
616
+ "| --- | --- | --- | --- |",
617
+ `| ${timestamp} | ${safeReviewer} | ${safeMessage} | ${safeEvidence} |`,
618
+ "",
619
+ ].join("\n");
620
+ const nextReview = replaceReviewConfirmation(reviewContent, confirmationBlock);
621
+ fs.writeFileSync(reviewPath, nextReview.endsWith("\n") ? nextReview : `${nextReview}\n`);
622
+
623
+ let progressContent = readFileSafe(progressPath);
624
+ progressContent = appendProgressLog(progressContent, {
625
+ event: "review-confirm",
626
+ message: message || `Human review confirmed by ${reviewer}`,
627
+ evidence: evidence || `TARGET:docs/09-PLANNING/${canonicalTaskId}/review.md`,
628
+ actor: reviewer || "Human Reviewer",
629
+ });
630
+ fs.writeFileSync(progressPath, progressContent.endsWith("\n") ? progressContent : `${progressContent}\n`);
631
+
632
+ return {
633
+ event: "review-confirm",
634
+ task: findTaskByDirectory(target, taskDir) || { id: canonicalTaskId, reviewStatus: "confirmed" },
635
+ };
636
+ }
637
+
638
+ function assertTaskDirectoryInsidePlanning(target, taskDir) {
639
+ const realTaskDir = fs.realpathSync(taskDir);
640
+ const allowedRoots = [
641
+ path.join(target.docsRoot, "09-PLANNING/TASKS"),
642
+ path.join(target.docsRoot, "09-PLANNING/MODULES"),
643
+ ].filter(fs.existsSync).map((root) => fs.realpathSync(root));
644
+ if (!allowedRoots.some((root) => realTaskDir === root || realTaskDir.startsWith(`${root}${path.sep}`))) {
645
+ throw new Error(`Task directory outside planning root: ${taskIdForDirectory(target, taskDir)}`);
646
+ }
647
+ }
648
+
649
+ function markdownCell(value) {
650
+ return String(value || "")
651
+ .replace(/\r?\n/g, " ")
652
+ .replaceAll("|", "\\|")
653
+ .trim();
654
+ }
655
+
656
+ function replaceReviewConfirmation(content, block) {
657
+ const trimmed = String(content || "").trimEnd();
658
+ if (/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$/im.test(trimmed)) {
659
+ return trimmed.replace(/^##\s*(?:Human Review Confirmation|人工审查确认)\s*$[\s\S]*?(?=^##\s+|\s*$)/im, block.trimEnd());
660
+ }
661
+ return `${trimmed}\n\n${block}`;
662
+ }
663
+
664
+ export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {
665
+ const target = normalizeTarget(targetInput);
666
+ const taskDir = resolveTaskDirectory(target, taskId);
667
+ const visualMapPath = path.join(taskDir, visualMapFile);
668
+ const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
669
+ if (!fs.existsSync(visualMapPath)) {
670
+ if (fs.existsSync(legacyPath)) throw new Error(`Task has legacy visual_roadmap.md only; rewrite it to visual_map.md before task-phase: ${taskId}`);
671
+ throw new Error(`Task visual map not found: ${taskId}`);
672
+ }
673
+ let content = readFileSafe(visualMapPath);
674
+ const normalizedState = state ? String(state).toLowerCase().replaceAll("-", "_") : "";
675
+ if (normalizedState && !allowedPhaseStates.has(normalizedState)) throw new Error(`Invalid phase state: ${state}`);
676
+ const normalizedEvidence = evidenceStatus ? String(evidenceStatus).toLowerCase() : "";
677
+ if (normalizedEvidence && !allowedEvidenceStatus.has(normalizedEvidence)) throw new Error(`Invalid evidence status: ${evidenceStatus}`);
678
+ const nextCompletion = completion === "" ? "" : Number.parseInt(String(completion), 10);
679
+ if (nextCompletion !== "" && (!Number.isInteger(nextCompletion) || nextCompletion < 0 || nextCompletion > 100)) {
680
+ throw new Error(`Invalid completion: ${completion}`);
681
+ }
682
+ const phaseUpdate = updateMarkdownTableRow(content, /^Phase ID$/i, (header, row) => {
683
+ const idIndex = getColumn(header, "Phase ID");
684
+ if ((row[idIndex] || "") !== phaseId) return null;
685
+ const next = [...row];
686
+ const stateIndex = getColumn(header, "State");
687
+ const completionIndex = getColumn(header, "Completion");
688
+ const evidenceIndex = getColumn(header, "Evidence Status");
689
+ if (normalizedState && stateIndex >= 0) next[stateIndex] = normalizedState;
690
+ if (nextCompletion !== "" && completionIndex >= 0) next[completionIndex] = String(nextCompletion);
691
+ if (normalizedEvidence && evidenceIndex >= 0) next[evidenceIndex] = normalizedEvidence;
692
+ return next;
693
+ });
694
+ if (!phaseUpdate.matched) throw new Error(`Phase not found: ${phaseId}`);
695
+ content = phaseUpdate.content;
696
+ fs.writeFileSync(visualMapPath, content);
697
+ return { event: "task-phase", task: findTaskByDirectory(target, taskDir), phaseId };
698
+ }
699
+
700
+ export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" } = {}) {
701
+ const target = normalizeTarget(targetInput);
702
+ const normalizedModuleKey = normalizeTaskId(moduleKey);
703
+ const normalizedState = String(state || "done").toLowerCase().replaceAll("_", "-");
704
+ if (!["planned", "in-progress", "done", "blocked", "superseded"].includes(normalizedState)) throw new Error(`Invalid module step state: ${state}`);
705
+ const modulePlanPath = path.join(target.docsRoot, "09-PLANNING/MODULES", normalizedModuleKey, "module_plan.md");
706
+ if (!fs.existsSync(modulePlanPath)) throw new Error(`Module plan not found: ${normalizedModuleKey}`);
707
+ let content = readFileSafe(modulePlanPath);
708
+ const stepUpdate = updateMarkdownTableRow(content, /^(Step ID|步骤 ID)$/i, (header, row) => {
709
+ const idIndex = firstColumn(header, ["Step ID", "步骤 ID"]);
710
+ if ((row[idIndex] || "") !== stepId) return null;
711
+ const next = [...row];
712
+ const statusIndex = firstColumn(header, ["Status", "状态"]);
713
+ if (statusIndex >= 0) next[statusIndex] = normalizedState;
714
+ return next;
715
+ });
716
+ if (!stepUpdate.matched) throw new Error(`Module step not found: ${stepId}`);
717
+ content = stepUpdate.content;
718
+ fs.writeFileSync(modulePlanPath, content);
719
+
720
+ const registryPath = path.join(target.docsRoot, "09-PLANNING/Module-Registry.md");
721
+ if (fs.existsSync(registryPath)) {
722
+ let registry = readFileSafe(registryPath);
723
+ const registryUpdate = updateMarkdownTableRow(registry, /^(ID|模块 Key)$/i, (header, row) => {
724
+ const moduleIndex = firstColumn(header, ["Module", "模块", "模块 Key"]);
725
+ const taskPlanIndex = getColumn(header, "Task Plan");
726
+ const matchesModule = normalizeTaskId(row[moduleIndex] || "") === normalizedModuleKey;
727
+ const matchesPlan = taskPlanIndex >= 0 && String(row[taskPlanIndex] || "").includes(`/MODULES/${normalizedModuleKey}/`);
728
+ if (!matchesModule && !matchesPlan) return null;
729
+ const next = [...row];
730
+ const statusIndex = firstColumn(header, ["Status", "状态"]);
731
+ const updatedIndex = firstColumn(header, ["Updated", "更新时间"]);
732
+ const currentStepIndex = firstColumn(header, ["Current Step", "当前步骤"]);
733
+ const chineseRegistry = header.some((cell) => /模块 Key|模块名称|状态|更新时间/.test(cell));
734
+ if (statusIndex >= 0) {
735
+ next[statusIndex] = normalizedState === "done"
736
+ ? chineseRegistry ? "completed" : "merged"
737
+ : normalizedState === "in-progress" ? chineseRegistry ? "in-progress" : "active" : normalizedState;
738
+ }
739
+ if (currentStepIndex >= 0) next[currentStepIndex] = stepId;
740
+ if (updatedIndex >= 0) next[updatedIndex] = todayDate();
741
+ return next;
742
+ });
743
+ registry = registryUpdate.content;
744
+ fs.writeFileSync(registryPath, registry);
745
+ }
746
+ return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState };
747
+ }
748
+
749
+ export function listLifecycleTasks(targetInput, { state = "", moduleKey = "" } = {}) {
750
+ const target = normalizeTarget(targetInput);
751
+ let tasks = collectTasks(target);
752
+ if (state) tasks = tasks.filter((task) => task.state === String(state).toLowerCase().replaceAll("-", "_"));
753
+ if (moduleKey) tasks = tasks.filter((task) => task.module === normalizeTaskId(moduleKey));
754
+ return { tasks };
755
+ }