coding-agent-harness 1.0.7 → 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 (238) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/CONTRIBUTING.md +9 -5
  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 +32 -6
  7. package/dist/check-dist-observation.mjs +73 -28
  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 +88 -0
  12. package/dist/check-runtime-emit.mjs +10 -3
  13. package/dist/check-type-boundaries.mjs +67 -8
  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 +65 -4
  18. package/dist/commands/registry.mjs +483 -0
  19. package/dist/commands/task-command.mjs +111 -53
  20. package/dist/harness.mjs +6 -303
  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 +5 -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 +68 -29
  44. package/dist/lib/preset-registry.mjs +374 -72
  45. package/dist/lib/preset-runner.mjs +560 -0
  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 +4 -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 +117 -159
  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 +38 -17
  70. package/dist/lib/task-scanner.mjs +75 -23
  71. package/dist/lib/task-template-materials.mjs +131 -0
  72. package/dist/lib/task-tombstone-commands.mjs +187 -18
  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 +2 -1
  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 +19 -11
  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 +29 -0
  120. package/presets/release-closeout/preset.yaml +100 -0
  121. package/presets/release-closeout/scripts/generate-release-package.mjs +572 -0
  122. package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
  123. package/presets/release-closeout/templates/findings.seed.md +5 -0
  124. package/presets/release-closeout/templates/review.seed.md +3 -0
  125. package/presets/release-closeout/templates/task_plan.append.md +24 -0
  126. package/presets/standard-task/preset.yaml +2 -2
  127. package/references/agents-md-pattern.md +23 -17
  128. package/references/lessons-governance.md +2 -2
  129. package/references/module-parallel-standard.md +3 -6
  130. package/references/pull-request-standard.md +2 -2
  131. package/references/ssot-governance.md +2 -2
  132. package/references/taskr-gap-analysis.md +3 -3
  133. package/run-dist.mjs +34 -0
  134. package/skills/preset-creator/SKILL.md +40 -8
  135. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
  136. package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
  137. package/skills/preset-creator/references/structure-aware-paths.md +112 -0
  138. package/templates/AGENTS.md.template +28 -26
  139. package/templates/architecture/README.md +2 -2
  140. package/templates/architecture/service-catalog.md +2 -2
  141. package/templates/architecture/services/service-template.md +1 -1
  142. package/templates/dashboard/assets/app-src/00-state.js +5 -1
  143. package/templates/dashboard/assets/app-src/10-router.js +7 -0
  144. package/templates/dashboard/assets/app-src/20-overview.js +8 -8
  145. package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
  146. package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
  147. package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
  148. package/templates/dashboard/assets/app-src/40-modules.js +257 -41
  149. package/templates/dashboard/assets/app-src/45-review.js +127 -1
  150. package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
  151. package/templates/dashboard/assets/app.css +928 -53
  152. package/templates/dashboard/assets/app.css.manifest.json +2 -0
  153. package/templates/dashboard/assets/app.js +1071 -98
  154. package/templates/dashboard/assets/app.manifest.json +1 -0
  155. package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
  156. package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
  157. package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
  158. package/templates/dashboard/assets/css-src/31-archive.css +94 -0
  159. package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
  160. package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
  161. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
  162. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
  163. package/templates/dashboard/assets/i18n.js +166 -2
  164. package/templates/development/README.md +9 -9
  165. package/templates/development/cross-repo-debugging.md +3 -3
  166. package/templates/development/external-context/service-template.md +1 -1
  167. package/templates/development/external-source-packs/README.md +2 -2
  168. package/templates/integrations/README.md +4 -4
  169. package/templates/integrations/api-contract.md +1 -1
  170. package/templates/integrations/event-contract.md +1 -1
  171. package/templates/integrations/third-party/vendor-template.md +1 -1
  172. package/templates/integrations/webhook-contract.md +1 -1
  173. package/templates/ledger/Harness-Ledger.md +1 -1
  174. package/templates/modules/module_brief.md +50 -0
  175. package/templates/modules/module_plan.md +49 -0
  176. package/templates/modules/registry_view.md +9 -0
  177. package/templates/modules/session_prompt_pack.md +55 -0
  178. package/templates/planning/brief.md +32 -8
  179. package/templates/planning/module_brief.md +28 -3
  180. package/templates/planning/module_plan.md +26 -11
  181. package/templates/planning/module_session_prompt.md +11 -2
  182. package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
  183. package/templates/planning/review.md +1 -1
  184. package/templates/planning/visual_map.md +1 -1
  185. package/templates/reference/docs-library-standard.md +7 -7
  186. package/templates/reference/execution-workflow-standard.md +13 -0
  187. package/templates/reference/external-source-intake-standard.md +10 -10
  188. package/templates/reference/pull-request-standard.md +2 -2
  189. package/templates/reference/repo-governance-standard.md +1 -1
  190. package/templates/reference/review-routing-standard.md +4 -0
  191. package/templates/ssot/Module-Registry.md +4 -38
  192. package/templates/walkthrough/walkthrough-template.md +1 -1
  193. package/templates-zh-CN/AGENTS.md.template +27 -25
  194. package/templates-zh-CN/CLAUDE.md.template +1 -1
  195. package/templates-zh-CN/architecture/README.md +2 -2
  196. package/templates-zh-CN/architecture/service-catalog.md +2 -2
  197. package/templates-zh-CN/architecture/services/service-template.md +1 -1
  198. package/templates-zh-CN/development/README.md +9 -9
  199. package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
  200. package/templates-zh-CN/development/external-context/service-template.md +1 -1
  201. package/templates-zh-CN/development/external-source-packs/README.md +2 -2
  202. package/templates-zh-CN/integrations/README.md +4 -4
  203. package/templates-zh-CN/integrations/api-contract.md +1 -1
  204. package/templates-zh-CN/integrations/event-contract.md +1 -1
  205. package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
  206. package/templates-zh-CN/integrations/webhook-contract.md +1 -1
  207. package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
  208. package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
  209. package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
  210. package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
  211. package/templates-zh-CN/modules/module_brief.md +47 -0
  212. package/templates-zh-CN/modules/module_plan.md +48 -0
  213. package/templates-zh-CN/modules/registry_view.md +9 -0
  214. package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
  215. package/templates-zh-CN/planning/INDEX.md +1 -0
  216. package/templates-zh-CN/planning/brief.md +26 -7
  217. package/templates-zh-CN/planning/module_brief.md +24 -2
  218. package/templates-zh-CN/planning/module_plan.md +35 -29
  219. package/templates-zh-CN/planning/module_session_prompt.md +15 -11
  220. package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
  221. package/templates-zh-CN/planning/review.md +1 -1
  222. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  223. package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
  224. package/templates-zh-CN/reference/docs-library-standard.md +27 -27
  225. package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
  226. package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
  227. package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
  228. package/templates-zh-CN/reference/pull-request-standard.md +1 -1
  229. package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
  230. package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
  231. package/templates-zh-CN/reference/review-routing-standard.md +3 -0
  232. package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
  233. package/templates-zh-CN/reference/worktree-standard.md +1 -1
  234. package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
  235. package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
  236. package/templates-zh-CN/ssot/Module-Registry.md +5 -44
  237. package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
  238. package/templates-zh-CN/walkthrough/walkthrough-template.md +4 -4
@@ -1,18 +1,19 @@
1
- // @ts-nocheck
2
1
  // Preset manifest parsing stays behavior-first until preset package domain types are modeled.
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
4
  import os from "node:os";
6
5
  import crypto from "node:crypto";
7
6
  import zlib from "node:zlib";
8
- import { builtinPresetRoot, projectPresetRoot, repoRoot, toPosix, userPresetRoot, userPresetRootForHome } from "./core-shared.mjs";
7
+ import { builtinPresetRoot, projectPresetRoot, readJsonSafe, repoRoot, renderHarnessTemplate, toPosix, validateHarnessPathTemplateTokens, userPresetRoot, userPresetRootForHome } from "./core-shared.mjs";
9
8
  const allowedEntrypoints = new Set(["newTask", "plan", "scaffold", "check"]);
10
9
  const allowedEntrypointTypes = new Set(["template", "script", "check"]);
10
+ const allowedActionTypes = new Set(["script"]);
11
11
  const allowedEvidenceTypes = new Set(["text", "json", "input-json", "preset-audit", "preset-manifest", "write-scope", "migration-verify", "migration-ledger", "dashboard-hash", "target-git-status", "target-commit", "harness-version", "generated-at"]);
12
12
  const allowedNewTaskTemplateKeys = new Set(["taskPlanAppend", "executionStrategyAppend", "visualMapAppend", "findingsSeed", "reviewSeed", "prompt"]);
13
13
  const maxPresetArchiveBytes = 25 * 1024 * 1024;
14
14
  const maxPresetArchiveUncompressedBytes = 50 * 1024 * 1024;
15
15
  const maxPresetArchiveEntries = 500;
16
+ const presetScriptTrustFile = ".harness-preset-trust.json";
16
17
  export function listPresetPackages({ targetInput = "", home = "" } = {}) {
17
18
  return listPresetPackageLayers({ targetInput, home }).filter((preset) => preset.effective);
18
19
  }
@@ -47,25 +48,29 @@ export function readPresetPackage(id, { targetInput = "", home = "" } = {}) {
47
48
  assertPresetManifestFile(path.dirname(manifestPath), manifestPath);
48
49
  const raw = fs.readFileSync(manifestPath, "utf8");
49
50
  const manifest = parseSimpleYaml(raw);
50
- const preset = normalizePresetManifest(manifest, { id: normalizedId, manifestPath, raw, source: found.source });
51
+ const source = found?.source || "local";
52
+ const preset = normalizePresetManifest(manifest, { id: normalizedId, manifestPath, raw, source });
51
53
  const report = validatePresetPackage(preset);
52
54
  if (report.failures.length)
53
55
  throw new Error(`Invalid preset package ${normalizedId}: ${report.failures.join("; ")}`);
54
56
  return preset;
55
57
  }
56
58
  export function inspectPresetPackage(id, { targetInput = "", home = "" } = {}) {
57
- const preset = fs.existsSync(path.join(path.resolve(id || ""), "preset.yaml")) ? readPresetPackageFromPath(path.resolve(id)) : readPresetPackage(id, { targetInput, home });
59
+ const localPath = path.resolve(id || "");
60
+ const preset = fs.existsSync(path.join(localPath, "preset.yaml")) ? readPresetPackageFromPath(localPath) : readPresetPackage(id, { targetInput, home });
58
61
  return publicPresetShape(preset);
59
62
  }
60
63
  export function checkPresetPackage(id, { targetInput = "", home = "" } = {}) {
61
- const preset = fs.existsSync(path.join(path.resolve(id || ""), "preset.yaml")) ? readPresetPackageFromPath(path.resolve(id)) : readPresetPackage(id, { targetInput, home });
64
+ const localPath = path.resolve(id || "");
65
+ const preset = fs.existsSync(path.join(localPath, "preset.yaml")) ? readPresetPackageFromPath(localPath) : readPresetPackage(id, { targetInput, home });
66
+ const scriptPolicy = buildPresetScriptPolicy(preset);
62
67
  const report = validatePresetPackage(preset);
63
68
  return {
64
69
  id: preset.id,
65
70
  version: preset.version,
66
71
  status: report.failures.length === 0 ? "pass" : "fail",
67
72
  failures: report.failures,
68
- warnings: report.warnings,
73
+ warnings: [...report.warnings, ...scriptPolicy.warnings],
69
74
  manifestPath: preset.manifestRelativePath,
70
75
  source: preset.source,
71
76
  inputs: preset.inputs,
@@ -74,6 +79,8 @@ export function checkPresetPackage(id, { targetInput = "", home = "" } = {}) {
74
79
  resources: preset.resources,
75
80
  context: preset.context,
76
81
  entrypoints: preset.entrypoints,
82
+ actions: preset.actions,
83
+ scriptPolicy,
77
84
  writeScopes: preset.writeScopes,
78
85
  };
79
86
  }
@@ -83,7 +90,8 @@ function readPresetPackageFromPath(directory, source = "local") {
83
90
  assertPresetManifestFile(directory, manifestPath);
84
91
  const raw = fs.readFileSync(manifestPath, "utf8");
85
92
  const manifest = parseSimpleYaml(raw);
86
- return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source });
93
+ const manifestRecord = asRecord(manifest);
94
+ return normalizePresetManifest(manifest, { id: normalizePresetId(String(manifestRecord.id || path.basename(directory))), manifestPath, raw, source });
87
95
  }
88
96
  function assertPresetDirectory(directory) {
89
97
  if (!fs.existsSync(directory))
@@ -107,7 +115,7 @@ function assertPresetManifestFile(directory, manifestPath) {
107
115
  if (!isInside(realRoot, realPath))
108
116
  throw new Error(`Preset manifest real path escapes preset package: ${displayManifestPath(manifestPath)}`);
109
117
  }
110
- export function installPresetPackage(source, { force = false, scope = "user", targetInput = ".", home = "" } = {}) {
118
+ export function installPresetPackage(source, { force = false, scope = "user", targetInput = ".", home = "", allowScripts = false } = {}) {
111
119
  if (!source)
112
120
  throw new Error("Missing preset source");
113
121
  const resolvedSource = resolveInstallSource(source);
@@ -117,6 +125,10 @@ export function installPresetPackage(source, { force = false, scope = "user", ta
117
125
  const stagedReport = validatePresetPackage(stagedPreset);
118
126
  if (stagedReport.failures.length)
119
127
  throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
128
+ const scriptPolicy = buildPresetScriptPolicy(stagedPreset);
129
+ if (resolvedSource.source !== "builtin" && scriptPolicy.requiresTrustedSource && !allowScripts) {
130
+ throw new Error(`Preset ${stagedPreset.id} declares script entrypoints or actions and requires explicit trust. Re-run with --allow-scripts if you trust this preset source.`);
131
+ }
120
132
  const id = stagedPreset.id;
121
133
  if (!id)
122
134
  throw new Error("Preset manifest missing id");
@@ -136,6 +148,9 @@ export function installPresetPackage(source, { force = false, scope = "user", ta
136
148
  throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
137
149
  fs.rmSync(destination, { recursive: true, force: true });
138
150
  fs.renameSync(tempDestination, destination);
151
+ if (scriptPolicy.requiresTrustedSource && allowScripts) {
152
+ writePresetScriptTrustMarker(destination, stagedPreset, scriptPolicy.scriptCommands);
153
+ }
139
154
  const preset = readPresetPackage(id, scope === "project" ? { targetInput, home } : { home });
140
155
  return {
141
156
  installed: true,
@@ -144,6 +159,10 @@ export function installPresetPackage(source, { force = false, scope = "user", ta
144
159
  source: preset.source,
145
160
  destination: toPosix(destination),
146
161
  manifestPath: preset.manifestRelativePath,
162
+ scriptPolicy: {
163
+ ...buildPresetScriptPolicy(preset),
164
+ trusted: presetScriptTrustValid(preset),
165
+ },
147
166
  };
148
167
  }
149
168
  catch (error) {
@@ -171,7 +190,7 @@ export function listBundledPresetIds() {
171
190
  return fs.readdirSync(builtinPresetRoot, { withFileTypes: true })
172
191
  .filter((entry) => entry.isDirectory())
173
192
  .map((entry) => tryNormalizePresetId(entry.name))
174
- .filter(Boolean)
193
+ .filter((id) => Boolean(id))
175
194
  .filter((id) => fs.existsSync(path.join(builtinPresetRoot, id, "preset.yaml")))
176
195
  .sort();
177
196
  }
@@ -204,6 +223,44 @@ export function seedBundledPresets({ force = false, scope = "user", targetInput
204
223
  skipped: presets.filter((preset) => preset.action === "skip-existing").length,
205
224
  };
206
225
  }
226
+ export function auditBundledPresetDrift({ scope = "user", targetInput = ".", home = "" } = {}) {
227
+ const targetRoot = scope === "project" ? projectPresetRoot(targetInput) : userPresetRootForHome(home);
228
+ const presets = listBundledPresetIds().map((id) => {
229
+ const builtin = readPresetPackageFromPath(path.join(builtinPresetRoot, id), "builtin");
230
+ const installedPath = path.join(targetRoot, id, "preset.yaml");
231
+ const installed = fs.existsSync(installedPath) ? readPresetPackageFromPath(path.dirname(installedPath), scope) : null;
232
+ const sameHash = Boolean(installed && installed.manifestSha256 === builtin.manifestSha256);
233
+ const sameVersion = Boolean(installed && installed.version === builtin.version);
234
+ const sameVersionDifferentHash = Boolean(installed && sameVersion && !sameHash);
235
+ const action = !installed
236
+ ? "install-available"
237
+ : sameHash
238
+ ? "up-to-date"
239
+ : installed.source === "project" || installed.source === "user"
240
+ ? "manual-review"
241
+ : "upgrade-available";
242
+ return {
243
+ id,
244
+ scope,
245
+ source: installed ? installed.source : "missing",
246
+ effective: installed ? true : false,
247
+ builtinVersion: builtin.version,
248
+ installedVersion: installed?.version || null,
249
+ builtinSha256: builtin.manifestSha256,
250
+ installedSha256: installed?.manifestSha256 || null,
251
+ sameVersionDifferentHash,
252
+ upgradeAction: action,
253
+ installedPath: installed ? installed.manifestRelativePath : toPosix(installedPath),
254
+ };
255
+ });
256
+ return {
257
+ operation: "preset-audit",
258
+ scope,
259
+ target: toPosix(targetRoot),
260
+ presets,
261
+ stale: presets.filter((preset) => preset.upgradeAction !== "up-to-date").length,
262
+ };
263
+ }
207
264
  export function validatePresetPackage(preset) {
208
265
  const failures = [];
209
266
  const warnings = [];
@@ -228,7 +285,8 @@ export function validatePresetPackage(preset) {
228
285
  if (preset.evidence?.files && (Array.isArray(preset.evidence.files) || typeof preset.evidence.files !== "object")) {
229
286
  failures.push("evidence.files must be a mapping");
230
287
  }
231
- for (const [name, evidence] of Object.entries(preset.evidence?.files || {})) {
288
+ const evidenceFiles = typeof preset.evidence?.files === "object" && preset.evidence.files !== null ? preset.evidence.files : {};
289
+ for (const [name, evidence] of Object.entries(evidenceFiles)) {
232
290
  if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
233
291
  failures.push(`evidence file ${name} must be a mapping`);
234
292
  continue;
@@ -255,9 +313,13 @@ export function validatePresetPackage(preset) {
255
313
  if (!entrypoint.writes.length)
256
314
  failures.push(`${name} missing write scope manifest`);
257
315
  for (const writeScope of entrypoint.writes) {
316
+ failures.push(...validateHarnessPathTemplateTokens(writeScope, `${name} write scope`));
258
317
  if (!preset.writeScopes.some((scope) => scope.path === writeScope)) {
259
318
  failures.push(`${name} writes undeclared scope: ${writeScope}`);
260
319
  }
320
+ if (name === "newTask" && !newTaskWriteScopeAllowed(writeScope)) {
321
+ failures.push("newTask entrypoint writes must stay under coding-agent-harness/planning/**");
322
+ }
261
323
  }
262
324
  if (["script", "check"].includes(entrypoint.type)) {
263
325
  const entryPath = path.join(preset.directory, entrypoint.command || "");
@@ -265,10 +327,56 @@ export function validatePresetPackage(preset) {
265
327
  failures.push(`${name} missing command`);
266
328
  else if (!isInside(preset.directory, entryPath))
267
329
  failures.push(`${name} command escapes preset package`);
268
- else
330
+ else {
269
331
  validatePresetPackageFile(preset, entrypoint.command, `${name} command`, failures);
332
+ warnOnRuntimePathLiterals(entryPath, `${name} command`, warnings);
333
+ }
334
+ }
335
+ for (const readScope of entrypoint.reads || [])
336
+ failures.push(...validateHarnessPathTemplateTokens(readScope, `${name} read scope`));
337
+ }
338
+ for (const [name, action] of Object.entries(preset.actions || {})) {
339
+ if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(name))
340
+ failures.push(`unsupported action id: ${name}`);
341
+ if (!allowedActionTypes.has(action.type))
342
+ failures.push(`${name} action has unsupported type: ${action.type || "(missing)"}`);
343
+ if (action.taskRequired !== true)
344
+ failures.push(`${name} action must set taskRequired: true`);
345
+ if (!action.writes.length)
346
+ failures.push(`${name} action missing write scope manifest`);
347
+ for (const [inputName, input] of Object.entries(action.inputs || {})) {
348
+ if (!["text", "flag", "json-file"].includes(input.type))
349
+ failures.push(`${name}.${inputName} has unsupported input type: ${input.type || "(missing)"}`);
350
+ if (!input.flag)
351
+ failures.push(`${name}.${inputName} input missing CLI flag`);
352
+ else if (!input.flag.startsWith("--"))
353
+ failures.push(`${name}.${inputName} input flag must start with --`);
354
+ }
355
+ for (const writeScope of action.writes) {
356
+ failures.push(...validateActionPathTemplateTokens(writeScope, `${name} action write scope`));
357
+ if (isBroadTaskRootScope(writeScope))
358
+ failures.push(`${name} action writes must be task-local; avoid broad task-root scope: ${writeScope}`);
359
+ if (!usesTaskLocalPath(writeScope))
360
+ warnings.push(`${name} action write scope is not task-local: ${writeScope}`);
361
+ }
362
+ for (const readScope of action.reads || [])
363
+ failures.push(...validateActionPathTemplateTokens(readScope, `${name} action read scope`));
364
+ if (action.type === "script") {
365
+ const actionPath = path.join(preset.directory, action.command || "");
366
+ if (!action.command)
367
+ failures.push(`${name} action missing command`);
368
+ else if (!action.command.endsWith(".mjs"))
369
+ failures.push(`${name} action command must be a .mjs file: ${action.command}`);
370
+ else if (!isInside(preset.directory, actionPath))
371
+ failures.push(`${name} action command escapes preset package`);
372
+ else {
373
+ validatePresetPackageFile(preset, action.command, `${name} action command`, failures);
374
+ warnOnRuntimePathLiterals(actionPath, `${name} action command`, warnings);
375
+ }
270
376
  }
271
377
  }
378
+ for (const scope of preset.writeScopes)
379
+ failures.push(...validateHarnessPathTemplateTokens(scope.path, `${scope.name || "write scope"} path`));
272
380
  for (const [templateKey, templatePath] of Object.entries(preset.newTaskTemplates)) {
273
381
  if (!allowedNewTaskTemplateKeys.has(templateKey)) {
274
382
  failures.push(`unsupported newTask template: ${templateKey}`);
@@ -282,7 +390,7 @@ export function validatePresetPackage(preset) {
282
390
  }
283
391
  return { failures, warnings };
284
392
  }
285
- export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [] } = {}) {
393
+ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [], resolvedInputs = {} } = {}) {
286
394
  const entrypoints = {
287
395
  [entrypoint]: preset.entrypoints[entrypoint],
288
396
  };
@@ -292,13 +400,64 @@ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypo
292
400
  version: preset.version,
293
401
  manifestPath: preset.manifestRelativePath,
294
402
  manifestSha256: preset.manifestSha256,
403
+ scriptSha256s: preset.scriptSha256s,
295
404
  entrypoints,
296
405
  writeScopes: scopes,
406
+ resolvedInputs,
297
407
  taskId,
298
408
  targetRoot,
299
409
  generatedAt: new Date().toISOString(),
300
410
  };
301
411
  }
412
+ export function buildPresetScriptPolicy(preset) {
413
+ const scriptCommands = [
414
+ ...Object.entries(preset.entrypoints || {})
415
+ .filter(([, entrypoint]) => entrypoint.type === "script")
416
+ .map(([name, entrypoint]) => `entrypoint:${name}:${entrypoint.command}`),
417
+ ...Object.entries(preset.actions || {})
418
+ .filter(([, action]) => action.type === "script")
419
+ .map(([name, action]) => `action:${name}:${action.command}`),
420
+ ];
421
+ const unsupportedCommands = Object.entries(preset.actions || {})
422
+ .filter(([, action]) => action.type === "script" && !String(action.command || "").endsWith(".mjs"))
423
+ .map(([name, action]) => `action:${name}:${action.command || "(missing)"}`);
424
+ const requiresTrustedSource = scriptCommands.length > 0 && preset.source !== "builtin";
425
+ return {
426
+ hasScripts: scriptCommands.length > 0,
427
+ scriptCommands,
428
+ scriptSha256s: preset.scriptSha256s,
429
+ riskLevel: scriptCommands.length > 0 ? "trusted-code" : "none",
430
+ requiresTrustedSource,
431
+ unsupportedCommands,
432
+ warnings: requiresTrustedSource
433
+ ? ["Script entrypoints and actions execute trusted local Node.js code; use --allow-scripts only for sources you trust."]
434
+ : [],
435
+ };
436
+ }
437
+ export function presetScriptTrustValid(preset) {
438
+ if (preset.source === "builtin")
439
+ return true;
440
+ const trustPath = path.join(preset.directory, presetScriptTrustFile);
441
+ const trust = asRecord(readJsonSafe(trustPath, {}));
442
+ const trustedScriptSha256s = asRecord(trust.scriptSha256s);
443
+ return (trust.schemaVersion === "preset-script-trust/v1" &&
444
+ trust.preset === preset.id &&
445
+ trust.manifestSha256 === preset.manifestSha256 &&
446
+ recordsEqual(trustedScriptSha256s, preset.scriptSha256s) &&
447
+ trust.trusted === true);
448
+ }
449
+ function writePresetScriptTrustMarker(destination, preset, scriptCommands) {
450
+ fs.writeFileSync(path.join(destination, presetScriptTrustFile), `${JSON.stringify({
451
+ schemaVersion: "preset-script-trust/v1",
452
+ preset: preset.id,
453
+ version: preset.version,
454
+ manifestSha256: preset.manifestSha256,
455
+ scriptCommands,
456
+ scriptSha256s: preset.scriptSha256s,
457
+ trusted: true,
458
+ trustedAt: new Date().toISOString(),
459
+ }, null, 2)}\n`);
460
+ }
302
461
  export function renderPresetTemplate(preset, templatePath, values) {
303
462
  if (!templatePath)
304
463
  return "";
@@ -306,10 +465,7 @@ export function renderPresetTemplate(preset, templatePath, values) {
306
465
  if (!isInside(preset.directory, absolute))
307
466
  throw new Error(`Preset template escapes package: ${templatePath}`);
308
467
  const content = fs.readFileSync(absolute, "utf8");
309
- return content.replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, key) => {
310
- const value = getValue(values, key);
311
- return value == null ? "" : String(value);
312
- });
468
+ return renderHarnessTemplate(content, values, { missing: "empty" });
313
469
  }
314
470
  function normalizePresetId(id) {
315
471
  const normalized = String(id || "").trim().toLowerCase().replaceAll("_", "-");
@@ -346,74 +502,140 @@ function projectPresetDestination(id, targetInput) {
346
502
  }
347
503
  function normalizePresetManifest(manifest, { id, manifestPath, raw, source }) {
348
504
  const directory = path.dirname(manifestPath);
349
- const entrypoints = normalizeEntryPoints(manifest.entrypoints || {});
350
- const writeScopes = Object.entries(manifest.writeScopes || {}).map(([name, value]) => ({
351
- name,
352
- path: String(value.path || value || "").trim(),
353
- access: String(value.access || "write").trim(),
354
- })).filter((scope) => scope.path);
505
+ const manifestRecord = asRecord(manifest);
506
+ const entrypoints = normalizeEntryPoints(asRecord(manifestRecord.entrypoints));
507
+ const actions = normalizeActions(asRecord(manifestRecord.actions));
508
+ const writeScopes = Object.entries(asRecord(manifestRecord.writeScopes)).map(([name, value]) => {
509
+ const scope = asRecord(value);
510
+ return {
511
+ name,
512
+ path: String(scope.path || value || "").trim(),
513
+ access: String(scope.access || "write").trim(),
514
+ };
515
+ }).filter((scope) => scope.path);
516
+ const task = asRecord(manifestRecord.task);
517
+ const entrypointRecord = asRecord(manifestRecord.entrypoints);
518
+ const newTask = asRecord(entrypointRecord.newTask);
355
519
  return {
356
- id: normalizePresetId(manifest.id || id),
357
- version: Number.parseInt(manifest.version, 10),
358
- purpose: String(manifest.purpose || ""),
359
- compatibleBudgets: asArray(manifest.compatibleBudgets),
360
- localeSupport: asArray(manifest.localeSupport),
361
- task: manifest.task || {},
362
- inputs: normalizeInputs(manifest.inputs || {}),
363
- templateValues: normalizeTemplateValues(manifest.templateValues || {}),
364
- metadata: normalizeTemplateValues(manifest.metadata || {}),
365
- resources: normalizeResources(manifest.resources || {}),
366
- context: normalizeContext(manifest.context || {}),
520
+ id: normalizePresetId(String(manifestRecord.id || id)),
521
+ version: Number.parseInt(String(manifestRecord.version || ""), 10),
522
+ purpose: String(manifestRecord.purpose || ""),
523
+ compatibleBudgets: asArray(manifestRecord.compatibleBudgets),
524
+ localeSupport: asArray(manifestRecord.localeSupport),
525
+ task,
526
+ inputs: normalizeInputs(asRecord(manifestRecord.inputs)),
527
+ templateValues: normalizeTemplateValues(asRecord(manifestRecord.templateValues)),
528
+ metadata: normalizeTemplateValues(asRecord(manifestRecord.metadata)),
529
+ resources: normalizeResources(asRecord(manifestRecord.resources)),
530
+ context: normalizeContext(asRecord(manifestRecord.context)),
367
531
  entrypoints,
368
- workbench: manifest.workbench || {},
369
- evidence: manifest.evidence || {},
370
- review: manifest.review || {},
532
+ actions,
533
+ workbench: asRecord(manifestRecord.workbench),
534
+ evidence: asEvidence(asRecord(manifestRecord.evidence)),
535
+ review: asRecord(manifestRecord.review),
371
536
  audit: {
372
- manifestRequired: asBoolean(manifest.audit?.manifestRequired),
373
- evidenceFiles: asArray(manifest.audit?.evidenceFiles),
537
+ manifestRequired: asBoolean(asRecord(manifestRecord.audit).manifestRequired),
538
+ evidenceFiles: asArray(asRecord(manifestRecord.audit).evidenceFiles),
374
539
  },
375
540
  writeScopes,
376
- newTaskTemplates: manifest.entrypoints?.newTask?.templates || {},
541
+ newTaskTemplates: stringRecord(newTask.templates),
377
542
  directory,
378
543
  source,
379
544
  manifestPath,
380
545
  manifestRelativePath: displayManifestPath(manifestPath),
381
546
  manifestSha256: crypto.createHash("sha256").update(raw).digest("hex"),
547
+ scriptSha256s: collectPresetScriptSha256s({ directory, entrypoints, actions }),
382
548
  };
383
549
  }
550
+ function collectPresetScriptSha256s({ directory, entrypoints, actions }) {
551
+ const scripts = {};
552
+ for (const [name, entrypoint] of Object.entries(entrypoints || {})) {
553
+ if (entrypoint.type !== "script")
554
+ continue;
555
+ const command = String(entrypoint.command || "");
556
+ if (!command)
557
+ continue;
558
+ const absolute = path.join(directory, command);
559
+ if (isInside(directory, absolute) && fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) {
560
+ scripts[`entrypoint:${name}:${command}`] = sha256File(absolute);
561
+ }
562
+ }
563
+ for (const [name, action] of Object.entries(actions || {})) {
564
+ if (action.type !== "script")
565
+ continue;
566
+ const command = String(action.command || "");
567
+ if (!command)
568
+ continue;
569
+ const absolute = path.join(directory, command);
570
+ if (isInside(directory, absolute) && fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) {
571
+ scripts[`action:${name}:${command}`] = sha256File(absolute);
572
+ }
573
+ }
574
+ return scripts;
575
+ }
576
+ function sha256File(filePath) {
577
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
578
+ }
579
+ function recordsEqual(left, right) {
580
+ const leftEntries = Object.entries(left).map(([key, value]) => [key, String(value)]).sort(([a], [b]) => a.localeCompare(b));
581
+ const rightEntries = Object.entries(right).map(([key, value]) => [key, String(value)]).sort(([a], [b]) => a.localeCompare(b));
582
+ return JSON.stringify(leftEntries) === JSON.stringify(rightEntries);
583
+ }
584
+ function asRecord(value) {
585
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
586
+ }
587
+ function stringRecord(value) {
588
+ return Object.fromEntries(Object.entries(asRecord(value)).map(([key, item]) => [key, String(item || "")]));
589
+ }
590
+ function asEvidence(value) {
591
+ if (value.files === undefined)
592
+ return value;
593
+ if (typeof value.files !== "object" || value.files === null || Array.isArray(value.files))
594
+ return value;
595
+ const rawFiles = asRecord(value.files);
596
+ const files = Object.fromEntries(Object.entries(rawFiles).map(([name, item]) => {
597
+ const record = asRecord(item);
598
+ return [name, {
599
+ path: record.path === undefined ? undefined : String(record.path),
600
+ type: record.type === undefined ? undefined : String(record.type),
601
+ value: record.value === undefined ? undefined : String(record.value),
602
+ }];
603
+ }));
604
+ return { ...value, files };
605
+ }
384
606
  function normalizeInputs(rawInputs) {
385
607
  return Object.fromEntries(Object.entries(rawInputs || {}).map(([name, value]) => [name, {
386
- type: String(value.type || "text").trim(),
387
- flag: String(value.flag || "").trim(),
388
- required: asBoolean(value.required),
389
- default: value.default,
390
- validateOperation: String(value.validateOperation || "").trim(),
391
- rejectPlanOnly: asBoolean(value.rejectPlanOnly),
392
- requireTarget: asBoolean(value.requireTarget),
393
- targetFromSession: asBoolean(value.targetFromSession),
608
+ type: String(asRecord(value).type || "text").trim(),
609
+ flag: String(asRecord(value).flag || "").trim(),
610
+ required: asBoolean(asRecord(value).required),
611
+ default: asRecord(value).default,
612
+ validateOperation: String(asRecord(value).validateOperation || "").trim(),
613
+ rejectPlanOnly: asBoolean(asRecord(value).rejectPlanOnly),
614
+ requireTarget: asBoolean(asRecord(value).requireTarget),
615
+ targetFromSession: asBoolean(asRecord(value).targetFromSession),
394
616
  }]));
395
617
  }
396
618
  function normalizeTemplateValues(rawValues) {
397
- return Object.fromEntries(Object.entries(rawValues || {}).map(([name, value]) => [name, typeof value === "object" && value !== null ? value : { value }]));
619
+ return Object.fromEntries(Object.entries(rawValues || {}).map(([name, value]) => [name, typeof value === "object" && value !== null ? asRecord(value) : { value }]));
398
620
  }
399
621
  function normalizeResources(rawResources) {
400
622
  return {
401
- references: normalizeResourceGroup(rawResources.references || {}),
402
- artifacts: normalizeResourceGroup(rawResources.artifacts || {}),
623
+ references: normalizeResourceGroup(asRecord(rawResources.references)),
624
+ artifacts: normalizeResourceGroup(asRecord(rawResources.artifacts)),
403
625
  };
404
626
  }
405
627
  function normalizeResourceGroup(rawGroup) {
406
628
  return Object.fromEntries(Object.entries(rawGroup || {}).map(([name, value]) => [name, {
407
629
  name,
408
- path: String(value.path || "").trim(),
409
- source: String(value.source || "").trim(),
410
- template: String(value.template || "").trim(),
630
+ path: String(asRecord(value).path || "").trim(),
631
+ source: String(asRecord(value).source || "").trim(),
632
+ template: String(asRecord(value).template || "").trim(),
411
633
  index: {
412
- id: String(value.index?.id || "").trim(),
413
- type: String(value.index?.type || "").trim(),
414
- summary: String(value.index?.summary || "").trim(),
415
- usedBy: String(value.index?.usedBy || "").trim(),
416
- producedBy: String(value.index?.producedBy || "").trim(),
634
+ id: String(asRecord(asRecord(value).index).id || "").trim(),
635
+ type: String(asRecord(asRecord(value).index).type || "").trim(),
636
+ summary: String(asRecord(asRecord(value).index).summary || "").trim(),
637
+ usedBy: String(asRecord(asRecord(value).index).usedBy || "").trim(),
638
+ producedBy: String(asRecord(asRecord(value).index).producedBy || "").trim(),
417
639
  },
418
640
  }]));
419
641
  }
@@ -425,13 +647,30 @@ function normalizeContext(rawContext) {
425
647
  function normalizeEntryPoints(rawEntryPoints) {
426
648
  const result = {};
427
649
  for (const [name, value] of Object.entries(rawEntryPoints || {})) {
650
+ const entrypoint = asRecord(value);
428
651
  result[name] = {
429
- type: String(value.type || "").trim(),
430
- command: value.command ? String(value.command).trim() : "",
431
- templates: value.templates || {},
432
- writes: asArray(value.writes),
433
- reads: asArray(value.reads),
434
- audit: asBoolean(value.audit),
652
+ type: String(entrypoint.type || "").trim(),
653
+ command: entrypoint.command ? String(entrypoint.command).trim() : "",
654
+ templates: stringRecord(entrypoint.templates),
655
+ writes: asArray(entrypoint.writes),
656
+ reads: asArray(entrypoint.reads),
657
+ audit: asBoolean(entrypoint.audit),
658
+ };
659
+ }
660
+ return result;
661
+ }
662
+ function normalizeActions(rawActions) {
663
+ const result = {};
664
+ for (const [name, value] of Object.entries(rawActions || {})) {
665
+ const action = asRecord(value);
666
+ result[name] = {
667
+ type: String(action.type || "").trim(),
668
+ command: action.command ? String(action.command).trim() : "",
669
+ taskRequired: asBoolean(action.taskRequired),
670
+ inputs: normalizeInputs(asRecord(action.inputs)),
671
+ writes: asArray(action.writes),
672
+ reads: asArray(action.reads),
673
+ audit: asBoolean(action.audit),
435
674
  };
436
675
  }
437
676
  return result;
@@ -445,6 +684,7 @@ function publicPresetShape(preset) {
445
684
  localeSupport: preset.localeSupport,
446
685
  task: preset.task,
447
686
  entrypoints: preset.entrypoints,
687
+ actions: preset.actions,
448
688
  workbench: preset.workbench,
449
689
  evidence: preset.evidence,
450
690
  review: preset.review,
@@ -458,6 +698,7 @@ function publicPresetShape(preset) {
458
698
  source: preset.source,
459
699
  manifestPath: preset.manifestRelativePath,
460
700
  manifestSha256: preset.manifestSha256,
701
+ scriptPolicy: buildPresetScriptPolicy(preset),
461
702
  };
462
703
  }
463
704
  function validateResourceCollection(preset, label, groupName, requiredPrefix, resourcePaths, failures) {
@@ -487,13 +728,14 @@ function validateResourceCollection(preset, label, groupName, requiredPrefix, re
487
728
  if (resource.source && resource.template)
488
729
  failures.push(`${label} resource ${name} cannot declare both source and template`);
489
730
  for (const field of ["source", "template"]) {
490
- if (!resource[field])
731
+ const declaredPath = resource[field];
732
+ if (!declaredPath)
491
733
  continue;
492
- const resourcePath = path.join(preset.directory, resource[field]);
734
+ const resourcePath = path.join(preset.directory, declaredPath);
493
735
  if (!isInside(preset.directory, resourcePath))
494
736
  failures.push(`${label} resource ${name} ${field} escapes preset package`);
495
737
  else
496
- validatePresetPackageFile(preset, resource[field], `${label} resource ${name} ${field}`, failures);
738
+ validatePresetPackageFile(preset, declaredPath, `${label} resource ${name} ${field}`, failures);
497
739
  }
498
740
  const id = resource.index?.id || "";
499
741
  if (!id)
@@ -526,6 +768,57 @@ function validateAuditEvidenceFiles(preset, failures) {
526
768
  }
527
769
  }
528
770
  }
771
+ function validateActionPathTemplateTokens(content, label) {
772
+ const failures = validateHarnessPathTemplateTokens(content, label);
773
+ const allowedTaskTokens = new Set([
774
+ "task.paths.dir",
775
+ "task.paths.taskPlan",
776
+ "task.paths.progress",
777
+ "task.paths.artifacts",
778
+ "task.paths.artifactsIndex",
779
+ "task.paths.visualMap",
780
+ ]);
781
+ for (const match of String(content || "").matchAll(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g)) {
782
+ const key = match[1];
783
+ if (key.startsWith("paths."))
784
+ continue;
785
+ if (key.startsWith("task.") && !allowedTaskTokens.has(key))
786
+ failures.push(`${label} uses unknown task token: ${key}`);
787
+ }
788
+ return failures;
789
+ }
790
+ function isBroadTaskRootScope(scope) {
791
+ const normalized = toPosix(path.normalize(String(scope || "")));
792
+ return normalized === "{{paths.tasksRoot}}/**" ||
793
+ normalized === "coding-agent-harness/planning/tasks/**" ||
794
+ normalized === ["docs", "09-PLANNING", "TASKS", "**"].join("/");
795
+ }
796
+ function usesTaskLocalPath(scope) {
797
+ return String(scope || "").includes("{{task.paths.");
798
+ }
799
+ function newTaskWriteScopeAllowed(writeScope) {
800
+ const normalized = toPosix(path.normalize(String(writeScope || "")));
801
+ const legacyPlanningScope = ["docs", "09-PLANNING"].join("/");
802
+ return (normalized === "coding-agent-harness/planning/**" ||
803
+ normalized.startsWith("coding-agent-harness/planning/") ||
804
+ normalized === "{{paths.planningRoot}}/**" ||
805
+ normalized.startsWith("{{paths.planningRoot}}/") ||
806
+ normalized === "{{paths.tasksRoot}}/**" ||
807
+ normalized.startsWith("{{paths.tasksRoot}}/") ||
808
+ normalized === "{{paths.modulesRoot}}/**" ||
809
+ normalized.startsWith("{{paths.modulesRoot}}/") ||
810
+ normalized === `${legacyPlanningScope}/**` ||
811
+ normalized.startsWith(`${legacyPlanningScope}/`));
812
+ }
813
+ function warnOnRuntimePathLiterals(filePath, label, warnings) {
814
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
815
+ return;
816
+ const content = fs.readFileSync(filePath, "utf8");
817
+ const runtimeLiteralPattern = /coding-agent-harness\/(?:planning|governance|context)\//;
818
+ if (runtimeLiteralPattern.test(content) && !content.includes("context.paths") && !content.includes("context.absolutePaths")) {
819
+ warnings.push(`${label} contains default harness path literals; prefer context.paths from the preset runner`);
820
+ }
821
+ }
529
822
  function validatePresetPackageFile(preset, relativePath, label, failures) {
530
823
  const filePath = path.join(preset.directory, relativePath || "");
531
824
  if (!isInside(preset.directory, filePath)) {
@@ -556,7 +849,8 @@ export function parseSimpleYaml(source) {
556
849
  for (const rawLine of String(source).split(/\r?\n/)) {
557
850
  if (!rawLine.trim() || rawLine.trimStart().startsWith("#"))
558
851
  continue;
559
- const indent = rawLine.match(/^\s*/)[0].length;
852
+ const indentMatch = rawLine.match(/^\s*/);
853
+ const indent = indentMatch ? indentMatch[0].length : 0;
560
854
  const line = rawLine.trim();
561
855
  const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
562
856
  if (!match)
@@ -568,7 +862,7 @@ export function parseSimpleYaml(source) {
568
862
  const rawValue = match[2] || "";
569
863
  if (!rawValue) {
570
864
  parent[key] = {};
571
- stack.push({ indent, object: parent[key] });
865
+ stack.push({ indent, object: asRecord(parent[key]) });
572
866
  }
573
867
  else {
574
868
  parent[key] = parseYamlScalar(rawValue);
@@ -615,7 +909,14 @@ function hasMarkdownTableDelimiter(value) {
615
909
  return /[|\r\n]/.test(String(value || ""));
616
910
  }
617
911
  function getValue(values, key) {
618
- return String(key).split(".").reduce((cursor, part) => (cursor && Object.prototype.hasOwnProperty.call(cursor, part) ? cursor[part] : undefined), values);
912
+ let cursor = values;
913
+ for (const part of String(key).split(".")) {
914
+ if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
915
+ return undefined;
916
+ const record = cursor;
917
+ cursor = Object.prototype.hasOwnProperty.call(record, part) ? record[part] : undefined;
918
+ }
919
+ return cursor;
619
920
  }
620
921
  function displayManifestPath(manifestPath) {
621
922
  const relative = path.relative(repoRoot, manifestPath);
@@ -650,7 +951,7 @@ function presetSearchRoots({ targetInput = "", home = "" } = {}) {
650
951
  function resolveInstallSource(source) {
651
952
  const localPath = path.resolve(source);
652
953
  if (fs.existsSync(path.join(localPath, "preset.yaml")))
653
- return { path: localPath, cleanup: () => { } };
954
+ return { path: localPath, source: "local", cleanup: () => { } };
654
955
  if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
655
956
  if (!localPath.toLowerCase().endsWith(".zip"))
656
957
  throw new Error(`Preset source file must be a .zip archive: ${toPosix(localPath)}`);
@@ -658,7 +959,7 @@ function resolveInstallSource(source) {
658
959
  }
659
960
  const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
660
961
  if (fs.existsSync(path.join(builtinPath, "preset.yaml")))
661
- return { path: builtinPath, cleanup: () => { } };
962
+ return { path: builtinPath, source: "builtin", cleanup: () => { } };
662
963
  throw new Error(`Preset source not found: ${source}`);
663
964
  }
664
965
  function resolveZipInstallSource(sourcePath) {
@@ -667,6 +968,7 @@ function resolveZipInstallSource(sourcePath) {
667
968
  extractPresetZip(sourcePath, tempRoot);
668
969
  return {
669
970
  path: presetRootFromExtractedArchive(tempRoot),
971
+ source: "archive",
670
972
  cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
671
973
  };
672
974
  }