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,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,6 +313,7 @@ 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
  }
@@ -268,10 +327,56 @@ export function validatePresetPackage(preset) {
268
327
  failures.push(`${name} missing command`);
269
328
  else if (!isInside(preset.directory, entryPath))
270
329
  failures.push(`${name} command escapes preset package`);
271
- else
330
+ else {
272
331
  validatePresetPackageFile(preset, entrypoint.command, `${name} command`, failures);
332
+ warnOnRuntimePathLiterals(entryPath, `${name} command`, warnings);
333
+ }
273
334
  }
335
+ for (const readScope of entrypoint.reads || [])
336
+ failures.push(...validateHarnessPathTemplateTokens(readScope, `${name} read scope`));
274
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
+ }
376
+ }
377
+ }
378
+ for (const scope of preset.writeScopes)
379
+ failures.push(...validateHarnessPathTemplateTokens(scope.path, `${scope.name || "write scope"} path`));
275
380
  for (const [templateKey, templatePath] of Object.entries(preset.newTaskTemplates)) {
276
381
  if (!allowedNewTaskTemplateKeys.has(templateKey)) {
277
382
  failures.push(`unsupported newTask template: ${templateKey}`);
@@ -295,6 +400,7 @@ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypo
295
400
  version: preset.version,
296
401
  manifestPath: preset.manifestRelativePath,
297
402
  manifestSha256: preset.manifestSha256,
403
+ scriptSha256s: preset.scriptSha256s,
298
404
  entrypoints,
299
405
  writeScopes: scopes,
300
406
  resolvedInputs,
@@ -303,6 +409,55 @@ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypo
303
409
  generatedAt: new Date().toISOString(),
304
410
  };
305
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
+ }
306
461
  export function renderPresetTemplate(preset, templatePath, values) {
307
462
  if (!templatePath)
308
463
  return "";
@@ -310,10 +465,7 @@ export function renderPresetTemplate(preset, templatePath, values) {
310
465
  if (!isInside(preset.directory, absolute))
311
466
  throw new Error(`Preset template escapes package: ${templatePath}`);
312
467
  const content = fs.readFileSync(absolute, "utf8");
313
- return content.replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, key) => {
314
- const value = getValue(values, key);
315
- return value == null ? "" : String(value);
316
- });
468
+ return renderHarnessTemplate(content, values, { missing: "empty" });
317
469
  }
318
470
  function normalizePresetId(id) {
319
471
  const normalized = String(id || "").trim().toLowerCase().replaceAll("_", "-");
@@ -350,74 +502,140 @@ function projectPresetDestination(id, targetInput) {
350
502
  }
351
503
  function normalizePresetManifest(manifest, { id, manifestPath, raw, source }) {
352
504
  const directory = path.dirname(manifestPath);
353
- const entrypoints = normalizeEntryPoints(manifest.entrypoints || {});
354
- const writeScopes = Object.entries(manifest.writeScopes || {}).map(([name, value]) => ({
355
- name,
356
- path: String(value.path || value || "").trim(),
357
- access: String(value.access || "write").trim(),
358
- })).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);
359
519
  return {
360
- id: normalizePresetId(manifest.id || id),
361
- version: Number.parseInt(manifest.version, 10),
362
- purpose: String(manifest.purpose || ""),
363
- compatibleBudgets: asArray(manifest.compatibleBudgets),
364
- localeSupport: asArray(manifest.localeSupport),
365
- task: manifest.task || {},
366
- inputs: normalizeInputs(manifest.inputs || {}),
367
- templateValues: normalizeTemplateValues(manifest.templateValues || {}),
368
- metadata: normalizeTemplateValues(manifest.metadata || {}),
369
- resources: normalizeResources(manifest.resources || {}),
370
- 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)),
371
531
  entrypoints,
372
- workbench: manifest.workbench || {},
373
- evidence: manifest.evidence || {},
374
- review: manifest.review || {},
532
+ actions,
533
+ workbench: asRecord(manifestRecord.workbench),
534
+ evidence: asEvidence(asRecord(manifestRecord.evidence)),
535
+ review: asRecord(manifestRecord.review),
375
536
  audit: {
376
- manifestRequired: asBoolean(manifest.audit?.manifestRequired),
377
- evidenceFiles: asArray(manifest.audit?.evidenceFiles),
537
+ manifestRequired: asBoolean(asRecord(manifestRecord.audit).manifestRequired),
538
+ evidenceFiles: asArray(asRecord(manifestRecord.audit).evidenceFiles),
378
539
  },
379
540
  writeScopes,
380
- newTaskTemplates: manifest.entrypoints?.newTask?.templates || {},
541
+ newTaskTemplates: stringRecord(newTask.templates),
381
542
  directory,
382
543
  source,
383
544
  manifestPath,
384
545
  manifestRelativePath: displayManifestPath(manifestPath),
385
546
  manifestSha256: crypto.createHash("sha256").update(raw).digest("hex"),
547
+ scriptSha256s: collectPresetScriptSha256s({ directory, entrypoints, actions }),
386
548
  };
387
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
+ }
388
606
  function normalizeInputs(rawInputs) {
389
607
  return Object.fromEntries(Object.entries(rawInputs || {}).map(([name, value]) => [name, {
390
- type: String(value.type || "text").trim(),
391
- flag: String(value.flag || "").trim(),
392
- required: asBoolean(value.required),
393
- default: value.default,
394
- validateOperation: String(value.validateOperation || "").trim(),
395
- rejectPlanOnly: asBoolean(value.rejectPlanOnly),
396
- requireTarget: asBoolean(value.requireTarget),
397
- 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),
398
616
  }]));
399
617
  }
400
618
  function normalizeTemplateValues(rawValues) {
401
- 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 }]));
402
620
  }
403
621
  function normalizeResources(rawResources) {
404
622
  return {
405
- references: normalizeResourceGroup(rawResources.references || {}),
406
- artifacts: normalizeResourceGroup(rawResources.artifacts || {}),
623
+ references: normalizeResourceGroup(asRecord(rawResources.references)),
624
+ artifacts: normalizeResourceGroup(asRecord(rawResources.artifacts)),
407
625
  };
408
626
  }
409
627
  function normalizeResourceGroup(rawGroup) {
410
628
  return Object.fromEntries(Object.entries(rawGroup || {}).map(([name, value]) => [name, {
411
629
  name,
412
- path: String(value.path || "").trim(),
413
- source: String(value.source || "").trim(),
414
- 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(),
415
633
  index: {
416
- id: String(value.index?.id || "").trim(),
417
- type: String(value.index?.type || "").trim(),
418
- summary: String(value.index?.summary || "").trim(),
419
- usedBy: String(value.index?.usedBy || "").trim(),
420
- 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(),
421
639
  },
422
640
  }]));
423
641
  }
@@ -429,13 +647,30 @@ function normalizeContext(rawContext) {
429
647
  function normalizeEntryPoints(rawEntryPoints) {
430
648
  const result = {};
431
649
  for (const [name, value] of Object.entries(rawEntryPoints || {})) {
650
+ const entrypoint = asRecord(value);
651
+ result[name] = {
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);
432
666
  result[name] = {
433
- type: String(value.type || "").trim(),
434
- command: value.command ? String(value.command).trim() : "",
435
- templates: value.templates || {},
436
- writes: asArray(value.writes),
437
- reads: asArray(value.reads),
438
- audit: asBoolean(value.audit),
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),
439
674
  };
440
675
  }
441
676
  return result;
@@ -449,6 +684,7 @@ function publicPresetShape(preset) {
449
684
  localeSupport: preset.localeSupport,
450
685
  task: preset.task,
451
686
  entrypoints: preset.entrypoints,
687
+ actions: preset.actions,
452
688
  workbench: preset.workbench,
453
689
  evidence: preset.evidence,
454
690
  review: preset.review,
@@ -462,6 +698,7 @@ function publicPresetShape(preset) {
462
698
  source: preset.source,
463
699
  manifestPath: preset.manifestRelativePath,
464
700
  manifestSha256: preset.manifestSha256,
701
+ scriptPolicy: buildPresetScriptPolicy(preset),
465
702
  };
466
703
  }
467
704
  function validateResourceCollection(preset, label, groupName, requiredPrefix, resourcePaths, failures) {
@@ -491,13 +728,14 @@ function validateResourceCollection(preset, label, groupName, requiredPrefix, re
491
728
  if (resource.source && resource.template)
492
729
  failures.push(`${label} resource ${name} cannot declare both source and template`);
493
730
  for (const field of ["source", "template"]) {
494
- if (!resource[field])
731
+ const declaredPath = resource[field];
732
+ if (!declaredPath)
495
733
  continue;
496
- const resourcePath = path.join(preset.directory, resource[field]);
734
+ const resourcePath = path.join(preset.directory, declaredPath);
497
735
  if (!isInside(preset.directory, resourcePath))
498
736
  failures.push(`${label} resource ${name} ${field} escapes preset package`);
499
737
  else
500
- validatePresetPackageFile(preset, resource[field], `${label} resource ${name} ${field}`, failures);
738
+ validatePresetPackageFile(preset, declaredPath, `${label} resource ${name} ${field}`, failures);
501
739
  }
502
740
  const id = resource.index?.id || "";
503
741
  if (!id)
@@ -530,14 +768,57 @@ function validateAuditEvidenceFiles(preset, failures) {
530
768
  }
531
769
  }
532
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
+ }
533
799
  function newTaskWriteScopeAllowed(writeScope) {
534
800
  const normalized = toPosix(path.normalize(String(writeScope || "")));
535
801
  const legacyPlanningScope = ["docs", "09-PLANNING"].join("/");
536
802
  return (normalized === "coding-agent-harness/planning/**" ||
537
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}}/") ||
538
810
  normalized === `${legacyPlanningScope}/**` ||
539
811
  normalized.startsWith(`${legacyPlanningScope}/`));
540
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
+ }
541
822
  function validatePresetPackageFile(preset, relativePath, label, failures) {
542
823
  const filePath = path.join(preset.directory, relativePath || "");
543
824
  if (!isInside(preset.directory, filePath)) {
@@ -568,7 +849,8 @@ export function parseSimpleYaml(source) {
568
849
  for (const rawLine of String(source).split(/\r?\n/)) {
569
850
  if (!rawLine.trim() || rawLine.trimStart().startsWith("#"))
570
851
  continue;
571
- const indent = rawLine.match(/^\s*/)[0].length;
852
+ const indentMatch = rawLine.match(/^\s*/);
853
+ const indent = indentMatch ? indentMatch[0].length : 0;
572
854
  const line = rawLine.trim();
573
855
  const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
574
856
  if (!match)
@@ -580,7 +862,7 @@ export function parseSimpleYaml(source) {
580
862
  const rawValue = match[2] || "";
581
863
  if (!rawValue) {
582
864
  parent[key] = {};
583
- stack.push({ indent, object: parent[key] });
865
+ stack.push({ indent, object: asRecord(parent[key]) });
584
866
  }
585
867
  else {
586
868
  parent[key] = parseYamlScalar(rawValue);
@@ -627,7 +909,14 @@ function hasMarkdownTableDelimiter(value) {
627
909
  return /[|\r\n]/.test(String(value || ""));
628
910
  }
629
911
  function getValue(values, key) {
630
- 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;
631
920
  }
632
921
  function displayManifestPath(manifestPath) {
633
922
  const relative = path.relative(repoRoot, manifestPath);
@@ -662,7 +951,7 @@ function presetSearchRoots({ targetInput = "", home = "" } = {}) {
662
951
  function resolveInstallSource(source) {
663
952
  const localPath = path.resolve(source);
664
953
  if (fs.existsSync(path.join(localPath, "preset.yaml")))
665
- return { path: localPath, cleanup: () => { } };
954
+ return { path: localPath, source: "local", cleanup: () => { } };
666
955
  if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
667
956
  if (!localPath.toLowerCase().endsWith(".zip"))
668
957
  throw new Error(`Preset source file must be a .zip archive: ${toPosix(localPath)}`);
@@ -670,7 +959,7 @@ function resolveInstallSource(source) {
670
959
  }
671
960
  const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
672
961
  if (fs.existsSync(path.join(builtinPath, "preset.yaml")))
673
- return { path: builtinPath, cleanup: () => { } };
962
+ return { path: builtinPath, source: "builtin", cleanup: () => { } };
674
963
  throw new Error(`Preset source not found: ${source}`);
675
964
  }
676
965
  function resolveZipInstallSource(sourcePath) {
@@ -679,6 +968,7 @@ function resolveZipInstallSource(sourcePath) {
679
968
  extractPresetZip(sourcePath, tempRoot);
680
969
  return {
681
970
  path: presetRootFromExtractedArchive(tempRoot),
971
+ source: "archive",
682
972
  cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
683
973
  };
684
974
  }