coding-agent-harness 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.en-US.md +14 -0
  4. package/README.md +230 -80
  5. package/README.zh-CN.md +290 -0
  6. package/SKILL.md +132 -198
  7. package/docs-release/README.md +80 -9
  8. package/docs-release/architecture/overview.md +298 -28
  9. package/docs-release/architecture/overview.zh-CN.md +292 -0
  10. package/docs-release/assets/dashboard-overview.png +0 -0
  11. package/docs-release/assets/harness-architecture.svg +163 -0
  12. package/docs-release/assets/harness-workflow.svg +64 -0
  13. package/docs-release/guides/agent-installation.en-US.md +237 -0
  14. package/docs-release/guides/agent-installation.md +149 -27
  15. package/docs-release/guides/contributing.md +100 -0
  16. package/docs-release/guides/contributing.zh-CN.md +99 -0
  17. package/docs-release/guides/document-audience-and-surfaces.en-US.md +113 -0
  18. package/docs-release/guides/document-audience-and-surfaces.md +113 -0
  19. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +334 -0
  20. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +334 -0
  21. package/docs-release/guides/legacy-migration-agent-prompt.md +373 -0
  22. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +350 -0
  23. package/docs-release/guides/migration-playbook.en-US.md +324 -0
  24. package/docs-release/guides/migration-playbook.md +328 -0
  25. package/docs-release/guides/parent-control-repository-pattern.en-US.md +254 -0
  26. package/docs-release/guides/parent-control-repository-pattern.md +254 -0
  27. package/docs-release/guides/preset-development.md +214 -0
  28. package/docs-release/guides/repository-operating-models.en-US.md +197 -0
  29. package/docs-release/guides/repository-operating-models.md +197 -0
  30. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  31. package/docs-release/guides/task-state-machine.md +214 -0
  32. package/docs-release/intl/README.md +15 -0
  33. package/docs-release/intl/de-DE.md +18 -0
  34. package/docs-release/intl/en-US.md +18 -0
  35. package/docs-release/intl/es-ES.md +18 -0
  36. package/docs-release/intl/fr-FR.md +18 -0
  37. package/docs-release/intl/ja-JP.md +18 -0
  38. package/docs-release/intl/ko-KR.md +18 -0
  39. package/docs-release/intl/zh-CN.md +18 -0
  40. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/brief.md +13 -0
  41. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  42. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/lesson_candidates.md +24 -0
  43. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/progress.md +1 -1
  44. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/task_plan.md +4 -2
  45. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/{visual_roadmap.md → visual_map.md} +9 -1
  46. package/package.json +10 -3
  47. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  48. package/presets/legacy-migration/preset.yaml +134 -0
  49. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  50. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  51. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  52. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  53. package/presets/legacy-migration/templates/review.seed.md +12 -0
  54. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  55. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  56. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  57. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  58. package/presets/lesson-sedimentation/preset.yaml +23 -0
  59. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  60. package/presets/module/preset.yaml +25 -0
  61. package/presets/module/templates/execution_strategy.append.md +8 -0
  62. package/presets/module/templates/task_plan.append.md +17 -0
  63. package/presets/standard-task/preset.yaml +31 -0
  64. package/presets/standard-task/templates/task_plan.append.md +7 -0
  65. package/references/adversarial-review-standard.md +2 -2
  66. package/references/agents-md-pattern.md +5 -5
  67. package/references/delivery-operating-model-standard.md +3 -3
  68. package/references/docs-directory-standard.md +53 -10
  69. package/references/external-source-intake-standard.md +75 -0
  70. package/references/harness-ledger.md +53 -94
  71. package/references/legacy-12-phase-bootstrap.md +41 -0
  72. package/references/lessons-governance.md +100 -88
  73. package/references/module-parallel-standard.md +14 -14
  74. package/references/planning-loop.md +51 -7
  75. package/references/project-onboarding-audit.md +10 -0
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +12 -1
  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/testing-standard.md +50 -0
  82. package/references/walkthrough-closeout.md +10 -9
  83. package/scripts/check-harness.mjs +111 -331
  84. package/scripts/commands/dashboard-command.mjs +67 -0
  85. package/scripts/commands/migration-command.mjs +96 -0
  86. package/scripts/commands/preset-command.mjs +73 -0
  87. package/scripts/commands/task-command.mjs +327 -0
  88. package/scripts/harness.mjs +106 -20
  89. package/scripts/lib/capability-registry.mjs +591 -0
  90. package/scripts/lib/check-module-parallel.mjs +237 -0
  91. package/scripts/lib/check-profiles.mjs +418 -0
  92. package/scripts/lib/check-task-contracts.mjs +47 -0
  93. package/scripts/lib/core-shared.mjs +196 -0
  94. package/scripts/lib/dashboard-data.mjs +412 -0
  95. package/scripts/lib/dashboard-workbench.mjs +257 -0
  96. package/scripts/lib/dashboard-writer.mjs +107 -4
  97. package/scripts/lib/git-status-summary.mjs +46 -0
  98. package/scripts/lib/governance-index-generator.mjs +174 -0
  99. package/scripts/lib/governance-sync.mjs +514 -0
  100. package/scripts/lib/governance-table-boundary.mjs +175 -0
  101. package/scripts/lib/harness-core.mjs +15 -1318
  102. package/scripts/lib/lesson-maintenance.mjs +152 -0
  103. package/scripts/lib/markdown-utils.mjs +158 -0
  104. package/scripts/lib/migration-planner.mjs +478 -0
  105. package/scripts/lib/migration-support.mjs +312 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +497 -0
  108. package/scripts/lib/preset-registry.mjs +627 -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-dashboard-renderer.mjs +102 -0
  112. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  113. package/scripts/lib/task-completion-consistency.mjs +16 -0
  114. package/scripts/lib/task-index.mjs +93 -0
  115. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  116. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  117. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  118. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  119. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  120. package/scripts/lib/task-lifecycle.mjs +649 -0
  121. package/scripts/lib/task-review-model.mjs +469 -0
  122. package/scripts/lib/task-scanner.mjs +576 -0
  123. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  124. package/scripts/postinstall.mjs +14 -0
  125. package/skills/preset-creator/SKILL.md +179 -0
  126. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  127. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  128. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  129. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  130. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  131. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  132. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  133. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  134. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  137. package/{templates/planning/visual_roadmap.md → skills/preset-creator/references/complex-task-skeleton/visual_map.md} +24 -2
  138. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  139. package/templates/AGENTS.md.template +51 -36
  140. package/templates/architecture/Architecture-SSoT.md +21 -0
  141. package/templates/architecture/README.md +49 -0
  142. package/templates/architecture/critical-flows.md +22 -0
  143. package/templates/architecture/local-repo-context.md +20 -0
  144. package/templates/architecture/service-catalog.md +17 -0
  145. package/templates/architecture/services/service-template.md +31 -0
  146. package/templates/architecture/system-map.md +22 -0
  147. package/templates/dashboard/assets/app-src/00-state.js +42 -0
  148. package/templates/dashboard/assets/app-src/10-router.js +77 -0
  149. package/templates/dashboard/assets/app-src/20-overview.js +241 -0
  150. package/templates/dashboard/assets/app-src/30-tasks.js +409 -0
  151. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  152. package/templates/dashboard/assets/app-src/40-modules.js +58 -0
  153. package/templates/dashboard/assets/app-src/45-review.js +347 -0
  154. package/templates/dashboard/assets/app-src/50-migration.js +183 -0
  155. package/templates/dashboard/assets/app-src/60-shared.js +61 -0
  156. package/templates/dashboard/assets/app-src/90-bindings.js +524 -0
  157. package/templates/dashboard/assets/app.css +3107 -300
  158. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  159. package/templates/dashboard/assets/app.js +2068 -306
  160. package/templates/dashboard/assets/app.manifest.json +12 -0
  161. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  162. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  163. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  164. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  165. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  166. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  167. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  168. package/templates/dashboard/assets/i18n.js +531 -44
  169. package/templates/dashboard/assets/mermaid-renderer.js +58 -8
  170. package/templates/development/README.md +52 -0
  171. package/templates/development/codebase-map.md +11 -0
  172. package/templates/development/cross-repo-debugging.md +18 -0
  173. package/templates/development/external-context/service-template.md +33 -0
  174. package/templates/development/external-source-packs/README.md +24 -0
  175. package/templates/development/external-source-packs/digest-template.md +28 -0
  176. package/templates/development/local-setup.md +16 -0
  177. package/templates/development/stubs-and-mocks.md +11 -0
  178. package/templates/integrations/README.md +40 -0
  179. package/templates/integrations/api-contract.md +42 -0
  180. package/templates/integrations/event-contract.md +46 -0
  181. package/templates/integrations/third-party/vendor-template.md +42 -0
  182. package/templates/integrations/webhook-contract.md +41 -0
  183. package/templates/ledger/Harness-Ledger.md +13 -25
  184. package/templates/lessons/lesson-arch-process-change.md +1 -1
  185. package/templates/lessons/lesson-new-doc.md +1 -1
  186. package/templates/lessons/lesson-ref-change.md +1 -1
  187. package/templates/planning/brief.md +32 -0
  188. package/templates/planning/execution_strategy.md +31 -0
  189. package/templates/planning/lesson_candidates.md +70 -0
  190. package/templates/planning/long-running-task-contract.md +7 -0
  191. package/templates/planning/module_brief.md +25 -0
  192. package/templates/planning/module_session_prompt.md +6 -0
  193. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  194. package/templates/planning/optional/references/INDEX.md +3 -3
  195. package/templates/planning/review.md +59 -0
  196. package/templates/planning/task_plan.md +40 -15
  197. package/templates/planning/visual_map.md +50 -0
  198. package/templates/reference/docs-library-standard.md +31 -0
  199. package/templates/reference/execution-workflow-standard.md +5 -2
  200. package/templates/reference/external-source-intake-standard.md +82 -0
  201. package/templates/reference/harness-ledger-standard.md +1 -0
  202. package/templates/reference/pull-request-standard.md +80 -0
  203. package/templates/reference/repo-governance-standard.md +8 -5
  204. package/templates/reference/review-routing-standard.md +6 -0
  205. package/templates/reference/walkthrough-standard.md +3 -1
  206. package/templates/verifier/verifier-output.md +1 -1
  207. package/templates/walkthrough/walkthrough-template.md +2 -2
  208. package/templates-zh-CN/AGENTS.md.template +73 -70
  209. package/templates-zh-CN/architecture/Architecture-SSoT.md +21 -0
  210. package/templates-zh-CN/architecture/README.md +51 -0
  211. package/templates-zh-CN/architecture/critical-flows.md +24 -0
  212. package/templates-zh-CN/architecture/local-repo-context.md +20 -0
  213. package/templates-zh-CN/architecture/service-catalog.md +17 -0
  214. package/templates-zh-CN/architecture/services/service-template.md +31 -0
  215. package/templates-zh-CN/architecture/system-map.md +22 -0
  216. package/templates-zh-CN/development/README.md +54 -0
  217. package/templates-zh-CN/development/codebase-map.md +11 -0
  218. package/templates-zh-CN/development/cross-repo-debugging.md +18 -0
  219. package/templates-zh-CN/development/external-context/service-template.md +33 -0
  220. package/templates-zh-CN/development/external-source-packs/README.md +24 -0
  221. package/templates-zh-CN/development/external-source-packs/digest-template.md +28 -0
  222. package/templates-zh-CN/development/local-setup.md +16 -0
  223. package/templates-zh-CN/development/stubs-and-mocks.md +11 -0
  224. package/templates-zh-CN/integrations/README.md +42 -0
  225. package/templates-zh-CN/integrations/api-contract.md +42 -0
  226. package/templates-zh-CN/integrations/event-contract.md +46 -0
  227. package/templates-zh-CN/integrations/third-party/vendor-template.md +42 -0
  228. package/templates-zh-CN/integrations/webhook-contract.md +41 -0
  229. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  230. package/templates-zh-CN/planning/brief.md +32 -0
  231. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  232. package/templates-zh-CN/planning/lesson_candidates.md +70 -0
  233. package/templates-zh-CN/planning/long-running-task-contract.md +1 -1
  234. package/templates-zh-CN/planning/module_brief.md +25 -0
  235. package/templates-zh-CN/planning/module_plan.md +2 -2
  236. package/templates-zh-CN/planning/module_session_prompt.md +4 -3
  237. package/templates-zh-CN/planning/review.md +59 -1
  238. package/templates-zh-CN/planning/task_plan.md +37 -11
  239. package/templates-zh-CN/planning/{visual_roadmap.md → visual_map.md} +21 -2
  240. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  241. package/templates-zh-CN/reference/docs-library-standard.md +36 -1
  242. package/templates-zh-CN/reference/execution-workflow-standard.md +10 -2
  243. package/templates-zh-CN/reference/external-source-intake-standard.md +82 -0
  244. package/templates-zh-CN/reference/harness-ledger-standard.md +7 -4
  245. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  246. package/templates-zh-CN/reference/repo-governance-standard.md +4 -1
  247. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  248. package/templates-zh-CN/reference/walkthrough-standard.md +6 -5
  249. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +2 -2
  250. package/templates-zh-CN/walkthrough/walkthrough-template.md +2 -2
  251. package/scripts/smoke-dashboard.mjs +0 -70
  252. package/scripts/test-harness.mjs +0 -483
  253. package/templates/ssot/Feature-SSoT.md +0 -43
  254. package/templates/ssot/Lessons-SSoT.md +0 -44
  255. package/templates-zh-CN/dashboard/assets/app.css +0 -399
  256. package/templates-zh-CN/dashboard/assets/app.js +0 -435
  257. package/templates-zh-CN/dashboard/assets/i18n.js +0 -47
  258. package/templates-zh-CN/dashboard/assets/markdown-reader.js +0 -116
  259. package/templates-zh-CN/dashboard/assets/mermaid-renderer.js +0 -59
  260. package/templates-zh-CN/dashboard/index.html +0 -18
  261. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  262. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -0,0 +1,576 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ allowedTaskStates,
5
+ allowedTaskBudgets,
6
+ visualMapFile,
7
+ legacyVisualRoadmapFile,
8
+ lessonCandidatesFile,
9
+ longRunningTaskContractFile,
10
+ taskContractMarker,
11
+ toPosix,
12
+ readFileSafe,
13
+ walkFiles,
14
+ titleFromMarkdown,
15
+ } from "./core-shared.mjs";
16
+ import {
17
+ tableAfterHeading,
18
+ firstColumn,
19
+ splitList,
20
+ splitDependencies,
21
+ } from "./markdown-utils.mjs";
22
+ import {
23
+ isLessonCandidateDecisionComplete,
24
+ parseLessonCandidateStatus,
25
+ validateLessonCandidateDetailArtifacts,
26
+ } from "./task-lesson-candidates.mjs";
27
+ import {
28
+ assessMaterialsReadiness,
29
+ collectReviewRisks,
30
+ collectStateConflicts,
31
+ deriveLifecycleState,
32
+ deriveReviewQueueState,
33
+ deriveTaskQueues,
34
+ isBlockingReviewRisk,
35
+ parseAgentReviewSubmission,
36
+ parseReviewConfirmation,
37
+ parseTaskIdentity,
38
+ parseTaskTombstone,
39
+ requiresReviewMaterials,
40
+ taskReviewStatus,
41
+ taskScannerVersion,
42
+ } from "./task-review-model.mjs";
43
+ export {
44
+ collectReviewRisks,
45
+ deriveLifecycleState,
46
+ deriveReviewQueueState,
47
+ isBlockingReviewRisk,
48
+ parseAgentReviewSubmission,
49
+ parseReviewConfirmation,
50
+ parseTaskIdentity,
51
+ parseTaskTombstone,
52
+ requiresReviewMaterials,
53
+ taskReviewStatus,
54
+ taskScannerVersion,
55
+ } from "./task-review-model.mjs";
56
+ export {
57
+ allowedLessonCandidateRowStatuses,
58
+ allowedLessonCandidateTaskStatuses,
59
+ isLessonCandidateDecisionComplete,
60
+ parseLessonCandidateStatus,
61
+ reviewCompleteLessonCandidateStatuses,
62
+ } from "./task-lesson-candidates.mjs";
63
+
64
+ export function parseTaskState(progressContent) {
65
+ return parseTaskStateInfo(progressContent).state;
66
+ }
67
+
68
+ export function parseTaskBudget(taskPlanContent) {
69
+ const match =
70
+ String(taskPlanContent || "").match(/^Selected budget\s*[::]\s*([^\n]+)/im) ||
71
+ String(taskPlanContent || "").match(/^选择预算\s*[::]\s*([^\n]+)/im);
72
+ if (!match) return "standard";
73
+ const raw = match[1].replace(/`/g, "").trim().toLowerCase();
74
+ const normalized = raw.replaceAll("_", "-").replace(/\s+/g, "-");
75
+ if (allowedTaskBudgets.has(normalized)) return normalized;
76
+ if (["long-running", "longrunning", "module-parallel"].includes(normalized)) return "complex";
77
+ return "standard";
78
+ }
79
+
80
+ function parseMetadataLine(content, labels) {
81
+ const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
82
+ const match = String(content || "").match(new RegExp(`^(?:${escaped})\\s*[::]\\s*([^\\n]+)`, "im"));
83
+ return match ? match[1].replace(/`/g, "").trim() : "";
84
+ }
85
+
86
+ function normalizeMetadataValue(value, fallback = "") {
87
+ const normalized = String(value || "")
88
+ .replace(/`/g, "")
89
+ .trim()
90
+ .toLowerCase()
91
+ .replaceAll("_", "-")
92
+ .replace(/\s+/g, "-");
93
+ return normalized || fallback;
94
+ }
95
+
96
+ export function parseTaskMetadata(taskPlanContent) {
97
+ const content = String(taskPlanContent || "");
98
+ const kind = normalizeMetadataValue(parseMetadataLine(content, ["Task Kind", "任务类型"]), "general");
99
+ const preset = normalizeMetadataValue(parseMetadataLine(content, ["Task Preset", "Preset", "任务预设"]), "none");
100
+ const presetVersion = parseMetadataLine(content, ["Preset Version", "预设版本"]);
101
+ const migrationTargetLevel = normalizeMetadataValue(
102
+ parseMetadataLine(content, ["Migration Target Level", "Target Level", "迁移目标等级", "目标等级"]),
103
+ "",
104
+ );
105
+ const migrationAchievedLevel = normalizeMetadataValue(
106
+ parseMetadataLine(content, ["Migration Achieved Level", "Achieved Level", "迁移实际完成等级", "实际完成等级"]),
107
+ "",
108
+ );
109
+ const evidenceBundle = parseMetadataLine(content, ["Evidence Bundle", "证据包"]);
110
+ return {
111
+ kind,
112
+ preset,
113
+ presetVersion,
114
+ migrationTargetLevel,
115
+ migrationAchievedLevel,
116
+ evidenceBundle,
117
+ };
118
+ }
119
+
120
+ export function parseTaskContractInfo(taskPlanContent) {
121
+ const content = String(taskPlanContent || "");
122
+ const explicit =
123
+ content.match(/^Task Contract\s*[::]\s*`?([^`\n]+)`?\s*$/im) ||
124
+ content.match(/^任务合同\s*[::]\s*`?([^`\n]+)`?\s*$/im);
125
+ const version = explicit ? explicit[1].trim() : "";
126
+ return {
127
+ version,
128
+ generated: version === "harness-task/v1" || content.includes(taskContractMarker),
129
+ };
130
+ }
131
+
132
+ export function parseTaskStateInfo(progressContent) {
133
+ const match = progressContent.match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
134
+ if (!match) return inferLegacyTaskState(progressContent);
135
+ const raw = match[1].replace(/`/g, "").trim();
136
+ if (!raw || raw.includes("|") || /^[-*]\s+/.test(raw)) return inferLegacyTaskState(progressContent);
137
+ const aliases = new Map([
138
+ ["进行中", "in_progress"],
139
+ ["已完成", "done"],
140
+ ["未开始", "not_started"],
141
+ ["计划中", "planned"],
142
+ ["审查中", "review"],
143
+ ["已阻塞", "blocked"],
144
+ ["pending", "planned"],
145
+ ]);
146
+ const normalized = aliases.get(raw) || raw.toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
147
+ return allowedTaskStates.has(normalized)
148
+ ? { state: normalized, source: "explicit", raw }
149
+ : { state: "unknown", source: "invalid", raw };
150
+ }
151
+
152
+ function inferLegacyTaskState(progressContent) {
153
+ const { header, rows } = tableAfterHeading(progressContent, /^(Status|状态)$/i);
154
+ const statusIndex = firstColumn(header, ["Status", "状态"]);
155
+ if (statusIndex < 0 || rows.length === 0) return { state: "unknown", source: "missing", raw: "" };
156
+ const states = rows.map((row) => normalizeLegacyState(row[statusIndex])).filter(Boolean);
157
+ if (states.includes("blocked")) return { state: "blocked", source: "legacy-table", raw: "blocked" };
158
+ if (states.includes("in_progress")) return { state: "in_progress", source: "legacy-table", raw: "in_progress" };
159
+ if (states.includes("review")) return { state: "review", source: "legacy-table", raw: "review" };
160
+ if (states.length > 0 && states.every((state) => state === "done")) return { state: "done", source: "legacy-table", raw: "done" };
161
+ if (states.some((state) => ["planned", "not_started"].includes(state))) return { state: "planned", source: "legacy-table", raw: "planned" };
162
+ return { state: "unknown", source: "missing", raw: "" };
163
+ }
164
+
165
+ function normalizeLegacyState(value) {
166
+ const raw = String(value || "").replace(/`/g, "").trim().toLowerCase();
167
+ if (!raw || /^(none|n\/a|na|-|—|–|无)$/.test(raw)) return "";
168
+ if (/block|阻塞|blocked/.test(raw)) return "blocked";
169
+ if (/in[-_\s]?progress|doing|active|进行中|当前|working/.test(raw)) return "in_progress";
170
+ if (/review|审查|审核|验证中/.test(raw)) return "review";
171
+ if (/done|complete|completed|merged|closed|完成|已完成/.test(raw)) return "done";
172
+ if (/pending|planned|todo|not[-_\s]?started|未开始|计划/.test(raw)) return "planned";
173
+ return "";
174
+ }
175
+
176
+ export function parsePhases(taskPlanContent) {
177
+ const { header, rows } = tableAfterHeading(taskPlanContent, /^Phase ID$/i);
178
+ if (rows.length === 0) return [];
179
+ const indexes = {
180
+ id: firstColumn(header, ["Phase ID", "阶段 ID"]),
181
+ dependsOn: firstColumn(header, ["Depends On", "依赖"]),
182
+ state: firstColumn(header, ["State", "状态"]),
183
+ completion: firstColumn(header, ["Completion", "完成度"]),
184
+ output: firstColumn(header, ["Output", "产出"]),
185
+ requiredEvidence: firstColumn(header, ["Required Evidence", "必要证据"]),
186
+ evidenceStatus: firstColumn(header, ["Evidence Status", "证据状态"]),
187
+ blockingRisk: firstColumn(header, ["Blocking Risk", "阻塞风险"]),
188
+ owner: firstColumn(header, ["Owner / Handoff", "负责人 / 交接"]),
189
+ };
190
+ return rows.map((row) => ({
191
+ id: row[indexes.id] || "",
192
+ dependsOn: splitDependencies(row[indexes.dependsOn] || ""),
193
+ state: row[indexes.state] || "planned",
194
+ completion: Number.parseInt(String(row[indexes.completion] || "0").replace("%", ""), 10) || 0,
195
+ output: row[indexes.output] || "",
196
+ requiredEvidence: splitList(row[indexes.requiredEvidence] || ""),
197
+ evidenceStatus: row[indexes.evidenceStatus] || "missing",
198
+ blockingRisk: row[indexes.blockingRisk] || "",
199
+ owner: row[indexes.owner] || "",
200
+ }));
201
+ }
202
+
203
+ export function readTaskContractFile(taskDir, fileName, legacyContent = "") {
204
+ const filePath = path.join(taskDir, fileName);
205
+ const content = readFileSafe(filePath);
206
+ if (content.trim()) return { path: filePath, content, source: "standalone" };
207
+ return { path: filePath, content: legacyContent, source: legacyContent.trim() ? "legacy" : "missing" };
208
+ }
209
+
210
+ export function readVisualMapContractFile(taskDir, legacyContent = "") {
211
+ const canonicalPath = path.join(taskDir, visualMapFile);
212
+ const canonical = readFileSafe(canonicalPath);
213
+ if (canonical.trim()) return { path: canonicalPath, content: canonical, source: "canonical", status: "present" };
214
+ const legacyPath = path.join(taskDir, legacyVisualRoadmapFile);
215
+ const legacy = readFileSafe(legacyPath);
216
+ if (legacy.trim()) return { path: legacyPath, content: legacy, source: "legacy", status: "legacy-only" };
217
+ return {
218
+ path: canonicalPath,
219
+ content: legacyContent,
220
+ source: legacyContent.trim() ? "legacy" : "missing",
221
+ status: legacyContent.trim() ? "legacy-only" : "missing",
222
+ };
223
+ }
224
+
225
+ export function isActiveTaskState(state) {
226
+ return ["active", "planned", "not_started", "in_progress", "review", "blocked", "reopened", "current-evidence"].includes(state);
227
+ }
228
+
229
+ export function listTaskPlanPaths(target) {
230
+ const taskRoots = [
231
+ path.join(target.docsRoot, "09-PLANNING/TASKS"),
232
+ path.join(target.docsRoot, "09-PLANNING/MODULES"),
233
+ ];
234
+ return taskRoots
235
+ .flatMap(walkFiles)
236
+ .filter((file) => file.endsWith("task_plan.md"))
237
+ .filter((file) => !file.includes(`${path.sep}_task-template${path.sep}`))
238
+ .filter((file) => !file.includes(`${path.sep}_optional-structures${path.sep}`))
239
+ .filter((file) => !file.includes(`${path.sep}_archive${path.sep}`));
240
+ }
241
+
242
+ export function taskIdForDirectory(target, taskDir) {
243
+ return toPosix(path.relative(path.join(target.docsRoot, "09-PLANNING"), taskDir));
244
+ }
245
+
246
+ export function inferTaskClassification({ id, title, relative, explicitModule, legacyCandidate = false }) {
247
+ if (explicitModule) {
248
+ return {
249
+ module: explicitModule,
250
+ source: "explicit",
251
+ bucket: "module",
252
+ };
253
+ }
254
+ const text = `${id} ${title} ${relative}`.toLowerCase();
255
+ const rules = [
256
+ ["dashboard", /dashboard|visibility|cockpit|console|ui|frontend|view|页面|看板|驾驶舱/],
257
+ ["migration", /migration|migrate|adoption|legacy|safe-adoption|迁移|历史|兼容/],
258
+ ["task-lifecycle", /task|phase|lifecycle|planning|计划|任务|阶段/],
259
+ ["review-quality", /review|finding|evidence|qa|test|regression|审查|证据|回归|测试/],
260
+ ["release-docs", /docs-release|readme|guide|install|playbook|文档|安装|指南/],
261
+ ["repo-governance", /git|ci|source-package|private|boundary|repo|branch|pr|仓库|边界/],
262
+ ["automation-cli", /cli|command|script|harness\.mjs|自动化|命令/],
263
+ ];
264
+ const match = rules.find(([, pattern]) => pattern.test(text));
265
+ return {
266
+ module: match ? match[0] : legacyCandidate ? "legacy-unclassified" : "unclassified",
267
+ source: match ? "inferred" : "fallback",
268
+ bucket: legacyCandidate ? "legacy" : "current",
269
+ };
270
+ }
271
+
272
+ export function assessBriefQuality(content, { source = "missing" } = {}) {
273
+ const text = String(content || "").trim();
274
+ const issues = [];
275
+ if (source !== "standalone") issues.push("missing-standalone-brief");
276
+ if (text.length < 120) issues.push("too-short");
277
+ if (!/^##\s+/m.test(text)) issues.push("missing-sections");
278
+ if (/\[(?:outcome|scope|risk|evidence|next|目标|范围|风险|证据|下一步)[^\]]*\]/i.test(text)) issues.push("unfilled-placeholder");
279
+ return { status: issues.length ? "fail" : "pass", issues };
280
+ }
281
+
282
+ export function explicitVisualMapStatus(briefContent) {
283
+ const match = String(briefContent || "").match(/^Visual Map Status:\s*(present|not-needed|missing|legacy-only)\s*$/im);
284
+ return match ? match[1] : "";
285
+ }
286
+
287
+ export function taskMigrationClassification(state, visualMapStatus) {
288
+ if (state === "unknown") return "unknown-needs-human";
289
+ if (isActiveTaskState(state)) return "active";
290
+ if (visualMapStatus === "present" || visualMapStatus === "legacy-only") return "historical-with-diagram";
291
+ return "historical-no-map-needed";
292
+ }
293
+
294
+ export function requiresCanonicalVisualMap(task) {
295
+ return ["active", "reopened", "current-evidence", "historical-with-diagram"].includes(task.migrationClassification);
296
+ }
297
+
298
+ export function taskCutoverCounters(tasks) {
299
+ const legacyVisualOnlyCount = tasks.filter((task) => task.visualMapStatus === "legacy-only").length;
300
+ const unknownClassificationCount = tasks.filter((task) => task.migrationClassification === "unknown-needs-human").length;
301
+ const weakBriefCount = tasks.filter((task) => task.briefQuality?.status !== "pass").length;
302
+ const visualMapRequiredCount = tasks.filter(requiresCanonicalVisualMap).length;
303
+ const missingCanonicalVisualMapCount = tasks.filter((task) => requiresCanonicalVisualMap(task) && task.visualMapSource !== "canonical").length;
304
+ return {
305
+ legacyVisualOnlyCount,
306
+ unknownClassificationCount,
307
+ weakBriefCount,
308
+ visualMapRequiredCount,
309
+ missingCanonicalVisualMapCount,
310
+ };
311
+ }
312
+
313
+ export function collectTasks(target) {
314
+ return listTaskPlanPaths(target).map((taskPlanPath) => {
315
+ const taskDir = path.dirname(taskPlanPath);
316
+ const taskPlan = readFileSafe(taskPlanPath);
317
+ const brief = readTaskContractFile(taskDir, "brief.md", "");
318
+ const executionStrategyPath = path.join(taskDir, "execution_strategy.md");
319
+ const progressPath = path.join(taskDir, "progress.md");
320
+ const reviewPath = path.join(taskDir, "review.md");
321
+ const findingsPath = path.join(taskDir, "findings.md");
322
+ const lessonCandidatesPath = path.join(taskDir, lessonCandidatesFile);
323
+ const longRunningContractPath = path.join(taskDir, longRunningTaskContractFile);
324
+ const visualMap = readVisualMapContractFile(taskDir, taskPlan);
325
+ const progress = readFileSafe(progressPath);
326
+ const review = readFileSafe(reviewPath);
327
+ const parsedLessonCandidates = parseLessonCandidateStatus(readFileSafe(lessonCandidatesPath));
328
+ const lessonDetailIssues = validateLessonCandidateDetailArtifacts(target, taskDir, parsedLessonCandidates);
329
+ const lessonCandidates = lessonDetailIssues.length
330
+ ? { ...parsedLessonCandidates, issues: [...parsedLessonCandidates.issues, ...lessonDetailIssues] }
331
+ : parsedLessonCandidates;
332
+ const phases = parsePhases(visualMap.content);
333
+ const completion =
334
+ phases.length > 0
335
+ ? Math.round(
336
+ phases.filter((phase) => phase.state !== "skipped").reduce((sum, phase) => sum + phase.completion, 0) /
337
+ Math.max(1, phases.filter((phase) => phase.state !== "skipped").length),
338
+ )
339
+ : 0;
340
+ const relative = toPosix(path.relative(target.projectRoot, taskDir));
341
+ const id = taskIdForDirectory(target, taskDir);
342
+ const identity = parseTaskIdentity(taskPlan, id);
343
+ const tombstone = parseTaskTombstone(taskPlan);
344
+ const title = titleFromMarkdown(brief.content || taskPlan, path.basename(taskDir));
345
+ const stateInfo = parseTaskStateInfo(progress);
346
+ const budget = parseTaskBudget(taskPlan);
347
+ const metadata = parseTaskMetadata(taskPlan);
348
+ const taskContract = parseTaskContractInfo(taskPlan);
349
+ const explicitModule = id.startsWith("MODULES/") ? id.split("/")[1] : null;
350
+ const legacyCandidate = brief.source !== "standalone" || visualMap.status === "legacy-only" || !fs.existsSync(executionStrategyPath);
351
+ const classification = inferTaskClassification({ id, title, relative, explicitModule, legacyCandidate });
352
+ const briefVisualStatus = explicitVisualMapStatus(brief.content);
353
+ const visualMapStatus = briefVisualStatus === "not-needed" && visualMap.status === "missing" ? "not-needed" : visualMap.status;
354
+ const risks = collectReviewRisks(review);
355
+ const reviewSubmission = parseAgentReviewSubmission(review, { taskKey: identity.taskKey });
356
+ const reviewConfirmation = parseReviewConfirmation(review, {
357
+ taskKey: identity.taskKey,
358
+ projectRoot: target.projectRoot,
359
+ taskDir,
360
+ reviewPath,
361
+ progressPath,
362
+ });
363
+ const reviewStatus = taskReviewStatus({ reviewContent: review, risks, confirmation: reviewConfirmation, submission: reviewSubmission });
364
+ const closeoutInfo = taskCloseoutInfo(target, taskPlanPath);
365
+ const lifecycleState = deriveLifecycleState({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status });
366
+ const materialReadiness = assessMaterialsReadiness({
367
+ budget,
368
+ taskDir,
369
+ taskPlan,
370
+ brief,
371
+ visualMap,
372
+ reviewSubmission,
373
+ lessonCandidates,
374
+ phases,
375
+ longRunningContractPath,
376
+ reviewSurfaceRequired: requiresReviewMaterials({
377
+ state: stateInfo.state,
378
+ lifecycleState,
379
+ closeoutStatus: closeoutInfo.status,
380
+ }),
381
+ });
382
+ const stateConflicts = collectStateConflicts({ state: stateInfo.state, reviewStatus, closeoutStatus: closeoutInfo.status, lifecycleState });
383
+ const reviewQueueState = deriveReviewQueueState({
384
+ state: stateInfo.state,
385
+ lifecycleState,
386
+ reviewStatus,
387
+ closeoutStatus: closeoutInfo.status,
388
+ budget,
389
+ walkthroughPath: closeoutInfo.walkthroughPath,
390
+ lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
391
+ materialsReady: materialReadiness.ready,
392
+ deletionState: tombstone.deletionState,
393
+ });
394
+ const queueModel = deriveTaskQueues({
395
+ id,
396
+ title,
397
+ state: stateInfo.state,
398
+ budget,
399
+ reviewStatus,
400
+ reviewSubmission,
401
+ reviewConfirmation,
402
+ reviewQueueState,
403
+ materialIssues: materialReadiness.issues,
404
+ risks,
405
+ stateConflicts,
406
+ lessonCandidates,
407
+ closeoutStatus: closeoutInfo.status,
408
+ tombstone,
409
+ taskDir,
410
+ target,
411
+ });
412
+ return {
413
+ id,
414
+ taskKey: identity.taskKey,
415
+ currentPath: `TARGET:${relative}`,
416
+ originalPath: `TARGET:${relative}`,
417
+ aliases: [],
418
+ identitySource: identity.identitySource,
419
+ shortId: path.basename(taskDir),
420
+ title,
421
+ path: `TARGET:${relative}`,
422
+ taskPlanPath: `TARGET:${toPosix(path.relative(target.projectRoot, taskPlanPath))}`,
423
+ executionStrategyPath: `TARGET:${toPosix(path.relative(target.projectRoot, executionStrategyPath))}`,
424
+ progressPath: `TARGET:${toPosix(path.relative(target.projectRoot, progressPath))}`,
425
+ reviewPath: `TARGET:${toPosix(path.relative(target.projectRoot, reviewPath))}`,
426
+ findingsPath: `TARGET:${toPosix(path.relative(target.projectRoot, findingsPath))}`,
427
+ module: explicitModule,
428
+ inferredModule: classification.module,
429
+ classificationSource: classification.source,
430
+ classificationBucket: classification.bucket,
431
+ briefSource: brief.source,
432
+ briefPath: `TARGET:${toPosix(path.relative(target.projectRoot, brief.path))}`,
433
+ visualMapSource: visualMap.source,
434
+ visualMapStatus,
435
+ visualMapPath: `TARGET:${toPosix(path.relative(target.projectRoot, visualMap.path))}`,
436
+ legacyVisualRoadmapPresent: fs.existsSync(path.join(taskDir, legacyVisualRoadmapFile)),
437
+ briefQuality: assessBriefQuality(brief.content, { source: brief.source }),
438
+ migrationClassification: taskMigrationClassification(stateInfo.state, visualMapStatus),
439
+ roadmapSource: visualMap.source,
440
+ state: stateInfo.state,
441
+ budget,
442
+ taskContractVersion: taskContract.version,
443
+ taskContractGenerated: taskContract.generated,
444
+ stateSource: stateInfo.source,
445
+ stateRaw: stateInfo.raw,
446
+ taskKind: metadata.kind,
447
+ taskPreset: metadata.preset,
448
+ presetVersion: metadata.presetVersion,
449
+ migrationTargetLevel: metadata.migrationTargetLevel,
450
+ migrationAchievedLevel: metadata.migrationAchievedLevel,
451
+ evidenceBundle: formatEvidenceBundle(metadata.evidenceBundle),
452
+ migrationSnapshot: collectMigrationSnapshot(target, metadata),
453
+ lifecycleState,
454
+ reviewStatus,
455
+ reviewSubmitted: Boolean(reviewSubmission?.submitted),
456
+ reviewSubmission,
457
+ reviewQueueState,
458
+ reviewConfirmation,
459
+ materialsReady: materialReadiness.ready,
460
+ materialIssues: materialReadiness.issues,
461
+ taskQueues: queueModel.taskQueues,
462
+ queueReasons: queueModel.queueReasons,
463
+ repairPrompt: queueModel.repairPrompt,
464
+ closeoutStatus: closeoutInfo.status,
465
+ walkthroughPath: closeoutInfo.walkthroughPath ? `TARGET:${closeoutInfo.walkthroughPath}` : "",
466
+ lessonCandidatePath: fs.existsSync(lessonCandidatesPath)
467
+ ? `TARGET:${toPosix(path.relative(target.projectRoot, lessonCandidatesPath))}`
468
+ : "",
469
+ lessonCandidateStatus: lessonCandidates.status,
470
+ lessonCandidateReviewDecision: lessonCandidates.reviewDecision,
471
+ lessonCandidatePromotionState: lessonCandidates.promotionState,
472
+ lessonCandidateCloseoutToken: lessonCandidates.closeoutToken,
473
+ lessonCandidateRowCount: lessonCandidates.rows.length,
474
+ lessonCandidateRows: lessonCandidates.rows,
475
+ lessonCandidateOpenCount: lessonCandidates.openCount,
476
+ lessonCandidateIssues: lessonCandidates.issues,
477
+ lessonCandidateDecisionComplete: isLessonCandidateDecisionComplete(lessonCandidates),
478
+ longRunningContractPath: fs.existsSync(longRunningContractPath)
479
+ ? `TARGET:${toPosix(path.relative(target.projectRoot, longRunningContractPath))}`
480
+ : "",
481
+ longRunningContractStatus: fs.existsSync(longRunningContractPath) ? "present" : "missing",
482
+ deletionState: tombstone.deletionState,
483
+ supersededBy: tombstone.supersededBy,
484
+ supersedes: tombstone.supersedes,
485
+ deleteReason: tombstone.deleteReason,
486
+ hiddenByDefault: tombstone.hiddenByDefault,
487
+ reopenEligible: tombstone.reopenEligible,
488
+ archiveEligible: tombstone.archiveEligible,
489
+ tombstoneSourcePath: tombstone.tombstoneSourcePath
490
+ ? `TARGET:${toPosix(path.relative(target.projectRoot, path.join(taskDir, "task_plan.md")))}#Task Tombstone`
491
+ : "",
492
+ stateConflicts,
493
+ completion,
494
+ phases,
495
+ risks,
496
+ evidence: collectEvidence(progress),
497
+ handoffs: collectHandoffs(progress, title),
498
+ dependencies: [],
499
+ };
500
+ });
501
+ }
502
+
503
+ function collectMigrationSnapshot(target, metadata) {
504
+ if (metadata.preset !== "legacy-migration") return null;
505
+ const evidenceBundle = String(metadata.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
506
+ const bundlePath = evidenceBundle ? path.join(target.projectRoot, evidenceBundle) : "";
507
+ const sessionPath = bundlePath ? path.join(bundlePath, "session.json") : "";
508
+ let session = null;
509
+ try {
510
+ session = sessionPath && fs.existsSync(sessionPath) ? JSON.parse(fs.readFileSync(sessionPath, "utf8")) : null;
511
+ } catch {
512
+ session = null;
513
+ }
514
+ const summary = session?.plan?.summary || {};
515
+ return {
516
+ targetLevel: metadata.migrationTargetLevel || "",
517
+ achievedLevel: metadata.migrationAchievedLevel || "",
518
+ evidenceBundle: evidenceBundle ? `TARGET:${evidenceBundle}` : "",
519
+ evidencePresent: Boolean(bundlePath && fs.existsSync(bundlePath)),
520
+ sessionPresent: Boolean(session),
521
+ sessionResult: session?.result || "",
522
+ normalStatus: session?.checks?.normal?.status || "",
523
+ strictStatus: session?.checks?.strict?.status || "",
524
+ strictDeferred: Boolean(session?.strictDeferred),
525
+ warnings: Number(summary.warnings || 0),
526
+ taskActions: Number(summary.taskActions || 0),
527
+ reviewSchemaGaps: Number(summary.reviewSchemaGaps || 0),
528
+ legacyReferenceGaps: Number(summary.legacyReferenceGaps || 0),
529
+ legacyResiduals: Number(summary.legacyResiduals || 0),
530
+ fullCutoverEligible: summary.fullCutoverEligible === true,
531
+ };
532
+ }
533
+
534
+ function formatEvidenceBundle(value) {
535
+ const normalized = String(value || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
536
+ return normalized ? `TARGET:${normalized}` : "";
537
+ }
538
+
539
+ function taskCloseoutInfo(target, taskPlanPath) {
540
+ const closeout = readFileSafe(path.join(target.docsRoot, "10-WALKTHROUGH/Closeout-SSoT.md"));
541
+ if (!closeout.trim()) return { status: "missing", walkthroughPath: "" };
542
+ const docsRelative = `docs/${toPosix(path.relative(target.docsRoot, taskPlanPath))}`;
543
+ const projectRelative = toPosix(path.relative(target.projectRoot, taskPlanPath));
544
+ const line = closeout
545
+ .split(/\r?\n/)
546
+ .find((entry) => entry.includes(docsRelative) || entry.includes(projectRelative));
547
+ if (!line) return { status: "missing", walkthroughPath: "" };
548
+ const walkthroughPath = extractWalkthroughPath(target, line);
549
+ const status = /\b(closed|complete|completed|done|skipped-with-reason|skipped|已关闭|已完成|跳过)\b/i.test(line) ? "closed" : "pending";
550
+ return { status, walkthroughPath };
551
+ }
552
+
553
+ function extractWalkthroughPath(target, closeoutLine) {
554
+ const matches = [...String(closeoutLine || "").matchAll(/`?((?:docs\/)?10-WALKTHROUGH\/[^`|\s]+\.md)`?/g)];
555
+ const match = matches.find((entry) => !entry[1].endsWith("Closeout-SSoT.md") && !entry[1].includes("/_"));
556
+ if (!match) return "";
557
+ const projectRelative = match[1].startsWith("docs/") ? match[1] : `docs/${match[1]}`;
558
+ if (!fs.existsSync(path.join(target.projectRoot, projectRelative))) return "";
559
+ return projectRelative;
560
+ }
561
+
562
+ function collectHandoffs(progressContent, taskId) {
563
+ if (!/Coordinator Handoff/i.test(progressContent) || !/pending-coordinator-pass/i.test(progressContent)) return [];
564
+ return [{ id: `H-${taskId}`, from: "worker", to: "coordinator", state: "pending", summary: "Coordinator handoff pending" }];
565
+ }
566
+
567
+ function collectEvidence(progressContent) {
568
+ const matches = [...progressContent.matchAll(/\b(command|diff|fixture|screenshot|review|report):((?:PUBLIC|PRIVATE|TARGET|EXTERNAL|URL):[^:\s|]+):([^\n|]+)/g)];
569
+ return matches.map((match, index) => ({
570
+ id: `E-${String(index + 1).padStart(3, "0")}`,
571
+ type: match[1],
572
+ path: match[2],
573
+ status: "present",
574
+ summary: match[3].trim(),
575
+ }));
576
+ }