coding-agent-harness 1.0.2 → 1.0.4

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