coding-agent-harness 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CONTRIBUTING.md +8 -4
  3. package/README.md +12 -2
  4. package/README.zh-CN.md +10 -2
  5. package/SKILL.md +14 -3
  6. package/dist/build-dist.mjs +19 -6
  7. package/dist/check-dist-observation.mjs +57 -29
  8. package/dist/check-harness.mjs +0 -1
  9. package/dist/check-import-graph.mjs +44 -27
  10. package/dist/check-lite-forbidden-surfaces.mjs +121 -0
  11. package/dist/check-no-ts-nocheck.mjs +7 -7
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +51 -9
  14. package/dist/commands/dashboard-command.mjs +52 -14
  15. package/dist/commands/migration-command.mjs +18 -8
  16. package/dist/commands/module-command.mjs +142 -0
  17. package/dist/commands/preset-command.mjs +51 -12
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +109 -52
  20. package/dist/harness.mjs +6 -304
  21. package/dist/lib/capability-registry.mjs +229 -53
  22. package/dist/lib/check-module-parallel.mjs +1 -6
  23. package/dist/lib/check-profiles.mjs +39 -46
  24. package/dist/lib/check-task-contracts.mjs +6 -4
  25. package/dist/lib/command-registry.mjs +248 -0
  26. package/dist/lib/core-shared.mjs +78 -3
  27. package/dist/lib/dashboard-data.mjs +203 -22
  28. package/dist/lib/dashboard-workbench.mjs +245 -21
  29. package/dist/lib/dashboard-writer.mjs +4 -1
  30. package/dist/lib/git-status-summary.mjs +0 -1
  31. package/dist/lib/governance-index-generator.mjs +7 -5
  32. package/dist/lib/governance-sync.mjs +46 -121
  33. package/dist/lib/governance-table-boundary.mjs +1 -14
  34. package/dist/lib/harness-core.mjs +4 -1
  35. package/dist/lib/harness-paths.mjs +115 -1
  36. package/dist/lib/impact-classifier.mjs +420 -0
  37. package/dist/lib/lesson-maintenance.mjs +1 -2
  38. package/dist/lib/markdown-utils.mjs +50 -1
  39. package/dist/lib/migration-planner.mjs +31 -16
  40. package/dist/lib/migration-support.mjs +5 -4
  41. package/dist/lib/module-registry.mjs +296 -0
  42. package/dist/lib/preset-audit-contracts.mjs +24 -1
  43. package/dist/lib/preset-engine.mjs +67 -29
  44. package/dist/lib/preset-registry.mjs +361 -71
  45. package/dist/lib/preset-runner.mjs +292 -26
  46. package/dist/lib/review-confirm-git-gate.mjs +73 -19
  47. package/dist/lib/status-builder.mjs +23 -8
  48. package/dist/lib/structure-migration.mjs +6 -4
  49. package/dist/lib/subagent-authorization-audit.mjs +8 -2
  50. package/dist/lib/task-archive-eligibility.mjs +65 -0
  51. package/dist/lib/task-audit-metadata.mjs +25 -11
  52. package/dist/lib/task-audit-migration.mjs +21 -14
  53. package/dist/lib/task-discovery-contract.mjs +32 -0
  54. package/dist/lib/task-index.mjs +3 -2
  55. package/dist/lib/task-lesson-candidates.mjs +1 -2
  56. package/dist/lib/task-lesson-sedimentation.mjs +310 -9
  57. package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
  58. package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
  59. package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
  60. package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
  61. package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
  62. package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
  63. package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
  64. package/dist/lib/task-lifecycle/template-files.mjs +2 -5
  65. package/dist/lib/task-lifecycle.mjs +116 -160
  66. package/dist/lib/task-metadata.mjs +10 -5
  67. package/dist/lib/task-preset-contract-drift.mjs +45 -0
  68. package/dist/lib/task-repository.mjs +192 -0
  69. package/dist/lib/task-review-model.mjs +36 -17
  70. package/dist/lib/task-scanner.mjs +74 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +186 -29
  73. package/dist/lib/types/check-profiles.js +1 -0
  74. package/dist/lib/types/impact.js +1 -0
  75. package/dist/lib/types/preset.js +1 -0
  76. package/dist/lib/types/task-lifecycle.js +1 -0
  77. package/dist/lib/types/task-scanner.js +1 -0
  78. package/dist/postinstall.mjs +2 -2
  79. package/dist/run-built-tests.mjs +10 -3
  80. package/docs-release/README.md +1 -0
  81. package/docs-release/architecture/document-contract-kernel/README.md +150 -0
  82. package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
  83. package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
  84. package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
  85. package/docs-release/architecture/overview.md +2 -2
  86. package/docs-release/architecture/overview.zh-CN.md +2 -2
  87. package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
  88. package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
  89. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
  90. package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
  91. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
  92. package/docs-release/architecture/system-explainer/README.md +1 -1
  93. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
  94. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
  95. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
  96. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
  97. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
  98. package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
  99. package/docs-release/guides/agent-installation.en-US.md +4 -6
  100. package/docs-release/guides/agent-installation.md +11 -8
  101. package/docs-release/guides/contributing.md +10 -3
  102. package/docs-release/guides/contributing.zh-CN.md +10 -3
  103. package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
  104. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
  105. package/docs-release/guides/migration-playbook.en-US.md +9 -6
  106. package/docs-release/guides/migration-playbook.md +9 -6
  107. package/docs-release/guides/preset-development.md +68 -2
  108. package/docs-release/guides/task-state-machine.en-US.md +8 -8
  109. package/docs-release/guides/task-state-machine.md +7 -7
  110. package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
  111. package/package.json +16 -12
  112. package/postinstall.mjs +37 -0
  113. package/presets/legacy-migration/preset.yaml +5 -5
  114. package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
  115. package/presets/lesson-sedimentation/preset.yaml +3 -3
  116. package/presets/module/preset.yaml +2 -2
  117. package/presets/module/templates/execution_strategy.append.md +1 -1
  118. package/presets/module/templates/task_plan.append.md +3 -3
  119. package/presets/release-closeout/checks/check-release-package.mjs +6 -1
  120. package/presets/release-closeout/preset.yaml +9 -9
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
  122. package/presets/release-closeout/templates/task_plan.append.md +5 -5
  123. package/presets/standard-task/preset.yaml +2 -2
  124. package/references/agents-md-pattern.md +23 -17
  125. package/references/lessons-governance.md +2 -2
  126. package/references/module-parallel-standard.md +3 -6
  127. package/references/ssot-governance.md +2 -2
  128. package/references/taskr-gap-analysis.md +3 -3
  129. package/run-dist.mjs +34 -0
  130. package/skills/preset-creator/SKILL.md +40 -8
  131. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  132. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  133. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  134. package/templates/AGENTS.md.template +28 -26
  135. package/templates/architecture/README.md +2 -2
  136. package/templates/architecture/service-catalog.md +2 -2
  137. package/templates/architecture/services/service-template.md +1 -1
  138. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  139. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  140. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  141. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  142. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  143. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  144. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  145. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  146. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  147. package/templates/dashboard/assets/app.css +928 -53
  148. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  149. package/templates/dashboard/assets/app.js +1071 -98
  150. package/templates/dashboard/assets/app.manifest.json +1 -0
  151. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  152. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  153. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  154. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  155. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  156. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  157. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  158. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  159. package/templates/dashboard/assets/i18n.js +166 -2
  160. package/templates/development/README.md +9 -9
  161. package/templates/development/cross-repo-debugging.md +3 -3
  162. package/templates/development/external-context/service-template.md +1 -1
  163. package/templates/development/external-source-packs/README.md +2 -2
  164. package/templates/integrations/README.md +4 -4
  165. package/templates/integrations/api-contract.md +1 -1
  166. package/templates/integrations/event-contract.md +1 -1
  167. package/templates/integrations/third-party/vendor-template.md +1 -1
  168. package/templates/integrations/webhook-contract.md +1 -1
  169. package/templates/ledger/Harness-Ledger.md +1 -1
  170. package/templates/modules/module_brief.md +50 -0
  171. package/templates/modules/module_plan.md +49 -0
  172. package/templates/modules/registry_view.md +9 -0
  173. package/templates/modules/session_prompt_pack.md +55 -0
  174. package/templates/planning/brief.md +32 -8
  175. package/templates/planning/module_brief.md +28 -3
  176. package/templates/planning/module_plan.md +26 -11
  177. package/templates/planning/module_session_prompt.md +11 -2
  178. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  179. package/templates/planning/review.md +1 -1
  180. package/templates/planning/visual_map.md +1 -1
  181. package/templates/reference/docs-library-standard.md +7 -7
  182. package/templates/reference/execution-workflow-standard.md +13 -0
  183. package/templates/reference/external-source-intake-standard.md +10 -10
  184. package/templates/reference/repo-governance-standard.md +1 -1
  185. package/templates/reference/review-routing-standard.md +4 -0
  186. package/templates/ssot/Module-Registry.md +4 -38
  187. package/templates/walkthrough/walkthrough-template.md +1 -1
  188. package/templates-zh-CN/AGENTS.md.template +27 -25
  189. package/templates-zh-CN/CLAUDE.md.template +1 -1
  190. package/templates-zh-CN/architecture/README.md +2 -2
  191. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  192. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  193. package/templates-zh-CN/development/README.md +9 -9
  194. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  195. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  196. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  197. package/templates-zh-CN/integrations/README.md +4 -4
  198. package/templates-zh-CN/integrations/api-contract.md +1 -1
  199. package/templates-zh-CN/integrations/event-contract.md +1 -1
  200. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  201. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  202. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  203. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  204. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  205. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  206. package/templates-zh-CN/modules/module_brief.md +47 -0
  207. package/templates-zh-CN/modules/module_plan.md +48 -0
  208. package/templates-zh-CN/modules/registry_view.md +9 -0
  209. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  210. package/templates-zh-CN/planning/INDEX.md +1 -0
  211. package/templates-zh-CN/planning/brief.md +26 -7
  212. package/templates-zh-CN/planning/module_brief.md +24 -2
  213. package/templates-zh-CN/planning/module_plan.md +35 -29
  214. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  215. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  216. package/templates-zh-CN/planning/review.md +1 -1
  217. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  218. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  219. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  220. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  221. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  222. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  223. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  224. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  225. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  226. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  227. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  228. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  229. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  230. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  231. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  232. package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
@@ -1,21 +1,21 @@
1
- // @ts-nocheck
2
1
  // Generic preset entrypoint runner. Domain logic belongs in preset packages.
3
2
  import fs from "node:fs";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import crypto from "node:crypto";
7
6
  import { spawnSync } from "node:child_process";
8
- import { normalizeTarget, readFileSafe, readJsonSafe, sanitizeDeep, toPosix, walkFiles, } from "./core-shared.mjs";
7
+ import { absoluteHarnessPathContext, harnessPathContext, normalizeTarget, readFileSafe, readJsonSafe, renderHarnessTemplate, sanitizeDeep, toPosix, walkFiles, } from "./core-shared.mjs";
9
8
  import { beginGovernanceSync, commitGovernanceSync, releaseGovernanceSync } from "./governance-sync.mjs";
10
9
  import { parseTaskMetadata } from "./task-metadata.mjs";
11
10
  import { taskIdForDirectory } from "./task-scanner.mjs";
12
11
  import { resolveTaskDirectory } from "./task-lifecycle.mjs";
13
- import { evaluateTemplateValues, assertPresetWriteScope } from "./preset-engine.mjs";
14
- import { buildPresetAudit, readPresetPackage } from "./preset-registry.mjs";
12
+ import { evaluateTemplateValues, assertPresetWriteScope, resolvePresetScopes } from "./preset-engine.mjs";
13
+ import { buildPresetAudit, buildPresetScriptPolicy, presetScriptTrustValid, readPresetPackage } from "./preset-registry.mjs";
15
14
  const materializationSchemaVersion = "preset-materialization/v1";
16
15
  const maxMaterializedFileBytes = 10 * 1024 * 1024;
17
16
  const maxMaterializedWrites = 500;
18
- export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", targetInput = ".", json = false } = {}) {
17
+ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", targetInput = ".", json = false, allowScripts = false, useCurrentPreset = false, reason = "" } = {}) {
18
+ void json;
19
19
  const target = normalizeTarget(targetInput);
20
20
  const preset = readPresetPackage(presetId, { targetInput });
21
21
  const entrypoint = preset.entrypoints?.[entrypointName];
@@ -23,6 +23,10 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
23
23
  throw new Error(`Preset ${preset.id} does not declare entrypoint: ${entrypointName}`);
24
24
  if (!["script", "check"].includes(entrypoint.type))
25
25
  throw new Error(`Preset entrypoint ${entrypointName} is not runnable by preset run`);
26
+ const scriptPolicy = buildPresetScriptPolicy(preset);
27
+ if (scriptPolicy.requiresTrustedSource && !allowScripts && !presetScriptTrustValid(preset)) {
28
+ throw new Error(`Preset entrypoint ${preset.id}.${entrypointName} executes trusted local code. Re-run with --allow-scripts if you trust this preset source.`);
29
+ }
26
30
  if (!taskRef)
27
31
  throw new Error("preset run requires --task <task-id>");
28
32
  const taskDir = resolveTaskDirectory(target, taskRef);
@@ -31,8 +35,11 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
31
35
  if (metadata.preset !== preset.id)
32
36
  throw new Error(`Task ${taskRef} was created by preset ${metadata.preset || "none"}, not ${preset.id}`);
33
37
  const taskId = taskIdForDirectory(target, taskDir);
34
- const resolvedInputs = readResolvedInputs(target, metadata);
35
- const values = evaluateTemplateValues(preset, resolvedInputs, { taskId, taskTitle: taskId, moduleKey: "" });
38
+ const audit = readPresetAudit(target, metadata);
39
+ const presetDrift = assessPresetDrift(audit, preset, { useCurrentPreset, reason });
40
+ const resolvedInputs = asRecord(audit.resolvedInputs);
41
+ const values = evaluateTemplateValues(preset, resolvedInputs, { taskId, taskTitle: taskId, moduleKey: "", target });
42
+ const resolvedScopes = resolvePresetScopes(preset, target);
36
43
  const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), `harness-preset-${preset.id}-${entrypointName}-`));
37
44
  const manifestPath = path.join(outputRoot, "materialization-manifest.json");
38
45
  const contextPath = path.join(outputRoot, "preset-context.json");
@@ -50,15 +57,20 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
50
57
  },
51
58
  targetRoot: target.projectRoot,
52
59
  targetRootPolicy: "read-only; direct target mutation before manifest materialization is a hard failure",
60
+ runtime: {
61
+ coreModule: new URL("./harness-core.mjs", import.meta.url).href,
62
+ },
53
63
  outputRoot,
54
64
  materializationManifestPath: manifestPath,
65
+ paths: harnessPathContext(target),
66
+ absolutePaths: absoluteHarnessPathContext(target),
55
67
  inputs: sanitizeDeep(resolvedInputs),
56
68
  values: sanitizeDeep(values),
57
69
  audit: buildPresetAudit(preset, {
58
70
  taskId,
59
71
  targetRoot: target.projectRoot,
60
72
  entrypoint: entrypointName,
61
- writeScopes: entrypoint.writes,
73
+ writeScopes: resolvedScopes.entrypoints[entrypointName] || entrypoint.writes,
62
74
  resolvedInputs,
63
75
  }),
64
76
  };
@@ -67,10 +79,7 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
67
79
  const script = spawnSync(process.execPath, [commandPath], {
68
80
  cwd: outputRoot,
69
81
  encoding: "utf8",
70
- env: {
71
- ...process.env,
72
- HARNESS_PRESET_CONTEXT: contextPath,
73
- },
82
+ env: presetScriptEnv(contextPath),
74
83
  timeout: 120000,
75
84
  maxBuffer: 10 * 1024 * 1024,
76
85
  });
@@ -82,7 +91,7 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
82
91
  const afterScriptSnapshot = targetSnapshot(target.projectRoot);
83
92
  assertSnapshotsEqual(beforeSnapshot, afterScriptSnapshot, "Preset script mutated target before materialization");
84
93
  const manifest = readMaterializationManifest(manifestPath);
85
- const materialization = validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, targetRoot: target.projectRoot });
94
+ const materialization = validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, target, entrypointName });
86
95
  const governanceContext = beginGovernanceSync(target, {
87
96
  operation: `preset-run ${preset.id}.${entrypointName}`,
88
97
  allowDirtyWorktree: true,
@@ -105,6 +114,7 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
105
114
  sha256: item.sha256,
106
115
  })),
107
116
  governance: { commit },
117
+ presetDrift,
108
118
  };
109
119
  }
110
120
  finally {
@@ -115,19 +125,253 @@ export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", ta
115
125
  fs.rmSync(outputRoot, { recursive: true, force: true });
116
126
  }
117
127
  }
118
- function readResolvedInputs(target, metadata) {
119
- const evidenceBundle = String(metadata.evidenceBundle || "").replace(/^TARGET:/, "").replace(/^\/+/, "");
128
+ export function runPresetAction(presetId, actionName, { taskRef = "", targetInput = ".", json = false, actionArgs = [], allowScripts = false, useCurrentPreset = false, reason = "" } = {}) {
129
+ void json;
130
+ const target = normalizeTarget(targetInput);
131
+ const preset = readPresetPackage(presetId, { targetInput });
132
+ const action = preset.actions?.[actionName];
133
+ if (!action)
134
+ throw new Error(`Preset ${preset.id} does not declare action: ${actionName}`);
135
+ if (action.type !== "script")
136
+ throw new Error(`Preset action ${actionName} is not runnable by preset action`);
137
+ if (action.taskRequired !== true)
138
+ throw new Error(`Preset action ${preset.id}.${actionName} requires taskRequired: true`);
139
+ if (!taskRef)
140
+ throw new Error("preset action requires --task <task-id>");
141
+ const scriptPolicy = buildPresetScriptPolicy(preset);
142
+ if (scriptPolicy.requiresTrustedSource && !allowScripts && !presetScriptTrustValid(preset)) {
143
+ throw new Error(`Preset action ${preset.id}.${actionName} executes trusted local code. Re-run with --allow-scripts if you trust this preset source.`);
144
+ }
145
+ const taskDir = resolveTaskDirectory(target, taskRef);
146
+ const taskPlan = readFileSafe(path.join(taskDir, "task_plan.md"));
147
+ const metadata = parseTaskMetadata(taskPlan);
148
+ if (metadata.preset !== preset.id)
149
+ throw new Error(`Task ${taskRef} was created by preset ${metadata.preset || "none"}, not ${preset.id}`);
150
+ const taskId = taskIdForDirectory(target, taskDir);
151
+ const taskPaths = taskPathContext(target, taskDir);
152
+ const actionInputs = resolveActionInputs(action, actionArgs);
153
+ const audit = readPresetAudit(target, metadata);
154
+ const presetDrift = assessPresetDrift(audit, preset, { useCurrentPreset, reason });
155
+ const creationInputs = asRecord(audit.resolvedInputs);
156
+ const values = evaluateTemplateValues(preset, creationInputs, { taskId, taskTitle: taskId, moduleKey: "", target });
157
+ const actionWriteScopes = resolveActionScopes(action.writes, { target, taskPaths, label: `${actionName} action write scope` });
158
+ const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), `harness-preset-${preset.id}-${actionName}-`));
159
+ const manifestPath = path.join(outputRoot, "materialization-manifest.json");
160
+ const contextPath = path.join(outputRoot, "preset-action-context.json");
161
+ let governanceContext = null;
162
+ try {
163
+ governanceContext = beginGovernanceSync(target, {
164
+ operation: `preset-action ${preset.id}.${actionName}`,
165
+ allowDirtyWorktree: true,
166
+ allowedRelativePaths: concreteActionWriteScopes(actionWriteScopes),
167
+ });
168
+ const beforeSnapshot = targetSnapshot(target.projectRoot);
169
+ const context = {
170
+ schemaVersion: "preset-action-context/v1",
171
+ preset: { id: preset.id, version: preset.version, source: preset.source, manifestSha256: preset.manifestSha256 },
172
+ action: { id: actionName, type: action.type },
173
+ task: {
174
+ id: taskId,
175
+ ref: taskRef,
176
+ dir: taskPaths.dir,
177
+ taskPlanPath: taskPaths.taskPlan,
178
+ paths: taskPaths,
179
+ },
180
+ targetRoot: target.projectRoot,
181
+ targetRootPolicy: "read-only; direct target mutation before manifest materialization is a hard failure",
182
+ outputRoot,
183
+ materializationManifestPath: manifestPath,
184
+ paths: harnessPathContext(target),
185
+ inputs: sanitizeDeep(actionInputs),
186
+ creationInputs: sanitizeDeep(creationInputs),
187
+ values: sanitizeDeep(values),
188
+ audit: buildPresetAudit(preset, {
189
+ taskId,
190
+ targetRoot: target.projectRoot,
191
+ entrypoint: `action:${actionName}`,
192
+ writeScopes: actionWriteScopes,
193
+ resolvedInputs: actionInputs,
194
+ }),
195
+ presetDrift,
196
+ };
197
+ fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`);
198
+ const commandPath = path.join(preset.directory, action.command || "");
199
+ const script = spawnSync(process.execPath, [commandPath], {
200
+ cwd: outputRoot,
201
+ encoding: "utf8",
202
+ env: presetScriptEnv(contextPath),
203
+ timeout: 120000,
204
+ maxBuffer: 128 * 1024,
205
+ });
206
+ if (script.error)
207
+ throw script.error;
208
+ if (script.status !== 0) {
209
+ throw new Error(`Preset action ${preset.id}.${actionName} failed with ${script.status}\n${boundedScriptOutput(script.stderr || script.stdout || "")}`.trim());
210
+ }
211
+ const afterScriptSnapshot = targetSnapshot(target.projectRoot);
212
+ assertSnapshotsEqual(beforeSnapshot, afterScriptSnapshot, "Preset script mutated target before materialization");
213
+ const manifest = readMaterializationManifest(manifestPath);
214
+ const materialization = validateMaterializationManifest(preset, {
215
+ type: action.type,
216
+ command: action.command,
217
+ templates: {},
218
+ writes: actionWriteScopes,
219
+ reads: action.reads,
220
+ audit: action.audit,
221
+ }, manifest, { outputRoot, target, entrypointName: actionName });
222
+ materializeWrites(target.projectRoot, materialization);
223
+ const commit = commitGovernanceSync(governanceContext, materialization.map((item) => item.destination), {
224
+ message: `chore(harness): run preset action ${preset.id} ${actionName}`,
225
+ });
226
+ return {
227
+ preset: preset.id,
228
+ action: actionName,
229
+ source: preset.source,
230
+ manifestSha256: preset.manifestSha256,
231
+ taskId,
232
+ status: manifest.status || "ok",
233
+ materialized: materialization.map((item) => ({
234
+ source: item.source,
235
+ destination: item.destination,
236
+ type: item.type,
237
+ sha256: item.sha256,
238
+ })),
239
+ governance: { commit },
240
+ presetDrift,
241
+ };
242
+ }
243
+ finally {
244
+ if (governanceContext)
245
+ releaseGovernanceSync(governanceContext);
246
+ fs.rmSync(outputRoot, { recursive: true, force: true });
247
+ }
248
+ }
249
+ function readPresetAudit(target, metadata) {
250
+ const evidenceBundle = normalizeTargetRelativePath(metadata.evidenceBundle || "", "Preset evidence bundle");
120
251
  if (!evidenceBundle)
121
252
  return {};
122
253
  const auditPath = path.join(target.projectRoot, evidenceBundle, "preset-audit.json");
123
- const audit = readJsonSafe(auditPath, {});
124
- return audit.resolvedInputs || {};
254
+ const audit = asRecord(readJsonSafe(auditPath, {}));
255
+ return audit;
256
+ }
257
+ function assessPresetDrift(audit, preset, { useCurrentPreset = false, reason = "" } = {}) {
258
+ const recorded = String(audit.manifestSha256 || "");
259
+ const current = String(preset.manifestSha256 || "");
260
+ if (!recorded || !current || recorded === current) {
261
+ return { detected: false, accepted: false, recordedManifestSha256: recorded, currentManifestSha256: current, reason: "" };
262
+ }
263
+ const normalizedReason = String(reason || "").trim();
264
+ if (!useCurrentPreset || !normalizedReason) {
265
+ throw new Error(`Preset manifest hash drift detected for ${preset.id}: recorded ${recorded}, current ${current}. Re-run with --use-current-preset --reason <why-current-semantics-are-intended>, or create a new task with the current preset.`);
266
+ }
267
+ return { detected: true, accepted: true, recordedManifestSha256: recorded, currentManifestSha256: current, reason: normalizedReason };
268
+ }
269
+ function resolveActionInputs(action, cliArgs) {
270
+ const remaining = [...cliArgs];
271
+ const inputs = {};
272
+ for (const [name, declaration] of Object.entries(action.inputs || {})) {
273
+ inputs[name] = resolveActionInputValue(name, declaration, remaining);
274
+ }
275
+ if (remaining.length > 0)
276
+ throw new Error(`Unknown action argument: ${remaining[0]}`);
277
+ return inputs;
278
+ }
279
+ function resolveActionInputValue(name, declaration, remaining) {
280
+ const flag = declaration.flag || name;
281
+ const index = remaining.indexOf(flag);
282
+ if (declaration.type === "flag") {
283
+ if (index >= 0) {
284
+ remaining.splice(index, 1);
285
+ return true;
286
+ }
287
+ return Boolean(declaration.default);
288
+ }
289
+ if (index < 0) {
290
+ if (declaration.required)
291
+ throw new Error(`Missing required action input ${flag}`);
292
+ return declaration.default ?? "";
293
+ }
294
+ const value = remaining[index + 1] || "";
295
+ if (!value || value.startsWith("--"))
296
+ throw new Error(`Missing value for action input ${flag}`);
297
+ remaining.splice(index, 2);
298
+ if (String(value).includes("\0"))
299
+ throw new Error(`Action input ${flag} contains NUL byte`);
300
+ if (declaration.type === "text") {
301
+ if (String(value).length > 4096)
302
+ throw new Error(`Action input ${flag} exceeds text length limit`);
303
+ return String(value);
304
+ }
305
+ if (declaration.type === "json-file") {
306
+ const filePath = path.resolve(String(value));
307
+ if (!fs.existsSync(filePath))
308
+ throw new Error(`Action input file not found for ${flag}: ${value}`);
309
+ const stat = fs.statSync(filePath);
310
+ if (!stat.isFile())
311
+ throw new Error(`Action input file must be a file for ${flag}: ${value}`);
312
+ if (stat.size > 1024 * 1024)
313
+ throw new Error(`Action input file exceeds size limit for ${flag}: ${value}`);
314
+ let readError = null;
315
+ const parsed = readJsonSafe(filePath, null, { onError: (error) => { readError = error; } });
316
+ if (!isRecord(parsed))
317
+ throw new Error(`Invalid action JSON input ${flag}: ${errorMessage(readError)}`);
318
+ return parsed;
319
+ }
320
+ throw new Error(`Unsupported action input type for ${flag}: ${declaration.type}`);
321
+ }
322
+ function taskPathContext(target, taskDir) {
323
+ const dir = toPosix(path.relative(target.projectRoot, taskDir));
324
+ return {
325
+ dir,
326
+ taskPlan: toPosix(path.join(dir, "task_plan.md")),
327
+ progress: toPosix(path.join(dir, "progress.md")),
328
+ artifacts: toPosix(path.join(dir, "artifacts")),
329
+ artifactsIndex: toPosix(path.join(dir, "artifacts/INDEX.md")),
330
+ visualMap: toPosix(path.join(dir, "visual_map.md")),
331
+ };
332
+ }
333
+ function resolveActionScopes(scopes, { target, taskPaths, label }) {
334
+ return (scopes || []).map((scope) => {
335
+ const rendered = renderHarnessTemplate(String(scope || ""), { paths: harnessPathContext(target), task: { paths: taskPaths } }, { strict: true });
336
+ const normalized = toPosix(path.normalize(rendered));
337
+ if (!rendered.trim() || path.isAbsolute(rendered) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
338
+ throw new Error(`${label} escapes target root: ${scope}`);
339
+ }
340
+ return normalized;
341
+ });
342
+ }
343
+ function concreteActionWriteScopes(scopes) {
344
+ return scopes.filter((scope) => !scope.endsWith("/**"));
345
+ }
346
+ function presetScriptEnv(contextPath) {
347
+ const env = { HARNESS_PRESET_CONTEXT: contextPath };
348
+ for (const key of ["PATH", "TMPDIR", "TEMP", "TMP", "SystemRoot", "ComSpec"]) {
349
+ if (process.env[key])
350
+ env[key] = process.env[key];
351
+ }
352
+ return env;
353
+ }
354
+ function normalizeTargetRelativePath(value, label) {
355
+ const raw = String(value || "").replace(/^TARGET:/, "").replace(/^\/+/, "").trim();
356
+ if (!raw)
357
+ return "";
358
+ const normalized = toPosix(path.normalize(raw));
359
+ if (path.isAbsolute(raw) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
360
+ throw new Error(`${label} escapes target root: ${raw}`);
361
+ }
362
+ return normalized;
363
+ }
364
+ function boundedScriptOutput(output) {
365
+ const redacted = String(output || "")
366
+ .replace(/(api[_-]?key|token|secret|password)=\S+/gi, "$1=[redacted]")
367
+ .replace(/\/Users\/[^/\s]+/g, "/Users/[redacted]");
368
+ return redacted.length > 4096 ? `${redacted.slice(0, 4096)}\n[output truncated]` : redacted;
125
369
  }
126
370
  function readMaterializationManifest(manifestPath) {
127
371
  if (!fs.existsSync(manifestPath))
128
372
  throw new Error("Preset entrypoint did not emit materialization manifest");
129
373
  const manifest = readJsonSafe(manifestPath, null);
130
- if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
374
+ if (!isRecord(manifest))
131
375
  throw new Error("Invalid preset materialization manifest");
132
376
  if (manifest.schemaVersion !== materializationSchemaVersion)
133
377
  throw new Error(`Invalid preset materialization schema: ${manifest.schemaVersion || "(missing)"}`);
@@ -135,9 +379,15 @@ function readMaterializationManifest(manifestPath) {
135
379
  throw new Error("Preset materialization manifest writes must be an array");
136
380
  if (manifest.writes.length > maxMaterializedWrites)
137
381
  throw new Error(`Preset materialization manifest has too many writes: ${manifest.writes.length}`);
138
- return manifest;
382
+ return {
383
+ schemaVersion: String(manifest.schemaVersion),
384
+ writes: manifest.writes.map((write) => asRecord(write)),
385
+ status: manifest.status === undefined ? undefined : String(manifest.status),
386
+ publicRedactionReport: isRecord(manifest.publicRedactionReport) ? { source: String(manifest.publicRedactionReport.source || "") } : undefined,
387
+ };
139
388
  }
140
- function validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, targetRoot }) {
389
+ function validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, target, entrypointName }) {
390
+ const targetRoot = target.projectRoot;
141
391
  const seenDestinations = new Set();
142
392
  const writes = manifest.writes.map((write, index) => {
143
393
  const source = normalizeManifestRelativePath(write.source, "Manifest source");
@@ -145,7 +395,10 @@ function validateMaterializationManifest(preset, entrypoint, manifest, { outputR
145
395
  if (seenDestinations.has(destination))
146
396
  throw new Error(`Duplicate materialization destination: ${destination}`);
147
397
  seenDestinations.add(destination);
148
- assertEntrypointWriteScope(preset, entrypoint, destination);
398
+ assertEntrypointWriteScope(preset, entrypoint, destination, target, entrypointName);
399
+ if (entrypointName && preset.actions?.[entrypointName] && isDefaultTaskGovernanceFile(destination)) {
400
+ throw new Error(`Preset action ${entrypointName} cannot write task governance file by default: ${destination}`);
401
+ }
149
402
  const sourcePath = path.join(outputRoot, source);
150
403
  assertOutputSource(outputRoot, sourcePath, source);
151
404
  const stat = fs.lstatSync(sourcePath);
@@ -200,9 +453,10 @@ function assertDestinationParent(targetRoot, destination) {
200
453
  throw new Error(`Manifest destination parent escapes target root: ${destination}`);
201
454
  }
202
455
  }
203
- function assertEntrypointWriteScope(preset, entrypoint, destination) {
204
- assertPresetWriteScope(preset, destination);
205
- if (!entrypoint.writes.some((scope) => matchesScope(scope, destination))) {
456
+ function assertEntrypointWriteScope(preset, entrypoint, destination, target, entrypointName) {
457
+ assertPresetWriteScope(preset, destination, target);
458
+ const resolved = resolvePresetScopes(preset, target).entrypoints[entrypointName] || entrypoint.writes;
459
+ if (!resolved.some((scope) => matchesScope(scope, destination))) {
206
460
  throw new Error(`Preset write scope violation for ${destination}`);
207
461
  }
208
462
  }
@@ -214,6 +468,9 @@ function matchesScope(scope, relativePath) {
214
468
  }
215
469
  return relativePath === normalizedScope;
216
470
  }
471
+ function isDefaultTaskGovernanceFile(destination) {
472
+ return /(^|\/)(review|brief|task_plan)\.md$/.test(destination);
473
+ }
217
474
  function enforcePublicRedaction(manifest, writes, { outputRoot }) {
218
475
  const publicWrites = writes.filter((write) => write.visibility === "public" || write.destination.startsWith("docs-release/"));
219
476
  if (publicWrites.length === 0)
@@ -221,8 +478,8 @@ function enforcePublicRedaction(manifest, writes, { outputRoot }) {
221
478
  const reportSource = normalizeManifestRelativePath(manifest.publicRedactionReport?.source || "", "Public redaction report source");
222
479
  const reportPath = path.join(outputRoot, reportSource);
223
480
  assertOutputSource(outputRoot, reportPath, reportSource);
224
- const report = readJsonSafe(reportPath, null);
225
- if (!report || report.status !== "pass")
481
+ const report = asRecord(readJsonSafe(reportPath, null));
482
+ if (report.status !== "pass")
226
483
  throw new Error("Public materialization requires a passing public redaction report");
227
484
  }
228
485
  function materializeWrites(targetRoot, writes) {
@@ -292,3 +549,12 @@ function isInside(root, candidate) {
292
549
  const relative = path.relative(root, candidate);
293
550
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
294
551
  }
552
+ function isRecord(value) {
553
+ return typeof value === "object" && value !== null && !Array.isArray(value);
554
+ }
555
+ function asRecord(value) {
556
+ return isRecord(value) ? value : {};
557
+ }
558
+ function errorMessage(error) {
559
+ return error instanceof Error ? error.message : String(error);
560
+ }
@@ -33,34 +33,41 @@ export function prepareReviewConfirmGitGate(projectRoot, allowedFilesAbs) {
33
33
  const gitRoot = root;
34
34
  const allowedPaths = allowedFilesAbs.map((filePath) => toPosix(path.relative(gitRoot, path.resolve(filePath))));
35
35
  assertAllowedPaths(allowedPaths);
36
- assertCleanWorkingTree(gitRoot);
36
+ const baselineEntries = statusEntries(gitRoot);
37
+ assertOwnedPathsClean(allowedPaths, baselineEntries);
37
38
  assertCommitIdentity(gitRoot);
38
- return { gitRoot, allowedPaths };
39
+ return {
40
+ gitRoot,
41
+ allowedPaths,
42
+ baselineOutsideEntries: outsideEntries(baselineEntries, allowedPaths),
43
+ };
39
44
  }
40
45
  export function commitReviewConfirmationGate(gate, { taskId, reviewPath, writeFinalAudit, message = "" }) {
41
46
  const subjectSuffix = taskId.replace(/[^A-Za-z0-9._/-]+/g, "-");
42
- assertOnlyAllowedChanged(gate.gitRoot, gate.allowedPaths);
47
+ assertOutsideStatusUnchanged(gate.gitRoot, gate.allowedPaths, gate.baselineOutsideEntries);
43
48
  git(gate.gitRoot, ["add", "--", ...gate.allowedPaths]);
44
- assertOnlyAllowedStaged(gate.gitRoot, gate.allowedPaths);
45
- const confirmCommit = commit(gate.gitRoot, `chore: confirm review ${subjectSuffix}`, {
49
+ assertOutsideStatusUnchanged(gate.gitRoot, gate.allowedPaths, gate.baselineOutsideEntries);
50
+ const confirmCommit = commitOnly(gate.gitRoot, `chore: confirm review ${subjectSuffix}`, gate.allowedPaths, {
46
51
  recovery: [
47
52
  "Review confirmation files were written but not committed.",
48
- `Inspect and either fix hooks then run: git add -- ${gate.allowedPaths.join(" ")} && git commit`,
53
+ `Inspect and either fix hooks then run: git add -- ${gate.allowedPaths.join(" ")} && git commit --only -- ${gate.allowedPaths.join(" ")}`,
49
54
  "Or manually revert the written review confirmation files if the confirmation should not proceed.",
50
55
  ],
51
56
  });
57
+ assertOwnedPathsClean(gate.allowedPaths, statusEntries(gate.gitRoot));
52
58
  writeFinalAudit(confirmCommit);
53
59
  const reviewRelativePath = toPosix(path.relative(gate.gitRoot, path.resolve(reviewPath)));
54
60
  git(gate.gitRoot, ["add", "--", reviewRelativePath]);
55
- assertOnlyAllowedStaged(gate.gitRoot, gate.allowedPaths);
56
- const auditCommit = commit(gate.gitRoot, `chore: record review confirmation audit ${subjectSuffix}`, {
61
+ assertOutsideStatusUnchanged(gate.gitRoot, gate.allowedPaths, gate.baselineOutsideEntries);
62
+ const auditCommit = commitOnly(gate.gitRoot, `chore: record review confirmation audit ${subjectSuffix}`, gate.allowedPaths, {
57
63
  recovery: [
58
64
  "The confirmation commit was created, but final audit metadata could not be committed.",
59
65
  `Confirmation commit SHA: ${confirmCommit}`,
60
- `Fix hooks, then stage ${reviewRelativePath} and commit the audit metadata.`,
66
+ `Fix hooks, then stage ${reviewRelativePath} and commit --only -- ${reviewRelativePath}.`,
61
67
  ],
62
68
  });
63
- assertCleanWorkingTree(gate.gitRoot);
69
+ assertOwnedPathsClean(gate.allowedPaths, statusEntries(gate.gitRoot));
70
+ assertOutsideStatusUnchanged(gate.gitRoot, gate.allowedPaths, gate.baselineOutsideEntries);
64
71
  return {
65
72
  commitSha: confirmCommit,
66
73
  auditCommitSha: auditCommit,
@@ -99,7 +106,8 @@ export function validateReviewConfirmationGitAudit({ projectRoot, taskId, review
99
106
  addIssue("git-audit-commit-not-reachable");
100
107
  const subject = git(root, ["show", "-s", "--format=%s", fullCommitSha], { allowFailure: true }).stdout.trim();
101
108
  const expectedSubject = `chore: confirm review ${String(taskId || "").replace(/[^A-Za-z0-9._/-]+/g, "-")}`;
102
- if (subject !== expectedSubject)
109
+ const batchSubject = subject === "chore: confirm selected reviews";
110
+ if (subject !== expectedSubject && !batchSubject)
103
111
  addIssue("git-audit-subject-mismatch");
104
112
  const changedPaths = git(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", fullCommitSha], { allowFailure: true }).stdout
105
113
  .split(/\r?\n/)
@@ -108,7 +116,13 @@ export function validateReviewConfirmationGitAudit({ projectRoot, taskId, review
108
116
  .sort();
109
117
  if (expectedPaths.length === 0)
110
118
  addIssue("git-audit-allowlist-missing");
111
- if (changedPaths.join("\n") !== expectedPaths.join("\n"))
119
+ if (batchSubject) {
120
+ const expectedIncluded = expectedPaths.every((expectedPath) => changedPaths.includes(expectedPath));
121
+ const batchPathsAllowed = changedPaths.every((changedPath) => /(^|\/)coding-agent-harness\/planning\/(?:tasks|modules)\/.+\/INDEX\.md$/.test(changedPath));
122
+ if (!expectedIncluded || !batchPathsAllowed)
123
+ addIssue("git-audit-allowlist-mismatch");
124
+ }
125
+ else if (changedPaths.join("\n") !== expectedPaths.join("\n"))
112
126
  addIssue("git-audit-allowlist-mismatch");
113
127
  return {
114
128
  valid: issues.length === 0,
@@ -134,13 +148,7 @@ function assertAllowedPaths(paths) {
134
148
  const disallowed = paths.filter((relativePath) => {
135
149
  if (!relativePath || relativePath.startsWith("../") || path.isAbsolute(relativePath))
136
150
  return true;
137
- if (relativePath === "AGENTS.md" || relativePath === "CLAUDE.md")
138
- return true;
139
- if (relativePath === "docs" || relativePath.startsWith("docs/"))
140
- return false;
141
- if (relativePath === ".harness-private" || relativePath.startsWith(".harness-private/"))
142
- return true;
143
- return false;
151
+ return !/(^|\/)coding-agent-harness\/planning\/(?:tasks|modules)\/.+\/INDEX\.md$/.test(relativePath);
144
152
  });
145
153
  if (disallowed.length > 0) {
146
154
  throw new ReviewConfirmGitGateError("Review confirmation write allowlist contains forbidden paths.", {
@@ -163,6 +171,41 @@ function assertCleanWorkingTree(gitRoot) {
163
171
  });
164
172
  }
165
173
  }
174
+ function assertOwnedPathsClean(allowedPaths, entries) {
175
+ const ownedDirty = entries.filter((entry) => allowedPaths.includes(entry.path));
176
+ if (ownedDirty.length > 0) {
177
+ throw new ReviewConfirmGitGateError("Review confirmation owned path is already dirty; refusing to overwrite existing task confirmation state.", {
178
+ code: "git-owned-path-dirty",
179
+ details: { entries: ownedDirty, allowedPaths },
180
+ recovery: [
181
+ "Commit or resolve the existing task INDEX.md edits before retrying review-confirm.",
182
+ "Unrelated dirty files may remain; only the review-confirm owned paths must be clean.",
183
+ ],
184
+ });
185
+ }
186
+ }
187
+ function outsideEntries(entries, allowedPaths) {
188
+ return entries.filter((entry) => !allowedPaths.includes(entry.path));
189
+ }
190
+ function assertOutsideStatusUnchanged(gitRoot, allowedPaths, baselineOutsideEntries) {
191
+ const currentOutside = outsideEntries(statusEntries(gitRoot), allowedPaths);
192
+ if (statusSignature(currentOutside) !== statusSignature(baselineOutsideEntries)) {
193
+ throw new ReviewConfirmGitGateError("Review confirmation changed files outside the write allowlist.", {
194
+ code: "git-outside-status-changed",
195
+ details: { before: baselineOutsideEntries, after: currentOutside, allowedPaths },
196
+ recovery: [
197
+ "Inspect the extra files and do not commit them through review-confirm.",
198
+ "Revert only unintended review-confirm side effects, then retry.",
199
+ ],
200
+ });
201
+ }
202
+ }
203
+ function statusSignature(entries) {
204
+ return entries
205
+ .map((entry) => `${entry.index}${entry.worktree} ${entry.path}`)
206
+ .sort()
207
+ .join("\n");
208
+ }
166
209
  function assertCommitIdentity(gitRoot) {
167
210
  const name = git(gitRoot, ["config", "--get", "user.name"], { allowFailure: true }).stdout.trim();
168
211
  const email = git(gitRoot, ["config", "--get", "user.email"], { allowFailure: true }).stdout.trim();
@@ -214,6 +257,17 @@ function commit(gitRoot, message, { recovery }) {
214
257
  }
215
258
  return git(gitRoot, ["rev-parse", "HEAD"]).stdout.trim();
216
259
  }
260
+ function commitOnly(gitRoot, message, paths, { recovery }) {
261
+ const result = git(gitRoot, ["commit", "--only", "-m", message, "--", ...paths], { allowFailure: true });
262
+ if (result.status !== 0) {
263
+ throw new ReviewConfirmGitGateError("Git commit failed during review confirmation auto-commit.", {
264
+ code: "git-commit-failed",
265
+ details: { stdout: result.stdout.trim(), stderr: result.stderr.trim() },
266
+ recovery,
267
+ });
268
+ }
269
+ return git(gitRoot, ["rev-parse", "HEAD"]).stdout.trim();
270
+ }
217
271
  function statusEntries(gitRoot) {
218
272
  const output = git(gitRoot, ["status", "--porcelain=v1", "--untracked-files=all"]).stdout;
219
273
  return output.split(/\r?\n/).filter(Boolean).map((line) => ({
@@ -1,24 +1,25 @@
1
- // @ts-nocheck
2
1
  import path from "node:path";
3
2
  import { normalizeTarget, toPosix } from "./core-shared.mjs";
4
3
  import { capabilityDefinitions, readCapabilityRegistry } from "./capability-registry.mjs";
5
4
  import { summarizeGitState } from "./git-status-summary.mjs";
6
- import { collectTasks, taskCutoverCounters } from "./task-scanner.mjs";
5
+ import { createScannerTaskRepository, taskCutoverCounters } from "./task-repository.mjs";
6
+ import { readHarnessModules } from "./module-registry.mjs";
7
+ const taskCutoverCountersForStatus = taskCutoverCounters;
7
8
  export function buildStatusData(targetInput, options = {}) {
8
- const target = targetInput?.projectRoot ? targetInput : normalizeTarget(targetInput);
9
+ const target = hasProjectRoot(targetInput) ? targetInput : normalizeTarget(targetInput);
9
10
  const validationMode = options.validationMode || "data-only";
10
11
  const gitState = options.gitState || summarizeGitState(target);
11
- const registry = options.capabilityState?.registry || readCapabilityRegistry(target);
12
+ const registry = (options.capabilityState?.registry || readCapabilityRegistry(target));
12
13
  const detected = options.capabilityState?.detected || [];
13
14
  const capabilityWarnings = options.capabilityState?.warnings || [];
14
15
  const failures = [...(options.failures || [])];
15
16
  const warnings = [...(options.warnings || [])];
16
17
  const legacy = options.legacy || { status: "skipped", code: 0, stdout: "", stderr: "" };
17
- const tasks = options.tasks || collectTasks(target, {
18
+ const tasks = options.tasks || createScannerTaskRepository(target, {
18
19
  requireGeneratedScaffoldProvenance: options.requireGeneratedScaffoldProvenance === true,
19
- taskPlanPaths: options.taskPlanPaths,
20
20
  closeoutContent: options.closeoutContent,
21
- });
21
+ }).list();
22
+ const modules = harnessModulesForStatus(target);
22
23
  const briefReady = tasks.filter((task) => task.briefSource === "standalone").length;
23
24
  const briefMissing = tasks.length - briefReady;
24
25
  const capabilityNames = new Map(registry.capabilities.map((capability) => [capability.name, capability]));
@@ -26,7 +27,7 @@ export function buildStatusData(targetInput, options = {}) {
26
27
  if (!capabilityNames.has(capability))
27
28
  capabilityNames.set(capability, { name: capability, state: "configured" });
28
29
  }
29
- const cutoverCounters = taskCutoverCounters(tasks);
30
+ const cutoverCounters = taskCutoverCountersForStatus(tasks);
30
31
  const fullCutoverEligible = validationMode === "validated" &&
31
32
  failures.length === 0 &&
32
33
  warnings.length === 0 &&
@@ -54,6 +55,7 @@ export function buildStatusData(targetInput, options = {}) {
54
55
  git: gitState.summary,
55
56
  summary: {
56
57
  tasks: tasks.length,
58
+ modules: modules.length,
57
59
  briefCoverage: {
58
60
  ready: briefReady,
59
61
  missing: briefMissing,
@@ -81,7 +83,20 @@ export function buildStatusData(targetInput, options = {}) {
81
83
  warnings: capabilityWarnings.filter((warning) => warning.includes(capability.name)),
82
84
  })),
83
85
  tasks,
86
+ modules,
84
87
  handoffs: tasks.flatMap((task) => task.handoffs || []),
85
88
  recentActivity: tasks.slice(0, 8).map((task) => ({ at: new Date().toISOString(), type: "task", summary: task.title })),
86
89
  };
87
90
  }
91
+ function harnessModulesForStatus(target) {
92
+ try {
93
+ const registry = readHarnessModules(target);
94
+ return Object.entries(registry.items || {}).map(([key, module]) => ({ key, ...module }));
95
+ }
96
+ catch {
97
+ return [];
98
+ }
99
+ }
100
+ function hasProjectRoot(value) {
101
+ return Boolean(value && typeof value === "object" && "projectRoot" in value);
102
+ }