coding-agent-harness 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/LICENSE +661 -21
  4. package/LICENSE-EXCEPTION.md +37 -0
  5. package/README.md +244 -87
  6. package/README.zh-CN.md +77 -35
  7. package/SKILL.md +32 -24
  8. package/docs-release/README.md +9 -5
  9. package/docs-release/architecture/overview.md +17 -5
  10. package/docs-release/architecture/overview.zh-CN.md +9 -5
  11. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  12. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  13. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  14. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  15. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  16. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  17. package/docs-release/architecture/system-explainer/README.md +67 -0
  18. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  19. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  20. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  21. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  22. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  23. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  24. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  25. package/docs-release/assets/dashboard-overview.png +0 -0
  26. package/docs-release/guides/agent-installation.en-US.md +39 -15
  27. package/docs-release/guides/agent-installation.md +43 -16
  28. package/docs-release/guides/contributing.md +100 -0
  29. package/docs-release/guides/contributing.zh-CN.md +99 -0
  30. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  31. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  32. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  33. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  34. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  35. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  36. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  37. package/docs-release/guides/migration-playbook.md +14 -15
  38. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  39. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  40. package/docs-release/guides/preset-development.md +238 -0
  41. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  42. package/docs-release/guides/repository-operating-models.md +5 -4
  43. package/docs-release/guides/task-state-machine.en-US.md +224 -0
  44. package/docs-release/guides/task-state-machine.md +231 -0
  45. package/docs-release/intl/en-US.md +1 -1
  46. package/docs-release/intl/zh-CN.md +1 -1
  47. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  48. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  49. package/package.json +10 -4
  50. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  51. package/presets/legacy-migration/preset.yaml +134 -0
  52. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  53. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  54. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  55. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  56. package/presets/legacy-migration/templates/review.seed.md +12 -0
  57. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  58. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  59. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  60. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  61. package/presets/lesson-sedimentation/preset.yaml +23 -0
  62. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  63. package/presets/module/preset.yaml +25 -0
  64. package/presets/module/templates/execution_strategy.append.md +8 -0
  65. package/presets/module/templates/task_plan.append.md +17 -0
  66. package/presets/standard-task/preset.yaml +31 -0
  67. package/presets/standard-task/templates/task_plan.append.md +7 -0
  68. package/references/adversarial-review-standard.md +2 -2
  69. package/references/agents-md-pattern.md +2 -2
  70. package/references/delivery-operating-model-standard.md +3 -3
  71. package/references/docs-directory-standard.md +6 -7
  72. package/references/harness-ledger.md +53 -96
  73. package/references/lessons-governance.md +88 -93
  74. package/references/module-parallel-standard.md +14 -14
  75. package/references/planning-loop.md +12 -6
  76. package/references/pull-request-standard.md +118 -0
  77. package/references/repo-governance-standard.md +11 -2
  78. package/references/review-routing-standard.md +7 -1
  79. package/references/ssot-governance.md +67 -59
  80. package/references/taskr-gap-analysis.md +600 -0
  81. package/references/walkthrough-closeout.md +7 -7
  82. package/scripts/check-harness.mjs +40 -301
  83. package/scripts/commands/dashboard-command.mjs +67 -0
  84. package/scripts/commands/migration-command.mjs +126 -0
  85. package/scripts/commands/preset-command.mjs +73 -0
  86. package/scripts/commands/task-command.mjs +328 -0
  87. package/scripts/harness.mjs +59 -260
  88. package/scripts/lib/capability-registry.mjs +82 -28
  89. package/scripts/lib/check-module-parallel.mjs +230 -0
  90. package/scripts/lib/check-profiles.mjs +90 -228
  91. package/scripts/lib/check-task-contracts.mjs +55 -0
  92. package/scripts/lib/core-shared.mjs +65 -2
  93. package/scripts/lib/dashboard-data.mjs +155 -24
  94. package/scripts/lib/dashboard-workbench.mjs +131 -12
  95. package/scripts/lib/dashboard-writer.mjs +20 -4
  96. package/scripts/lib/git-status-summary.mjs +46 -0
  97. package/scripts/lib/governance-index-generator.mjs +174 -0
  98. package/scripts/lib/governance-sync.mjs +611 -0
  99. package/scripts/lib/governance-table-boundary.mjs +175 -0
  100. package/scripts/lib/harness-core.mjs +6 -0
  101. package/scripts/lib/lesson-maintenance.mjs +36 -29
  102. package/scripts/lib/markdown-utils.mjs +33 -0
  103. package/scripts/lib/migration-planner.mjs +4 -6
  104. package/scripts/lib/migration-support.mjs +1 -1
  105. package/scripts/lib/phase-kind.mjs +50 -0
  106. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  107. package/scripts/lib/preset-engine.mjs +494 -0
  108. package/scripts/lib/preset-registry.mjs +776 -0
  109. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  110. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  111. package/scripts/lib/status-builder.mjs +88 -0
  112. package/scripts/lib/status-dashboard-renderer.mjs +105 -0
  113. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  114. package/scripts/lib/task-audit-metadata.mjs +385 -0
  115. package/scripts/lib/task-audit-migration.mjs +350 -0
  116. package/scripts/lib/task-completion-consistency.mjs +26 -0
  117. package/scripts/lib/task-index.mjs +93 -0
  118. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  119. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  120. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  121. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  122. package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
  123. package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
  124. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  125. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  126. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  127. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  128. package/scripts/lib/task-lifecycle.mjs +338 -477
  129. package/scripts/lib/task-metadata.mjs +118 -0
  130. package/scripts/lib/task-review-model.mjs +455 -0
  131. package/scripts/lib/task-scanner.mjs +193 -372
  132. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  133. package/scripts/postinstall.mjs +14 -0
  134. package/skills/preset-creator/SKILL.md +179 -0
  135. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  136. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  137. package/skills/preset-creator/references/complex-task-skeleton/brief.md +43 -0
  138. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  139. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  140. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  141. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  142. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  143. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  144. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  145. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  146. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  147. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  148. package/templates/AGENTS.md.template +24 -18
  149. package/templates/dashboard/assets/app-src/00-state.js +13 -0
  150. package/templates/dashboard/assets/app-src/10-router.js +5 -1
  151. package/templates/dashboard/assets/app-src/20-overview.js +18 -8
  152. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  153. package/templates/dashboard/assets/app-src/35-task-detail.js +286 -0
  154. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  155. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  156. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  157. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  158. package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
  159. package/templates/dashboard/assets/app.css +1501 -376
  160. package/templates/dashboard/assets/app.css.manifest.json +10 -0
  161. package/templates/dashboard/assets/app.js +1240 -101
  162. package/templates/dashboard/assets/app.manifest.json +2 -0
  163. package/templates/dashboard/assets/css-src/00-foundation.css +346 -0
  164. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  165. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  166. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  167. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  168. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +489 -0
  169. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  170. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  171. package/templates/dashboard/assets/i18n.js +263 -23
  172. package/templates/ledger/Harness-Ledger.md +13 -25
  173. package/templates/lessons/lesson-arch-process-change.md +1 -1
  174. package/templates/lessons/lesson-new-doc.md +1 -1
  175. package/templates/lessons/lesson-ref-change.md +1 -1
  176. package/templates/planning/INDEX.md +87 -0
  177. package/templates/planning/brief.md +1 -1
  178. package/templates/planning/execution_strategy.md +31 -0
  179. package/templates/planning/lesson_candidates.md +18 -6
  180. package/templates/planning/module_session_prompt.md +1 -0
  181. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  182. package/templates/planning/optional/references/INDEX.md +3 -3
  183. package/templates/planning/review.md +41 -0
  184. package/templates/planning/task_plan.md +5 -21
  185. package/templates/planning/visual_map.md +13 -9
  186. package/templates/planning/visual_map.simple.md +52 -0
  187. package/templates/reference/execution-workflow-standard.md +31 -3
  188. package/templates/reference/pull-request-standard.md +80 -0
  189. package/templates/reference/repo-governance-standard.md +7 -6
  190. package/templates/reference/review-routing-standard.md +6 -0
  191. package/templates/reference/walkthrough-standard.md +2 -1
  192. package/templates/verifier/verifier-output.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +25 -19
  194. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  195. package/templates-zh-CN/planning/INDEX.md +87 -0
  196. package/templates-zh-CN/planning/brief.md +1 -1
  197. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  198. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  199. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  200. package/templates-zh-CN/planning/review.md +41 -1
  201. package/templates-zh-CN/planning/task_plan.md +4 -44
  202. package/templates-zh-CN/planning/visual_map.md +14 -7
  203. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  204. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  205. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  206. package/templates-zh-CN/reference/execution-workflow-standard.md +33 -7
  207. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  208. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  209. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  210. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  211. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  212. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  213. package/docs-release/assets/dashboard-overview-en.png +0 -0
  214. package/scripts/smoke-dashboard.mjs +0 -92
  215. package/scripts/test-harness.mjs +0 -1395
  216. package/templates/ssot/Feature-SSoT.md +0 -43
  217. package/templates/ssot/Lessons-SSoT.md +0 -44
  218. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  219. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -1,13 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
- import { spawnSync } from "node:child_process";
5
4
  import {
6
- repoRoot,
7
5
  visualMapFile,
8
6
  legacyVisualRoadmapFile,
9
7
  lessonCandidatesFile,
10
- longRunningTaskContractFile,
11
8
  allowedTaskStates,
12
9
  allowedTaskBudgets,
13
10
  allowedPhaseStates,
@@ -17,73 +14,56 @@ import {
17
14
  toPosix,
18
15
  readFileSafe,
19
16
  readBundledTemplate,
20
- localizedTemplateSource,
21
17
  todayDate,
22
18
  localDate,
23
19
  datePrefix,
24
- nowTimestamp,
25
20
  normalizeTaskId,
26
21
  renderTaskTemplate,
27
22
  } from "./core-shared.mjs";
28
- import { verifyMigrationSession } from "./migration-planner.mjs";
29
23
  import { readCapabilityRegistry } from "./capability-registry.mjs";
24
+ import { readPresetPackage } from "./preset-registry.mjs";
25
+ import {
26
+ assertPresetWriteScope,
27
+ buildPresetContext,
28
+ evaluateTemplateValues,
29
+ resolvePresetInputs,
30
+ renderPresetResourceIndex,
31
+ renderPresetTaskTemplate,
32
+ } from "./preset-engine.mjs";
30
33
  import {
31
34
  collectTasks,
32
- collectReviewRisks,
33
- isBlockingReviewRisk,
34
35
  listTaskPlanPaths,
35
- parsePhases,
36
36
  parseTaskBudget,
37
- parseLessonCandidateStatus,
38
- isLessonCandidateDecisionComplete,
39
- parseReviewConfirmation,
40
- readVisualMapContractFile,
41
37
  taskIdForDirectory,
42
38
  } from "./task-scanner.mjs";
39
+ import { getColumn, firstColumn, updateMarkdownTableRow } from "./markdown-utils.mjs";
40
+ import { validateLifecycleTransition, validateReviewEntryGate } from "./task-lifecycle/review-gates.mjs";
41
+ import { advanceLifecyclePhase, autoRecordNoLessonCandidateDecision } from "./task-lifecycle/phase-sync.mjs";
42
+ import { confirmTaskReview as confirmTaskReviewWithContext } from "./task-lifecycle/review-confirm.mjs";
43
+ import { appendProgressLog } from "./task-lifecycle/text-utils.mjs";
44
+ import { buildScaffoldProvenance } from "./task-lifecycle/scaffold-provenance.mjs";
45
+ import { buildCreationTaskAudit } from "./task-audit-metadata.mjs";
43
46
  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
- }
47
+ renderAgentReviewSubmission,
48
+ replaceAgentReviewSubmission,
49
+ } from "./task-lifecycle/review-submission.mjs";
50
+ import {
51
+ appendLongRunningContractFile,
52
+ moduleTemplateFiles,
53
+ taskFilesForBudget,
54
+ } from "./task-lifecycle/template-files.mjs";
55
+ import {
56
+ planCreateTaskChanges,
57
+ refreshPresetCommandAudit,
58
+ } from "./task-lifecycle/create-task-helpers.mjs";
59
+ import {
60
+ beginGovernanceSync,
61
+ commitGovernanceSync,
62
+ governanceRelativePaths,
63
+ releaseGovernanceSync,
64
+ syncModuleStepGovernance,
65
+ syncTaskGovernance,
66
+ } from "./governance-sync.mjs";
87
67
 
88
68
  function taskRoot(target, taskId, { moduleKey = "" } = {}) {
89
69
  const normalizedTaskId = normalizeTaskId(taskId);
@@ -91,7 +71,7 @@ function taskRoot(target, taskId, { moduleKey = "" } = {}) {
91
71
  return path.join(target.docsRoot, "09-PLANNING/TASKS", normalizedTaskId);
92
72
  }
93
73
 
94
- function resolveTaskDirectory(target, taskRef) {
74
+ export function resolveTaskDirectory(target, taskRef) {
95
75
  const raw = String(taskRef || "").replace(/^docs\/09-PLANNING\//, "").replace(/^\/+/, "");
96
76
  if (!raw) throw new Error("Missing task id");
97
77
  const direct = raw.startsWith("TASKS/") || raw.startsWith("MODULES/") ? path.join(target.docsRoot, "09-PLANNING", raw) : "";
@@ -153,76 +133,10 @@ function normalizeTaskBudgetInput(budget) {
153
133
  throw new Error(`Invalid task budget: ${budget}. Expected one of: simple, standard, complex`);
154
134
  }
155
135
 
156
- function normalizeTaskPresetInput(preset) {
136
+ function normalizeTaskPresetInput(preset, { targetInput = "" } = {}) {
157
137
  const normalized = String(preset || "none").trim().toLowerCase().replaceAll("_", "-");
158
138
  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
- }
139
+ return readPresetPackage(normalized, { targetInput }).id;
226
140
  }
227
141
 
228
142
  function updateProgressState(content, state, locale) {
@@ -236,22 +150,6 @@ function updateProgressState(content, state, locale) {
236
150
  return `${content.trimEnd()}\n\n## Status\n\n${label}\n`;
237
151
  }
238
152
 
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
153
  function ensureDatePrefix(slug) {
256
154
  if (datePrefix.test(slug)) return slug;
257
155
  return `${localDate()}-${slug}`;
@@ -262,42 +160,119 @@ function bareSlug(datedId) {
262
160
  return datedId;
263
161
  }
264
162
 
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}`);
163
+ function automaticTaskSlug(seed) {
164
+ return normalizeTaskId(seed || "task").slice(0, 48).replace(/-+$/g, "") || "task";
165
+ }
166
+
167
+ function randomTaskSuffix() {
168
+ return crypto.randomBytes(4).toString("hex");
169
+ }
170
+
171
+ function resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey, automaticTaskId }) {
172
+ if (!automaticTaskId) {
173
+ const rawNormalized = normalizeTaskId(taskId || (presetPackage?.task?.defaultTaskId || ""));
174
+ const normalizedTaskId = ensureDatePrefix(rawNormalized);
175
+ if (!normalizedTaskId) throw new Error("Missing task id");
176
+ return { normalizedTaskId, semanticSlug: bareSlug(normalizedTaskId) };
271
177
  }
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);
178
+
179
+ const semanticSlug = automaticTaskSlug(title || presetPackage?.task?.defaultTaskId || "task");
180
+ for (let attempt = 0; attempt < 8; attempt += 1) {
181
+ const normalizedTaskId = `${localDate()}-${semanticSlug}-${randomTaskSuffix()}`;
182
+ if (!fs.existsSync(taskRoot(target, normalizedTaskId, { moduleKey }))) return { normalizedTaskId, semanticSlug };
183
+ }
184
+ throw new Error(`Unable to allocate automatic task id for: ${semanticSlug}`);
185
+ }
186
+
187
+ export function createTask(targetInput, taskId, { title = "", locale = "en-US", dryRun = false, moduleKey = "", budget = "standard", longRunning = false, preset = "", fromSession = "", presetArgs = [], automaticTaskId = false } = {}) {
188
+ const requestedPreset = preset || (moduleKey ? "module" : "");
189
+ const normalizedPreset = normalizeTaskPresetInput(requestedPreset, { targetInput });
190
+ const presetPackage = normalizedPreset === "none" ? null : readPresetPackage(normalizedPreset, { targetInput });
191
+ const presetInputs = presetPackage ? resolvePresetInputs(presetPackage, { cliArgs: presetArgs, fromSession, targetInput }) : null;
192
+ const target = normalizeTarget(presetInputs?.targetInput || targetInput);
193
+ if (presetInputs?.targetInput && targetInput && targetInput !== "." && path.resolve(targetInput) !== path.resolve(presetInputs.targetInput)) {
194
+ throw new Error(`--from-session target mismatch: session target is ${presetInputs.targetInput}`);
195
+ }
196
+ const normalizedBudget = normalizeTaskBudgetInput(budget);
197
+ if (presetPackage && !presetPackage.compatibleBudgets.includes(normalizedBudget)) throw new Error(`${normalizedPreset} preset requires --budget ${presetPackage.compatibleBudgets.join("|")}`);
198
+ if (presetPackage?.task?.projectLevelOnly === true && moduleKey) throw new Error(`${normalizedPreset} preset is project-level and cannot be combined with --module`);
199
+ if (presetPackage?.task?.requiresFromSession === true && !fromSession) throw new Error(`${normalizedPreset} preset requires --from-session`);
280
200
  const normalizedModuleKey = moduleKey ? normalizeTaskId(moduleKey) : "";
201
+ const identity = resolveTaskIdentity({ target, taskId, title, presetPackage, moduleKey: normalizedModuleKey, automaticTaskId });
202
+ const normalizedTaskId = identity.normalizedTaskId;
203
+ const semanticSlug = identity.semanticSlug;
281
204
  const normalizedLocale = normalizeLocale(locale || readCapabilityRegistry(target).locale);
282
- const normalizedBudget = normalizeTaskBudgetInput(budget);
283
205
  const taskTitle = title || (normalizedPreset === "legacy-migration" ? "Harness v1 legacy migration" : semanticSlug);
284
206
  const directory = taskRoot(target, normalizedTaskId, { moduleKey: normalizedModuleKey });
285
207
  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 })
208
+ const scaffoldProvenance = buildScaffoldProvenance({
209
+ taskId,
210
+ normalizedTaskId,
211
+ title,
212
+ locale: normalizedLocale,
213
+ budget: normalizedBudget,
214
+ longRunning,
215
+ moduleKey: normalizedModuleKey,
216
+ preset: normalizedPreset,
217
+ fromSession,
218
+ targetInput: presetInputs?.targetInput || targetInput,
219
+ automaticTaskId,
220
+ });
221
+ const baseTaskAudit = buildCreationTaskAudit(scaffoldProvenance, { projectRoot: target.projectRoot });
222
+ const evaluatedPresetValues = presetPackage ? evaluateTemplateValues(presetPackage, presetInputs.inputs, { taskId: normalizedTaskId, taskTitle, moduleKey: normalizedModuleKey }) : null;
223
+ const presetContext = presetPackage
224
+ ? buildPresetContext({ ...presetPackage, task: { ...(presetPackage.task || {}), kind: presetPackage.task?.kind || "general" } }, {
225
+ target,
226
+ taskDir: directory,
227
+ taskId: normalizedTaskId,
228
+ taskTitle,
229
+ resolvedInputs: presetInputs.inputs,
230
+ evaluatedValues: evaluatedPresetValues,
231
+ })
288
232
  : null;
233
+ const task = {
234
+ id: taskIdForDirectory(target, directory),
235
+ shortId: normalizedTaskId,
236
+ title: taskTitle,
237
+ module: normalizedModuleKey || null,
238
+ path: `TARGET:${toPosix(path.relative(target.projectRoot, directory))}`,
239
+ locale: normalizedLocale,
240
+ budget: normalizedBudget,
241
+ kind: presetContext?.kind || "general",
242
+ preset: normalizedPreset,
243
+ presetVersion: presetContext?.presetVersion || "",
244
+ presetAudit: presetContext?.audit || null,
245
+ migrationTargetLevel: presetContext?.migrationTargetLevel || "",
246
+ migrationAchievedLevel: presetContext?.migrationAchievedLevel || "",
247
+ evidenceBundle: presetContext?.evidenceBundle || "",
248
+ longRunning,
249
+ };
250
+ const plannedChanges = planCreateTaskChanges({
251
+ target,
252
+ directory,
253
+ normalizedModuleKey,
254
+ normalizedLocale,
255
+ normalizedBudget,
256
+ longRunning,
257
+ presetContext,
258
+ task,
259
+ });
260
+ const plannedGovernance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun: true });
261
+ const plannedWriteScopes = governanceRelativePaths([...plannedChanges, ...plannedGovernance.changes]);
289
262
  const changes = [];
263
+ const governanceContext = beginGovernanceSync(target, { operation: `new-task ${normalizedTaskId}`, dryRun, allowDirtyWorktree: true, allowedRelativePaths: plannedWriteScopes });
264
+ try {
290
265
  if (normalizedModuleKey) {
291
266
  const moduleDirectory = path.dirname(directory);
292
267
  for (const [destination, source] of moduleTemplateFiles({ locale: normalizedLocale })) {
293
268
  const destinationPath = path.join(moduleDirectory, destination);
294
269
  if (fs.existsSync(destinationPath)) continue;
295
- const sourcePath = path.join(repoRoot, source);
296
270
  changes.push({
297
271
  destination: toPosix(path.relative(target.projectRoot, destinationPath)),
298
272
  source,
299
273
  action: dryRun ? "would-create" : "create",
300
274
  });
275
+ if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
301
276
  if (dryRun) continue;
302
277
  fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
303
278
  fs.writeFileSync(
@@ -307,6 +282,13 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
307
282
  title: normalizedModuleKey,
308
283
  locale: normalizedLocale,
309
284
  budget: normalizedBudget,
285
+ moduleKey: normalizedModuleKey,
286
+ preset: normalizedPreset,
287
+ presetVersion: presetContext?.presetVersion || "",
288
+ evidenceBundle: presetContext?.evidenceBundle || "",
289
+ longRunning,
290
+ scaffoldProvenance,
291
+ taskAudit: buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot }),
310
292
  }),
311
293
  );
312
294
  }
@@ -317,12 +299,12 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
317
299
  });
318
300
  for (const [destination, source] of files) {
319
301
  const destinationPath = path.join(directory, destination);
320
- const sourcePath = path.join(repoRoot, source);
321
302
  changes.push({
322
303
  destination: toPosix(path.relative(target.projectRoot, destinationPath)),
323
304
  source,
324
305
  action: dryRun ? "would-create" : "create",
325
306
  });
307
+ if (presetPackage) assertPresetWriteScope(presetPackage, toPosix(path.relative(target.projectRoot, destinationPath)));
326
308
  if (dryRun) continue;
327
309
  fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
328
310
  fs.writeFileSync(
@@ -332,6 +314,18 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
332
314
  title: taskTitle,
333
315
  locale: normalizedLocale,
334
316
  budget: normalizedBudget,
317
+ moduleKey: normalizedModuleKey,
318
+ preset: normalizedPreset,
319
+ presetVersion: presetContext?.presetVersion || "",
320
+ evidenceBundle: presetContext?.evidenceBundle || "",
321
+ longRunning,
322
+ scaffoldProvenance: {
323
+ ...scaffoldProvenance,
324
+ templateSource: source,
325
+ },
326
+ taskAudit: destination === "INDEX.md"
327
+ ? buildCreationTaskAudit({ ...scaffoldProvenance, templateSource: source }, { projectRoot: target.projectRoot })
328
+ : baseTaskAudit,
335
329
  }), presetContext),
336
330
  );
337
331
  }
@@ -343,211 +337,61 @@ export function createTask(targetInput, taskId, { title = "", locale = "en-US",
343
337
  source: evidence.source,
344
338
  action: dryRun ? "would-create" : "create",
345
339
  });
340
+ assertPresetWriteScope(presetPackage, toPosix(evidence.relativePath));
346
341
  if (dryRun) continue;
347
342
  fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
348
343
  fs.writeFileSync(destinationPath, evidence.content);
349
344
  }
345
+ for (const resource of presetContext.resourceFiles || []) {
346
+ const destinationPath = path.join(target.projectRoot, resource.relativePath);
347
+ changes.push({
348
+ destination: toPosix(resource.relativePath),
349
+ source: resource.source,
350
+ action: dryRun ? "would-create" : "create",
351
+ });
352
+ assertPresetWriteScope(presetPackage, toPosix(resource.relativePath));
353
+ if (dryRun) continue;
354
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
355
+ fs.writeFileSync(destinationPath, resource.content);
356
+ }
357
+ for (const [kind, rows] of Object.entries(presetContext.resourceIndexRows || {})) {
358
+ if (!rows.length) continue;
359
+ const destination = kind === "references" ? "references/INDEX.md" : "artifacts/INDEX.md";
360
+ const destinationPath = path.join(directory, destination);
361
+ const relativePath = toPosix(path.relative(target.projectRoot, destinationPath));
362
+ changes.push({
363
+ destination: relativePath,
364
+ source: `preset-${kind}-index`,
365
+ action: dryRun ? "would-update" : "update",
366
+ });
367
+ assertPresetWriteScope(presetPackage, relativePath);
368
+ if (dryRun) continue;
369
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
370
+ const existing = fs.existsSync(destinationPath) ? fs.readFileSync(destinationPath, "utf8") : "";
371
+ fs.writeFileSync(destinationPath, renderPresetResourceIndex(existing, kind, rows));
372
+ }
350
373
  }
374
+ const governance = syncTaskGovernance(target, task, { event: "new-task", state: "planned", message: "task registered by CLI", dryRun });
375
+ changes.push(...governance.changes);
376
+ const commandWriteScopes = governanceRelativePaths(changes);
377
+ if (presetContext) {
378
+ refreshPresetCommandAudit(target, presetContext, { commandWriteScopes, dryRun });
379
+ task.presetAudit = presetContext.audit;
380
+ }
381
+ const commit = commitGovernanceSync(governanceContext, commandWriteScopes, {
382
+ message: `chore(harness): register task ${task.id}`,
383
+ });
351
384
  return {
352
385
  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
- },
386
+ task,
369
387
  changes,
388
+ governance: { ...governance, commit },
370
389
  };
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";
390
+ } finally {
391
+ releaseGovernanceSync(governanceContext);
447
392
  }
448
393
  }
449
394
 
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
395
  export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", state = "", message = "", evidence = "" } = {}) {
552
396
  const target = normalizeTarget(targetInput);
553
397
  const taskDir = resolveTaskDirectory(target, taskId);
@@ -556,111 +400,77 @@ export function updateTaskLifecycle(targetInput, taskId, { event = "task-log", s
556
400
  const normalizedState = state ? String(state).toLowerCase().replaceAll("-", "_") : "";
557
401
  if (normalizedState && !allowedTaskStates.has(normalizedState)) throw new Error(`Invalid task state: ${state}`);
558
402
  const currentTask = findTaskByDirectory(target, taskDir);
403
+ const canonicalTaskId = taskIdForDirectory(target, taskDir);
559
404
  const budget = parseTaskBudget(readFileSafe(path.join(taskDir, "task_plan.md")));
560
405
  validateLifecycleTransition({
561
406
  event,
562
407
  currentState: currentTask?.state || "unknown",
563
408
  budget,
564
409
  reviewContent: readFileSafe(path.join(taskDir, "review.md")),
410
+ indexContent: readFileSafe(path.join(taskDir, "INDEX.md")),
411
+ reviewTaskKey: canonicalTaskId,
412
+ projectRoot: target.projectRoot,
413
+ taskDir,
565
414
  });
566
415
  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
- };
416
+ const governanceContext = beginGovernanceSync(target, { operation: `${event} ${canonicalTaskId}` });
417
+ try {
418
+ let content = readFileSafe(progressPath);
419
+ if (normalizedState) content = updateProgressState(content, normalizedState, registry.locale);
420
+ content = appendProgressLog(content, { event, message, evidence });
421
+ fs.writeFileSync(progressPath, content.endsWith("\n") ? content : `${content}\n`);
422
+ const allowedPaths = [toPosix(path.relative(target.projectRoot, progressPath))];
423
+ const advancedPhasePath = advanceLifecyclePhase(target, taskDir, event);
424
+ if (advancedPhasePath) allowedPaths.push(advancedPhasePath);
425
+ if (event === "task-review") {
426
+ const reviewPath = path.join(taskDir, "review.md");
427
+ const reviewContent = readFileSafe(reviewPath);
428
+ fs.writeFileSync(
429
+ reviewPath,
430
+ replaceAgentReviewSubmission(
431
+ reviewContent,
432
+ renderAgentReviewSubmission({
433
+ target,
434
+ taskDir,
435
+ canonicalTaskId,
436
+ message,
437
+ evidence,
438
+ }),
439
+ ),
440
+ );
441
+ allowedPaths.push(toPosix(path.relative(target.projectRoot, reviewPath)));
442
+ const lessonDecisionPath = autoRecordNoLessonCandidateDecision(target, taskDir);
443
+ if (lessonDecisionPath) allowedPaths.push(lessonDecisionPath);
444
+ }
445
+ const task =
446
+ findTaskByDirectory(target, taskDir) ||
447
+ {
448
+ id: canonicalTaskId,
449
+ shortId: path.basename(taskDir),
450
+ title: canonicalTaskId,
451
+ path: `TARGET:${toPosix(path.relative(target.projectRoot, taskDir))}`,
452
+ state: normalizedState || currentTask?.state || "unknown",
453
+ };
454
+ const governanceState = normalizedState || task.state || currentTask?.state || "planned";
455
+ const governance = syncTaskGovernance(target, task, { event, state: governanceState, message, dryRun: false });
456
+ const commit = commitGovernanceSync(governanceContext, [...allowedPaths, ...governanceRelativePaths(governance.changes)], {
457
+ message: `chore(harness): advance task ${canonicalTaskId} to ${governanceState}`,
458
+ });
459
+ return {
460
+ event,
461
+ task,
462
+ governance: { ...governance, commit },
463
+ };
464
+ } finally {
465
+ releaseGovernanceSync(governanceContext);
466
+ }
575
467
  }
576
468
 
577
469
  export function confirmTaskReview(targetInput, taskId, { reviewer = "Human Reviewer", message = "", confirmText = "", evidence = "" } = {}) {
578
470
  const target = normalizeTarget(targetInput);
579
471
  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
- }
472
+ return confirmTaskReviewWithContext({ target, taskDir, findTaskByDirectory }, { reviewer, message, confirmText, evidence });
647
473
  }
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
474
  export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", completion = "", evidenceStatus = "" } = {}) {
665
475
  const target = normalizeTarget(targetInput);
666
476
  const taskDir = resolveTaskDirectory(target, taskId);
@@ -692,9 +502,17 @@ export function updateTaskPhase(targetInput, taskId, phaseId, { state = "", comp
692
502
  return next;
693
503
  });
694
504
  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 };
505
+ const governanceContext = beginGovernanceSync(target, { operation: `task-phase ${taskId} ${phaseId}` });
506
+ try {
507
+ content = phaseUpdate.content;
508
+ fs.writeFileSync(visualMapPath, content);
509
+ const commit = commitGovernanceSync(governanceContext, [toPosix(path.relative(target.projectRoot, visualMapPath))], {
510
+ message: `chore(harness): update task phase ${taskId} ${phaseId}`,
511
+ });
512
+ return { event: "task-phase", task: findTaskByDirectory(target, taskDir), phaseId, governance: { commit } };
513
+ } finally {
514
+ releaseGovernanceSync(governanceContext);
515
+ }
698
516
  }
699
517
 
700
518
  export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" } = {}) {
@@ -714,42 +532,85 @@ export function updateModuleStep(targetInput, moduleKey, stepId, { state = "" }
714
532
  return next;
715
533
  });
716
534
  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);
535
+ const governanceContext = beginGovernanceSync(target, { operation: `module-step ${normalizedModuleKey} ${stepId}` });
536
+ try {
537
+ content = stepUpdate.content;
538
+ fs.writeFileSync(modulePlanPath, content);
539
+
540
+ const registryPath = path.join(target.docsRoot, "09-PLANNING/Module-Registry.md");
541
+ if (fs.existsSync(registryPath)) {
542
+ let registry = readFileSafe(registryPath);
543
+ const registryUpdate = updateMarkdownTableRow(registry, /^(ID|模块 Key)$/i, (header, row) => {
544
+ const moduleIndex = firstColumn(header, ["Module", "模块", "模块 Key"]);
545
+ const taskPlanIndex = getColumn(header, "Task Plan");
546
+ const matchesModule = normalizeTaskId(row[moduleIndex] || "") === normalizedModuleKey;
547
+ const matchesPlan = taskPlanIndex >= 0 && String(row[taskPlanIndex] || "").includes(`/MODULES/${normalizedModuleKey}/`);
548
+ if (!matchesModule && !matchesPlan) return null;
549
+ const next = [...row];
550
+ const statusIndex = firstColumn(header, ["Status", "状态"]);
551
+ const updatedIndex = firstColumn(header, ["Updated", "更新时间"]);
552
+ const currentStepIndex = firstColumn(header, ["Current Step", "当前步骤"]);
553
+ const chineseRegistry = header.some((cell) => /模块 Key|模块名称|状态|更新时间/.test(cell));
554
+ if (statusIndex >= 0) {
555
+ next[statusIndex] = normalizedState === "done"
556
+ ? chineseRegistry ? "completed" : "merged"
557
+ : normalizedState === "in-progress" ? chineseRegistry ? "in-progress" : "active" : normalizedState;
558
+ }
559
+ if (currentStepIndex >= 0) next[currentStepIndex] = stepId;
560
+ if (updatedIndex >= 0) next[updatedIndex] = todayDate();
561
+ return next;
562
+ });
563
+ registry = registryUpdate.content;
564
+ fs.writeFileSync(registryPath, registry);
565
+ }
566
+ const governance = syncModuleStepGovernance(target, { moduleKey: normalizedModuleKey, stepId, state: normalizedState });
567
+ const commit = commitGovernanceSync(
568
+ governanceContext,
569
+ [
570
+ toPosix(path.relative(target.projectRoot, modulePlanPath)),
571
+ toPosix(path.relative(target.projectRoot, registryPath)),
572
+ ...governanceRelativePaths(governance.changes),
573
+ ],
574
+ { message: `chore(harness): update module ${normalizedModuleKey} step ${stepId}` },
575
+ );
576
+ return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState, governance: { ...governance, commit } };
577
+ } finally {
578
+ releaseGovernanceSync(governanceContext);
745
579
  }
746
- return { event: "module-step", moduleKey: normalizedModuleKey, stepId, state: normalizedState };
747
580
  }
748
581
 
749
- export function listLifecycleTasks(targetInput, { state = "", moduleKey = "" } = {}) {
582
+ export function listLifecycleTasks(targetInput, { state = "", moduleKey = "", queue = "", preset = "", review = "", lesson = "", search = "", missingMaterials = false } = {}) {
750
583
  const target = normalizeTarget(targetInput);
751
584
  let tasks = collectTasks(target);
752
585
  if (state) tasks = tasks.filter((task) => task.state === String(state).toLowerCase().replaceAll("-", "_"));
753
586
  if (moduleKey) tasks = tasks.filter((task) => task.module === normalizeTaskId(moduleKey));
587
+ if (queue) {
588
+ const normalizedQueue = queryToken(queue);
589
+ tasks = tasks.filter((task) => (task.taskQueues || []).map(queryToken).includes(normalizedQueue));
590
+ }
591
+ if (preset) tasks = tasks.filter((task) => queryToken(task.taskPreset || "none") === queryToken(preset));
592
+ if (review) tasks = tasks.filter((task) => queryToken(task.reviewStatus || "") === queryToken(review));
593
+ if (lesson) {
594
+ const needle = queryToken(lesson);
595
+ tasks = tasks.filter((task) => [task.lessonCandidateStatus, task.lessonCandidateReviewDecision, task.lessonCandidatePromotionState].some((value) => queryToken(value) === needle));
596
+ }
597
+ if (missingMaterials) tasks = tasks.filter((task) => !task.materialsReady);
598
+ if (search) {
599
+ const needle = String(search).toLowerCase();
600
+ tasks = tasks.filter((task) => [
601
+ task.id,
602
+ task.taskKey,
603
+ task.shortId,
604
+ task.title,
605
+ task.currentPath,
606
+ task.taskPlanPath,
607
+ task.module,
608
+ task.inferredModule,
609
+ ].some((value) => String(value || "").toLowerCase().includes(needle)));
610
+ }
754
611
  return { tasks };
755
612
  }
613
+
614
+ function queryToken(value) {
615
+ return String(value || "").trim().toLowerCase().replaceAll("_", "-");
616
+ }