coding-agent-harness 1.0.8 → 1.1.0

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 (232) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CONTRIBUTING.md +8 -4
  3. package/README.md +12 -2
  4. package/README.zh-CN.md +10 -2
  5. package/SKILL.md +14 -3
  6. package/dist/build-dist.mjs +19 -6
  7. package/dist/check-dist-observation.mjs +57 -29
  8. package/dist/check-harness.mjs +0 -1
  9. package/dist/check-import-graph.mjs +44 -27
  10. package/dist/check-lite-forbidden-surfaces.mjs +121 -0
  11. package/dist/check-no-ts-nocheck.mjs +7 -7
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +51 -9
  14. package/dist/commands/dashboard-command.mjs +52 -14
  15. package/dist/commands/migration-command.mjs +18 -8
  16. package/dist/commands/module-command.mjs +142 -0
  17. package/dist/commands/preset-command.mjs +51 -12
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +109 -52
  20. package/dist/harness.mjs +6 -304
  21. package/dist/lib/capability-registry.mjs +229 -53
  22. package/dist/lib/check-module-parallel.mjs +1 -6
  23. package/dist/lib/check-profiles.mjs +39 -46
  24. package/dist/lib/check-task-contracts.mjs +6 -4
  25. package/dist/lib/command-registry.mjs +248 -0
  26. package/dist/lib/core-shared.mjs +78 -3
  27. package/dist/lib/dashboard-data.mjs +203 -22
  28. package/dist/lib/dashboard-workbench.mjs +245 -21
  29. package/dist/lib/dashboard-writer.mjs +4 -1
  30. package/dist/lib/git-status-summary.mjs +0 -1
  31. package/dist/lib/governance-index-generator.mjs +7 -5
  32. package/dist/lib/governance-sync.mjs +46 -121
  33. package/dist/lib/governance-table-boundary.mjs +1 -14
  34. package/dist/lib/harness-core.mjs +4 -1
  35. package/dist/lib/harness-paths.mjs +115 -1
  36. package/dist/lib/impact-classifier.mjs +420 -0
  37. package/dist/lib/lesson-maintenance.mjs +1 -2
  38. package/dist/lib/markdown-utils.mjs +50 -1
  39. package/dist/lib/migration-planner.mjs +31 -16
  40. package/dist/lib/migration-support.mjs +5 -4
  41. package/dist/lib/module-registry.mjs +296 -0
  42. package/dist/lib/preset-audit-contracts.mjs +24 -1
  43. package/dist/lib/preset-engine.mjs +67 -29
  44. package/dist/lib/preset-registry.mjs +361 -71
  45. package/dist/lib/preset-runner.mjs +292 -26
  46. package/dist/lib/review-confirm-git-gate.mjs +73 -19
  47. package/dist/lib/status-builder.mjs +23 -8
  48. package/dist/lib/structure-migration.mjs +6 -4
  49. package/dist/lib/subagent-authorization-audit.mjs +8 -2
  50. package/dist/lib/task-archive-eligibility.mjs +65 -0
  51. package/dist/lib/task-audit-metadata.mjs +25 -11
  52. package/dist/lib/task-audit-migration.mjs +21 -14
  53. package/dist/lib/task-discovery-contract.mjs +32 -0
  54. package/dist/lib/task-index.mjs +3 -2
  55. package/dist/lib/task-lesson-candidates.mjs +1 -2
  56. package/dist/lib/task-lesson-sedimentation.mjs +310 -9
  57. package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
  58. package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
  59. package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
  60. package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
  61. package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
  62. package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
  63. package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
  64. package/dist/lib/task-lifecycle/template-files.mjs +2 -5
  65. package/dist/lib/task-lifecycle.mjs +116 -160
  66. package/dist/lib/task-metadata.mjs +10 -5
  67. package/dist/lib/task-preset-contract-drift.mjs +45 -0
  68. package/dist/lib/task-repository.mjs +192 -0
  69. package/dist/lib/task-review-model.mjs +36 -17
  70. package/dist/lib/task-scanner.mjs +74 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +186 -29
  73. package/dist/lib/types/check-profiles.js +1 -0
  74. package/dist/lib/types/impact.js +1 -0
  75. package/dist/lib/types/preset.js +1 -0
  76. package/dist/lib/types/task-lifecycle.js +1 -0
  77. package/dist/lib/types/task-scanner.js +1 -0
  78. package/dist/postinstall.mjs +2 -2
  79. package/dist/run-built-tests.mjs +10 -3
  80. package/docs-release/README.md +1 -0
  81. package/docs-release/architecture/document-contract-kernel/README.md +150 -0
  82. package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
  83. package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
  84. package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
  85. package/docs-release/architecture/overview.md +2 -2
  86. package/docs-release/architecture/overview.zh-CN.md +2 -2
  87. package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
  88. package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
  89. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
  90. package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
  91. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
  92. package/docs-release/architecture/system-explainer/README.md +1 -1
  93. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
  94. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
  95. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
  96. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
  97. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
  98. package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
  99. package/docs-release/guides/agent-installation.en-US.md +4 -6
  100. package/docs-release/guides/agent-installation.md +11 -8
  101. package/docs-release/guides/contributing.md +10 -3
  102. package/docs-release/guides/contributing.zh-CN.md +10 -3
  103. package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
  104. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
  105. package/docs-release/guides/migration-playbook.en-US.md +9 -6
  106. package/docs-release/guides/migration-playbook.md +9 -6
  107. package/docs-release/guides/preset-development.md +68 -2
  108. package/docs-release/guides/task-state-machine.en-US.md +8 -8
  109. package/docs-release/guides/task-state-machine.md +7 -7
  110. package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
  111. package/package.json +16 -12
  112. package/postinstall.mjs +37 -0
  113. package/presets/legacy-migration/preset.yaml +5 -5
  114. package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
  115. package/presets/lesson-sedimentation/preset.yaml +3 -3
  116. package/presets/module/preset.yaml +2 -2
  117. package/presets/module/templates/execution_strategy.append.md +1 -1
  118. package/presets/module/templates/task_plan.append.md +3 -3
  119. package/presets/release-closeout/checks/check-release-package.mjs +6 -1
  120. package/presets/release-closeout/preset.yaml +9 -9
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
  122. package/presets/release-closeout/templates/task_plan.append.md +5 -5
  123. package/presets/standard-task/preset.yaml +2 -2
  124. package/references/agents-md-pattern.md +23 -17
  125. package/references/lessons-governance.md +2 -2
  126. package/references/module-parallel-standard.md +3 -6
  127. package/references/ssot-governance.md +2 -2
  128. package/references/taskr-gap-analysis.md +3 -3
  129. package/run-dist.mjs +34 -0
  130. package/skills/preset-creator/SKILL.md +40 -8
  131. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  132. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  133. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  134. package/templates/AGENTS.md.template +28 -26
  135. package/templates/architecture/README.md +2 -2
  136. package/templates/architecture/service-catalog.md +2 -2
  137. package/templates/architecture/services/service-template.md +1 -1
  138. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  139. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  140. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  141. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  142. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  143. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  144. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  145. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  146. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  147. package/templates/dashboard/assets/app.css +928 -53
  148. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  149. package/templates/dashboard/assets/app.js +1071 -98
  150. package/templates/dashboard/assets/app.manifest.json +1 -0
  151. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  152. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  153. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  154. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  155. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  156. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  157. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  158. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  159. package/templates/dashboard/assets/i18n.js +166 -2
  160. package/templates/development/README.md +9 -9
  161. package/templates/development/cross-repo-debugging.md +3 -3
  162. package/templates/development/external-context/service-template.md +1 -1
  163. package/templates/development/external-source-packs/README.md +2 -2
  164. package/templates/integrations/README.md +4 -4
  165. package/templates/integrations/api-contract.md +1 -1
  166. package/templates/integrations/event-contract.md +1 -1
  167. package/templates/integrations/third-party/vendor-template.md +1 -1
  168. package/templates/integrations/webhook-contract.md +1 -1
  169. package/templates/ledger/Harness-Ledger.md +1 -1
  170. package/templates/modules/module_brief.md +50 -0
  171. package/templates/modules/module_plan.md +49 -0
  172. package/templates/modules/registry_view.md +9 -0
  173. package/templates/modules/session_prompt_pack.md +55 -0
  174. package/templates/planning/brief.md +32 -8
  175. package/templates/planning/module_brief.md +28 -3
  176. package/templates/planning/module_plan.md +26 -11
  177. package/templates/planning/module_session_prompt.md +11 -2
  178. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  179. package/templates/planning/review.md +1 -1
  180. package/templates/planning/visual_map.md +1 -1
  181. package/templates/reference/docs-library-standard.md +7 -7
  182. package/templates/reference/execution-workflow-standard.md +13 -0
  183. package/templates/reference/external-source-intake-standard.md +10 -10
  184. package/templates/reference/repo-governance-standard.md +1 -1
  185. package/templates/reference/review-routing-standard.md +4 -0
  186. package/templates/ssot/Module-Registry.md +4 -38
  187. package/templates/walkthrough/walkthrough-template.md +1 -1
  188. package/templates-zh-CN/AGENTS.md.template +27 -25
  189. package/templates-zh-CN/CLAUDE.md.template +1 -1
  190. package/templates-zh-CN/architecture/README.md +2 -2
  191. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  192. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  193. package/templates-zh-CN/development/README.md +9 -9
  194. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  195. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  196. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  197. package/templates-zh-CN/integrations/README.md +4 -4
  198. package/templates-zh-CN/integrations/api-contract.md +1 -1
  199. package/templates-zh-CN/integrations/event-contract.md +1 -1
  200. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  201. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  202. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  203. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  204. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  205. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  206. package/templates-zh-CN/modules/module_brief.md +47 -0
  207. package/templates-zh-CN/modules/module_plan.md +48 -0
  208. package/templates-zh-CN/modules/registry_view.md +9 -0
  209. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  210. package/templates-zh-CN/planning/INDEX.md +1 -0
  211. package/templates-zh-CN/planning/brief.md +26 -7
  212. package/templates-zh-CN/planning/module_brief.md +24 -2
  213. package/templates-zh-CN/planning/module_plan.md +35 -29
  214. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  215. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  216. package/templates-zh-CN/planning/review.md +1 -1
  217. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  218. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  219. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  220. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  221. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  222. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  223. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  224. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  225. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  226. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  227. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  228. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  229. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  230. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  231. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  232. package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
@@ -43,7 +43,7 @@ templateValues:
43
43
  entrypoints:
44
44
  newTask:
45
45
  type: template
46
- writes: [coding-agent-harness/planning/tasks/**]
46
+ writes: [{{paths.tasksRoot}}/**]
47
47
  reads: [session.json]
48
48
  audit: true
49
49
  templates:
@@ -55,19 +55,19 @@ entrypoints:
55
55
  plan:
56
56
  type: script
57
57
  command: scripts/plan-work-queue.mjs
58
- writes: [coding-agent-harness/planning/tasks/**]
58
+ writes: [{{paths.tasksRoot}}/**]
59
59
  reads: [docs/**, .git/**]
60
60
  audit: true
61
61
  scaffold:
62
62
  type: script
63
63
  command: scripts/scaffold-task-contracts.mjs
64
- writes: [coding-agent-harness/planning/tasks/**]
64
+ writes: [{{paths.tasksRoot}}/**]
65
65
  reads: [docs/**]
66
66
  audit: true
67
67
  check:
68
68
  type: check
69
69
  command: checks/preset-check.mjs
70
- writes: [coding-agent-harness/planning/tasks/**]
70
+ writes: [{{paths.tasksRoot}}/**]
71
71
  reads: [docs/**]
72
72
  audit: true
73
73
  workbench:
@@ -130,5 +130,5 @@ audit:
130
130
  evidenceFiles: [preset-manifest.json, preset-audit.json, write-scope.json]
131
131
  writeScopes:
132
132
  taskArtifacts:
133
- path: coding-agent-harness/planning/tasks/**
133
+ path: {{paths.tasksRoot}}/**
134
134
  access: write
@@ -15,4 +15,4 @@ Declare lanes before dispatching workers.
15
15
 
16
16
  | Lane ID | Allowed globs | Forbidden globs | Shared file owner | Worktree / branch | Handoff path | Merge order | Verification command |
17
17
  | --- | --- | --- | --- | --- | --- | --- | --- |
18
- | coordinator | coding-agent-harness/planning/tasks/** | AGENTS.md, CLAUDE.md, coding-agent-harness/governance/generated/Harness-Ledger.md until closeout | coordinator | current | progress.md | 1 | harness check --profile target-project . |
18
+ | coordinator | {{paths.harnessRoot}}/planning/tasks/** | AGENTS.md, CLAUDE.md, {{paths.harnessRoot}}/governance/generated/Harness-Ledger.md until closeout | coordinator | current | progress.md | 1 | harness check --profile target-project . |
@@ -9,14 +9,14 @@ task:
9
9
  entrypoints:
10
10
  newTask:
11
11
  type: template
12
- writes: [coding-agent-harness/planning/tasks/**]
13
- reads: [coding-agent-harness/planning/tasks/**/lesson_candidates.md]
12
+ writes: [{{paths.tasksRoot}}/**]
13
+ reads: [{{paths.tasksRoot}}/**/lesson_candidates.md]
14
14
  audit: true
15
15
  templates:
16
16
  prompt: templates/prompt.md
17
17
  writeScopes:
18
18
  taskDocs:
19
- path: coding-agent-harness/planning/tasks/**
19
+ path: {{paths.tasksRoot}}/**
20
20
  access: write
21
21
  audit:
22
22
  manifestRequired: true
@@ -8,7 +8,7 @@ task:
8
8
  entrypoints:
9
9
  newTask:
10
10
  type: template
11
- writes: [coding-agent-harness/planning/**]
11
+ writes: [{{paths.planningRoot}}/**]
12
12
  audit: true
13
13
  templates:
14
14
  taskPlanAppend: templates/task_plan.append.md
@@ -21,5 +21,5 @@ audit:
21
21
  evidenceFiles: [preset-audit.json]
22
22
  writeScopes:
23
23
  planningDocs:
24
- path: coding-agent-harness/planning/**
24
+ path: {{paths.planningRoot}}/**
25
25
  access: write
@@ -3,6 +3,6 @@
3
3
  | Field | Value |
4
4
  | --- | --- |
5
5
  | Module Key | {{moduleKey}} |
6
- | Module Plan | coding-agent-harness/planning/modules/{{moduleKey}}/module_plan.md |
6
+ | Module Plan | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/module_plan.md |
7
7
 
8
8
  Keep shared module decisions in the module plan or module context files. Keep task-specific evidence in this task directory.
@@ -12,6 +12,6 @@ Read these module-level entry points before changing shared module behavior. Con
12
12
 
13
13
  | Reference | Path | Why / When |
14
14
  | --- | --- | --- |
15
- | Module brief | coding-agent-harness/planning/modules/{{moduleKey}}/brief.md | Start here for the module purpose and current scope. |
16
- | Module plan | coding-agent-harness/planning/modules/{{moduleKey}}/module_plan.md | Use this for module steps, active task links, and handoff state. |
17
- | Module visual map | coding-agent-harness/planning/modules/{{moduleKey}}/visual_map.md | Inspect when the change affects module sequencing or dependencies. |
15
+ | Module brief | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/brief.md | Start here for the module purpose and current scope. |
16
+ | Module plan | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/module_plan.md | Use this for module steps, active task links, and handoff state. |
17
+ | Module visual map | {{paths.harnessRoot}}/planning/modules/{{moduleKey}}/visual_map.md | Inspect when the change affects module sequencing or dependencies. |
@@ -5,7 +5,12 @@ import path from "node:path";
5
5
 
6
6
  const context = JSON.parse(fs.readFileSync(process.env.HARNESS_PRESET_CONTEXT, "utf8"));
7
7
  const release = String(context.inputs.release || "").trim();
8
- const releaseRoot = path.join(context.targetRoot, "coding-agent-harness/governance/releases", release);
8
+ const governanceRoot = context.paths?.governanceRoot;
9
+ if (!governanceRoot) {
10
+ console.error("release-closeout check requires structure-aware context.paths from the preset runner");
11
+ process.exit(2);
12
+ }
13
+ const releaseRoot = path.join(context.targetRoot, governanceRoot, "releases", release);
9
14
  const required = ["INDEX.md", "task-aggregate.json", "task-archive-plan.md", "public-summary.md", "public-redaction-report.json"];
10
15
  const missing = required.filter((file) => !fs.existsSync(path.join(releaseRoot, file)));
11
16
  if (missing.length) {
@@ -47,7 +47,7 @@ templateValues:
47
47
  entrypoints:
48
48
  newTask:
49
49
  type: template
50
- writes: [coding-agent-harness/planning/tasks/**]
50
+ writes: [{{paths.tasksRoot}}/**]
51
51
  audit: true
52
52
  templates:
53
53
  taskPlanAppend: templates/task_plan.append.md
@@ -57,20 +57,20 @@ entrypoints:
57
57
  plan:
58
58
  type: script
59
59
  command: scripts/generate-release-package.mjs
60
- writes: [coding-agent-harness/governance/releases/**]
61
- reads: [coding-agent-harness/planning/tasks/**]
60
+ writes: [{{paths.governanceRoot}}/releases/**]
61
+ reads: [{{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
62
62
  audit: true
63
63
  scaffold:
64
64
  type: script
65
65
  command: scripts/generate-release-package.mjs
66
- writes: [coding-agent-harness/governance/releases/**]
67
- reads: [coding-agent-harness/planning/tasks/**]
66
+ writes: [{{paths.governanceRoot}}/releases/**]
67
+ reads: [{{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
68
68
  audit: true
69
69
  check:
70
70
  type: check
71
71
  command: checks/check-release-package.mjs
72
- writes: [coding-agent-harness/governance/releases/**]
73
- reads: [coding-agent-harness/governance/releases/**, coding-agent-harness/planning/tasks/**]
72
+ writes: [{{paths.governanceRoot}}/releases/**]
73
+ reads: [{{paths.governanceRoot}}/releases/**, {{paths.tasksRoot}}/**, {{paths.modulesRoot}}/**]
74
74
  audit: true
75
75
  evidence:
76
76
  bundleDir: artifacts/preset
@@ -93,8 +93,8 @@ audit:
93
93
  evidenceFiles: [preset-audit.json, preset-manifest.json, write-scope.json]
94
94
  writeScopes:
95
95
  taskDocs:
96
- path: coding-agent-harness/planning/tasks/**
96
+ path: {{paths.tasksRoot}}/**
97
97
  access: write
98
98
  releasePackage:
99
- path: coding-agent-harness/governance/releases/**
99
+ path: {{paths.governanceRoot}}/releases/**
100
100
  access: write
@@ -4,32 +4,70 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  const context = JSON.parse(fs.readFileSync(process.env.HARNESS_PRESET_CONTEXT, "utf8"));
7
+ const harnessCore = await import(context.runtime?.coreModule || new URL("../../../dist/harness-core.mjs", import.meta.url).href);
8
+ const {
9
+ normalizeTarget,
10
+ collectTasks: collectCoreTasks,
11
+ archiveBlockReason: sharedArchiveBlockReason,
12
+ } = harnessCore;
13
+
7
14
  const release = safeRelease(context.inputs.release);
8
15
  if (!release) {
9
16
  console.error("release-closeout requires inputs.release");
10
17
  process.exit(2);
11
18
  }
12
19
 
13
- const tasksRoot = path.join(context.targetRoot, "coding-agent-harness/planning/tasks");
20
+ const paths = context.paths || {};
21
+ if (!paths.tasksRoot || !paths.governanceRoot) {
22
+ console.error("release-closeout requires structure-aware context.paths from the preset runner");
23
+ process.exit(2);
24
+ }
14
25
  const releaseRoot = path.join(context.outputRoot, "release");
15
26
  fs.mkdirSync(releaseRoot, { recursive: true });
16
27
 
17
- const tasks = collectTasks(tasksRoot)
18
- .filter((task) => task.id !== context.task.id && task.preset !== "release-closeout")
28
+ const target = normalizeTarget(context.targetRoot);
29
+ const allTasks = collectCoreTasks(target)
30
+ .map(releaseTaskFromCore)
31
+ .filter((task) => task.id !== context.task.id && task.shortId !== context.task.id && task.preset !== "release-closeout")
19
32
  .sort((a, b) => a.id.localeCompare(b.id));
20
- const eligibleArchive = tasks.filter((task) => task.state === "done" && task.deletionState !== "superseded");
21
- const blockedArchive = tasks.filter((task) => task.state === "blocked" || task.queue === "blocked");
33
+ let selection;
34
+ try {
35
+ selection = selectTasks(allTasks, context.inputs, release);
36
+ } catch (error) {
37
+ console.error(error.message);
38
+ process.exit(2);
39
+ }
40
+
41
+ const tasks = selection.tasks.sort((a, b) => a.id.localeCompare(b.id));
42
+ if (tasks.length === 0) {
43
+ console.error("release-closeout selector matched no tasks");
44
+ process.exit(2);
45
+ }
46
+
47
+ const eligibility = tasks.map((task) => ({ task, reason: archiveBlockReason(task) }));
48
+ const eligibleArchive = eligibility.filter((item) => !item.reason).map((item) => item.task);
49
+ const notEligibleArchive = eligibility.filter((item) => item.reason);
50
+ const governanceRoot = paths.governanceRoot;
51
+ const destinationRoot = `${governanceRoot}/releases/${release}`;
52
+ const releasePackageRef = `${destinationRoot}/INDEX.md`;
22
53
 
23
54
  const aggregate = {
24
55
  schemaVersion: "release-closeout-aggregate/v1",
25
56
  release,
26
57
  generatedAt: new Date().toISOString(),
58
+ selector: selection.selector,
27
59
  summary: {
28
60
  totalTasks: tasks.length,
29
61
  doneTasks: tasks.filter((task) => task.state === "done").length,
30
62
  blockedTasks: tasks.filter((task) => task.state === "blocked").length,
31
63
  archiveEligibleTasks: eligibleArchive.length,
32
64
  },
65
+ matched: tasks.map(taskSummary),
66
+ excluded: selection.excluded.map((task) => ({ ...taskSummary(task), reason: "not-selected" })),
67
+ notEligible: [
68
+ ...selection.missing.map((id) => ({ id, reason: "task-not-found" })),
69
+ ...notEligibleArchive.map(({ task, reason }) => ({ ...taskSummary(task), reason })),
70
+ ],
33
71
  tasks: tasks.map((task) => ({
34
72
  id: task.id,
35
73
  title: task.title,
@@ -37,7 +75,7 @@ const aggregate = {
37
75
  preset: task.preset || "none",
38
76
  deletionState: task.deletionState,
39
77
  archiveEligible: eligibleArchive.some((candidate) => candidate.id === task.id),
40
- archiveBlockedReason: task.state === "blocked" ? "blocked tasks cannot be archived" : "",
78
+ archiveBlockedReason: archiveBlockReason(task),
41
79
  })),
42
80
  };
43
81
 
@@ -63,6 +101,11 @@ Generated by \`presets/release-closeout\` through the generic preset runner.
63
101
  const archivePlan = `# Release ${release} Task Archive Plan
64
102
 
65
103
  This plan is generated by the release-closeout preset. It does not archive tasks by itself.
104
+ Run the commands from the target project root after reviewing this package.
105
+
106
+ ## Eligible Archive Commands
107
+
108
+ ${eligibleArchive.length ? eligibleArchive.map((task) => `- ${archiveCommand(task, release, releasePackageRef)}`).join("\n") : "- none"}
66
109
 
67
110
  ## Eligible Tasks
68
111
 
@@ -70,7 +113,7 @@ ${eligibleArchive.length ? eligibleArchive.map((task) => `- ${task.id} - ${task.
70
113
 
71
114
  ## Not Eligible
72
115
 
73
- ${blockedArchive.length ? blockedArchive.map((task) => `- ${task.id} - blocked tasks cannot be archived`).join("\n") : "- none"}
116
+ ${aggregate.notEligible.length ? aggregate.notEligible.map((task) => `- ${task.id} - ${task.reason}`).join("\n") : "- none"}
74
117
  `;
75
118
 
76
119
  const publicSummaryRaw = `# Release ${release} Public Summary
@@ -101,7 +144,6 @@ write("task-aggregate.json", `${JSON.stringify(aggregate, null, 2)}\n`);
101
144
  write("public-summary.md", publicSummary);
102
145
  write("public-redaction-report.json", `${JSON.stringify(redactionReport, null, 2)}\n`);
103
146
 
104
- const destinationRoot = `coding-agent-harness/governance/releases/${release}`;
105
147
  fs.writeFileSync(context.materializationManifestPath, `${JSON.stringify({
106
148
  schemaVersion: "preset-materialization/v1",
107
149
  status: "ok",
@@ -119,27 +161,264 @@ function write(name, content) {
119
161
  fs.writeFileSync(path.join(releaseRoot, name), content.endsWith("\n") ? content : `${content}\n`);
120
162
  }
121
163
 
122
- function collectTasks(root) {
123
- if (!fs.existsSync(root)) return [];
124
- const taskPlans = walk(root).filter((file) => path.basename(file) === "task_plan.md");
125
- return taskPlans.map((taskPlanPath) => {
126
- const taskDir = path.dirname(taskPlanPath);
127
- const taskPlan = read(taskPlanPath);
128
- const progress = read(path.join(taskDir, "progress.md"));
129
- const tombstone = parseTombstone(taskPlan);
164
+ function selectTasks(tasks, inputs, release) {
165
+ const hasTaskList = Boolean(inputs.taskList);
166
+ const hasTaskQuery = Boolean(String(inputs.taskQuery || "").trim());
167
+ if (!hasTaskList && !hasTaskQuery) throw new Error("release-closeout requires --task-list or --task-query");
168
+ if (hasTaskList && hasTaskQuery) throw new Error("release-closeout accepts only one selector: --task-list or --task-query");
169
+
170
+ if (hasTaskList) {
171
+ const taskList = inputs.taskList;
172
+ if (!taskList || typeof taskList !== "object" || Array.isArray(taskList)) {
173
+ throw new Error("--task-list must be a JSON object");
174
+ }
175
+ if (taskList.schemaVersion !== "release-closeout-task-list/v1") {
176
+ throw new Error("--task-list schemaVersion must be release-closeout-task-list/v1");
177
+ }
178
+ if (taskList.release && taskList.release !== release) {
179
+ throw new Error(`--task-list release ${taskList.release} does not match --release ${release}`);
180
+ }
181
+ const requestedTaskIds = Array.isArray(taskList.taskIds) ? taskList.taskIds.map(normalizeTaskListId).filter(Boolean) : [];
182
+ if (requestedTaskIds.length === 0) throw new Error("--task-list requires non-empty taskIds");
183
+ const selected = resolveTaskReferences(requestedTaskIds, tasks);
184
+ const selectedIds = new Set(selected.map((task) => task.id));
130
185
  return {
131
- id: path.basename(taskDir),
132
- title: titleFrom(taskPlan, path.basename(taskDir)),
133
- preset: metadataLine(taskPlan, ["Task Preset", "Preset"]).toLowerCase(),
134
- state: parseState(progress),
135
- deletionState: tombstone.state || "active",
136
- queue: parseState(progress) === "blocked" ? "blocked" : "active",
137
- evidenceSnippet: progress.split(/\r?\n/).find((line) => /\/Users\/|\/Volumes\/|file:\/\//.test(line)) || "",
138
- tombstone,
186
+ selector: {
187
+ type: "task-list",
188
+ schemaVersion: taskList.schemaVersion,
189
+ release,
190
+ sourcePath: taskList.sourcePath || "",
191
+ taskIds: requestedTaskIds,
192
+ resolvedTaskIds: selected.map((task) => task.id),
193
+ },
194
+ tasks: selected,
195
+ excluded: tasks.filter((task) => !selectedIds.has(task.id)),
196
+ missing: [],
139
197
  };
198
+ }
199
+
200
+ const query = String(inputs.taskQuery || "").trim();
201
+ const terms = parseTaskQuery(query);
202
+ const selected = tasks.filter((task) => terms.every((term) => matchesQueryTerm(task, term)));
203
+ const selectedIds = new Set(selected.map((task) => task.id));
204
+ return {
205
+ selector: {
206
+ type: "task-query",
207
+ query,
208
+ terms,
209
+ },
210
+ tasks: selected,
211
+ excluded: tasks.filter((task) => !selectedIds.has(task.id)),
212
+ missing: [],
213
+ };
214
+ }
215
+
216
+ function normalizeTaskListId(value) {
217
+ return String(value || "").trim();
218
+ }
219
+
220
+ function resolveTaskReferences(references, tasks) {
221
+ return references.map((reference) => {
222
+ const canonical = tasks.filter((task) => task.id === reference);
223
+ const candidates = canonical.length > 0 ? canonical : tasks.filter((task) => taskReferenceMatches(task, reference));
224
+ if (candidates.length === 0) {
225
+ throw new Error(`Missing task reference: ${reference}`);
226
+ }
227
+ if (candidates.length > 1) {
228
+ throw new Error(`Ambiguous task reference: ${reference} matched ${candidates.map((task) => task.id).join(", ")}`);
229
+ }
230
+ return candidates[0];
140
231
  });
141
232
  }
142
233
 
234
+ function taskReferenceMatches(task, reference) {
235
+ if (/^(TASKS|MODULES|EXTERNAL)\//.test(reference)) return task.id === reference;
236
+ return task.shortId === reference || task.legacyId === reference || task.id.endsWith(`/${reference}`);
237
+ }
238
+
239
+ function parseTaskQuery(query) {
240
+ const rawTerms = query.split(/\s+/).map((item) => item.trim()).filter(Boolean);
241
+ if (rawTerms.length === 0) throw new Error("--task-query requires at least one selector term");
242
+ return rawTerms.map((term) => {
243
+ let match = term.match(/^date:(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
244
+ if (match) return { type: "date", from: match[1], to: match[2] };
245
+ match = term.match(/^state:([A-Za-z0-9_-]+)$/);
246
+ if (match) return { type: "state", value: normalizeState(match[1]) };
247
+ match = term.match(/^preset:([A-Za-z0-9._-]+)$/);
248
+ if (match) return { type: "preset", value: match[1].toLowerCase() };
249
+ match = term.match(/^module:([A-Za-z0-9._-]+)$/);
250
+ if (match) return { type: "module", value: match[1].toLowerCase() };
251
+ throw new Error(`Unsupported --task-query term: ${term}`);
252
+ });
253
+ }
254
+
255
+ function matchesQueryTerm(task, term) {
256
+ if (term.type === "date") {
257
+ const date = task.date || "";
258
+ return date >= term.from && date <= term.to;
259
+ }
260
+ if (term.type === "state") return task.state === term.value;
261
+ if (term.type === "preset") return (task.preset || "none") === term.value;
262
+ if (term.type === "module") return (task.module || "").toLowerCase() === term.value;
263
+ return false;
264
+ }
265
+
266
+ function taskSummary(task) {
267
+ return {
268
+ id: task.id,
269
+ title: task.title,
270
+ state: task.state,
271
+ preset: task.preset || "none",
272
+ deletionState: task.deletionState,
273
+ module: task.module || "",
274
+ shortId: task.shortId || "",
275
+ };
276
+ }
277
+
278
+ function archiveBlockReason(task) {
279
+ return sharedArchiveBlockReason(task, { archivedBy: task.reviewConfirmation?.reviewer || "" });
280
+ }
281
+
282
+ function releaseTaskFromCore(task) {
283
+ const shortId = task.shortId || task.id.split("/").at(-1) || "";
284
+ return {
285
+ ...task,
286
+ preset: task.taskPreset || "none",
287
+ date: shortId.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || "",
288
+ queue: (task.taskQueues || []).includes("blocked") ? "blocked" : "active",
289
+ hasOpenBlockingFindings: (task.risks || []).some((risk) => String(risk.open) !== "no" && (String(risk.blocksRelease) === "yes" || ["P0", "P1", "P2"].includes(String(risk.severity)))),
290
+ materialsIncomplete: task.materialsReady === false,
291
+ evidenceSnippet: "",
292
+ };
293
+ }
294
+
295
+ function archiveCommand(task, release, releasePackageRef) {
296
+ return [
297
+ "harness",
298
+ "task-archive",
299
+ quoteCli(task.id),
300
+ "--reason",
301
+ quoteCli(`release closeout ${release}`),
302
+ "--archived-by",
303
+ quoteCli(task.reviewConfirmation?.reviewer || ""),
304
+ "--archive-field",
305
+ quoteCli(`retention bucket=release:${release}`),
306
+ "--archive-field",
307
+ quoteCli(`release package=${releasePackageRef}`),
308
+ ".",
309
+ ].join(" ");
310
+ }
311
+
312
+ function quoteCli(value) {
313
+ return `"${String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
314
+ }
315
+
316
+ function taskRootsFromContext(targetRoot, paths) {
317
+ const roots = [];
318
+ const seen = new Set();
319
+ const addRoot = (kind, relativeRoot) => {
320
+ if (!relativeRoot) return;
321
+ const root = path.join(targetRoot, relativeRoot);
322
+ const key = path.resolve(root);
323
+ if (seen.has(key)) return;
324
+ seen.add(key);
325
+ roots.push({ kind, root, relativeRoot: toPosix(relativeRoot) });
326
+ };
327
+ addRoot("TASKS", paths.tasksRoot);
328
+ addRoot("MODULES", paths.modulesRoot);
329
+ addRoot("EXTERNAL", paths.externalRoot);
330
+ if (Array.isArray(paths.taskRoots)) {
331
+ for (const relativeRoot of paths.taskRoots) addRoot(inferTaskRootKind(relativeRoot, paths), relativeRoot);
332
+ }
333
+ return roots;
334
+ }
335
+
336
+ function inferTaskRootKind(relativeRoot, paths) {
337
+ const normalized = toPosix(relativeRoot);
338
+ if (normalized === toPosix(paths.tasksRoot || "")) return "TASKS";
339
+ if (normalized === toPosix(paths.modulesRoot || "")) return "MODULES";
340
+ if (normalized === toPosix(paths.externalRoot || "")) return "EXTERNAL";
341
+ return "EXTERNAL";
342
+ }
343
+
344
+ function collectTasks(roots) {
345
+ const tasks = [];
346
+ for (const rootInfo of roots) {
347
+ if (!fs.existsSync(rootInfo.root)) continue;
348
+ const taskPlans = walk(rootInfo.root).filter((file) => path.basename(file) === "task_plan.md");
349
+ for (const taskPlanPath of taskPlans) {
350
+ const taskDir = path.dirname(taskPlanPath);
351
+ const identity = taskIdentity(rootInfo, taskDir);
352
+ if (!identity) continue;
353
+ const taskPlan = read(taskPlanPath);
354
+ const progress = read(path.join(taskDir, "progress.md"));
355
+ const review = read(path.join(taskDir, "review.md"));
356
+ const index = read(path.join(taskDir, "INDEX.md"));
357
+ const budget = parseBudget(taskPlan);
358
+ const state = parseState(progress);
359
+ const tombstone = parseTombstone(taskPlan);
360
+ tasks.push({
361
+ ...identity,
362
+ title: titleFrom(taskPlan, identity.shortId),
363
+ preset: metadataLine(taskPlan, ["Task Preset", "Preset"]).toLowerCase(),
364
+ state,
365
+ deletionState: tombstone.state || "active",
366
+ queue: state === "blocked" ? "blocked" : "active",
367
+ budget,
368
+ hasOpenBlockingFindings: hasOpenBlockingReviewFinding(review),
369
+ materialsIncomplete: budget !== "simple" && hasIncompleteMaterials(taskDir),
370
+ reviewConfirmation: parseReviewConfirmation(index, identity.id),
371
+ evidenceSnippet: progress.split(/\r?\n/).find((line) => /\/Users\/|\/Volumes\/|file:\/\//.test(line)) || "",
372
+ tombstone,
373
+ });
374
+ }
375
+ }
376
+ return tasks;
377
+ }
378
+
379
+ function taskIdentity(rootInfo, taskDir) {
380
+ const relative = toPosix(path.relative(rootInfo.root, taskDir));
381
+ if (!relative || relative.startsWith("..")) return null;
382
+ const segments = relative.split("/").filter(Boolean);
383
+ const shortId = segments.at(-1) || path.basename(taskDir);
384
+ const date = shortId.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || "";
385
+ if (rootInfo.kind === "TASKS") {
386
+ return {
387
+ id: `TASKS/${relative}`,
388
+ legacyId: shortId,
389
+ shortId,
390
+ date,
391
+ module: "",
392
+ rootKind: "TASKS",
393
+ };
394
+ }
395
+ if (rootInfo.kind === "MODULES") {
396
+ if (segments.length < 3 || segments[1] !== "tasks") return null;
397
+ const moduleKey = segments[0];
398
+ const taskRelative = segments.slice(2).join("/");
399
+ return {
400
+ id: `MODULES/${moduleKey}/${taskRelative}`,
401
+ legacyId: shortId,
402
+ shortId,
403
+ date,
404
+ module: moduleKey,
405
+ rootKind: "MODULES",
406
+ };
407
+ }
408
+ return {
409
+ id: `EXTERNAL/${toPosix(path.basename(rootInfo.root))}/${relative}`,
410
+ legacyId: shortId,
411
+ shortId,
412
+ date,
413
+ module: "",
414
+ rootKind: "EXTERNAL",
415
+ };
416
+ }
417
+
418
+ function toPosix(value) {
419
+ return String(value || "").split(path.sep).join("/");
420
+ }
421
+
143
422
  function walk(root) {
144
423
  const files = [];
145
424
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
@@ -171,10 +450,40 @@ function metadataLine(content, labels) {
171
450
 
172
451
  function parseState(progress) {
173
452
  const match = String(progress || "").match(/^##\s*(?:Current Status|Status|状态)\s*[::]?\s*(?:\n\s*)?([^\n]+)/im);
174
- const raw = (match ? match[1] : "").trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
453
+ return normalizeState(match ? match[1] : "");
454
+ }
455
+
456
+ function normalizeState(value) {
457
+ const raw = String(value || "").trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_");
175
458
  return ["not_started", "planned", "in_progress", "review", "blocked", "done"].includes(raw) ? raw : "unknown";
176
459
  }
177
460
 
461
+ function parseBudget(taskPlan) {
462
+ const match = String(taskPlan || "").match(/Selected budget\s*[::]\s*([A-Za-z0-9_-]+)/i);
463
+ return match ? match[1].trim().toLowerCase() : "standard";
464
+ }
465
+
466
+ function hasOpenBlockingReviewFinding(review) {
467
+ const lines = String(review || "").split(/\r?\n/);
468
+ for (const line of lines) {
469
+ const row = line.trim();
470
+ if (!row.startsWith("|") || /---/.test(row) || /severity|finding/i.test(row)) continue;
471
+ const cells = row.slice(1, -1).split("|").map((cell) => cell.trim().toLowerCase());
472
+ const hasBlockingSeverity = cells.some((cell) => /^p[0-2]\b/.test(cell));
473
+ const openOrBlocking = cells.some((cell) => /^(yes|true|open|是|开放)$/.test(cell));
474
+ if (hasBlockingSeverity && openOrBlocking) return true;
475
+ }
476
+ return /^Open Blocking Findings\s*[::]\s*(yes|true|open|是)/im.test(review);
477
+ }
478
+
479
+ function hasIncompleteMaterials(taskDir) {
480
+ for (const fileName of ["task_plan.md", "progress.md", "review.md", "INDEX.md"]) {
481
+ const content = read(path.join(taskDir, fileName));
482
+ if (/Materials\s*(?:Status|Ready)\s*[::]\s*(incomplete|missing|no|false)/im.test(content)) return true;
483
+ }
484
+ return false;
485
+ }
486
+
178
487
  function parseTombstone(content) {
179
488
  const match = String(content || "").match(/^##\s*(?:Task Tombstone|任务墓碑)\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/im);
180
489
  if (!match) return { state: "active", fields: {} };
@@ -188,6 +497,59 @@ function parseTombstone(content) {
188
497
  return { state: fields.state || "soft-deleted", fields };
189
498
  }
190
499
 
500
+ function parseReviewConfirmation(content, taskId) {
501
+ const fields = markdownFieldsFromBlock(content, /^##\s*(?:Task Audit Metadata|任务审计元数据)\s*$/im);
502
+ if (fields.size === 0) return { confirmed: false, missingFields: ["Task Audit Metadata"] };
503
+ if (normalizeToken(fields.get("human review status")) !== "confirmed") return { confirmed: false, missingFields: [] };
504
+ const required = ["confirmation id", "confirmed at", "reviewer", "reviewer email", "confirm text", "evidence checked", "review commit sha", "audit status"];
505
+ const missing = required.filter((field) => !isConcreteAuditField(fields.get(field)));
506
+ const confirmText = fields.get("confirm text") || "";
507
+ if (isConcreteAuditField(confirmText) && !taskKeysMatch(confirmText, taskId)) missing.push("confirm text match");
508
+ const auditStatus = fields.get("audit status") || "";
509
+ if (isConcreteAuditField(auditStatus) && normalizeToken(auditStatus) !== "committed") missing.push("audit status committed");
510
+ const commitSha = fields.get("review commit sha") || "";
511
+ if (isConcreteAuditField(commitSha) && !/^[0-9a-f]{7,40}$/i.test(commitSha)) missing.push("review commit sha valid");
512
+ return {
513
+ confirmed: missing.length === 0,
514
+ missingFields: missing,
515
+ confirmationId: fields.get("confirmation id") || "",
516
+ confirmedAt: fields.get("confirmed at") || "",
517
+ reviewer: fields.get("reviewer") || "",
518
+ reviewerEmail: fields.get("reviewer email") || "",
519
+ confirmText,
520
+ evidenceChecked: fields.get("evidence checked") || "",
521
+ commitSha,
522
+ };
523
+ }
524
+
525
+ function markdownFieldsFromBlock(content, headingPattern) {
526
+ const match = String(content || "").match(new RegExp(`${headingPattern.source}([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, headingPattern.flags));
527
+ const fields = new Map();
528
+ if (!match) return fields;
529
+ for (const line of match[1].split(/\r?\n/)) {
530
+ const row = line.trim();
531
+ if (!row.startsWith("|") || /---/.test(row) || /Field\s*\|\s*Value/i.test(row)) continue;
532
+ const cells = row.slice(1, -1).split("|").map((cell) => cell.trim());
533
+ if (cells.length >= 2) fields.set(cells[0].toLowerCase(), cells.slice(1).join("|").trim());
534
+ }
535
+ return fields;
536
+ }
537
+
538
+ function isConcreteAuditField(value) {
539
+ const raw = String(value || "").replace(/`/g, "").trim();
540
+ return Boolean(raw) && !/^(n\/a|na|none|pending(?:[-_ ].*)?|todo|tbd|\[.*\]|-|—|–|不适用|无|待定|\{\})$/i.test(raw) && !/\{\{[^}]+\}\}/.test(raw);
541
+ }
542
+
543
+ function normalizeToken(value) {
544
+ return String(value || "").trim().toLowerCase().replaceAll("_", "-").replace(/\s+/g, "-");
545
+ }
546
+
547
+ function taskKeysMatch(candidate, expected) {
548
+ const left = String(candidate || "").replace(/`/g, "").trim();
549
+ const right = String(expected || "").replace(/`/g, "").trim();
550
+ return left === right || right.endsWith(`/${left}`);
551
+ }
552
+
191
553
  function safeRelease(value) {
192
554
  const release = String(value || "").trim();
193
555
  return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(release) ? release : "";