coding-agent-harness 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTING.md +98 -0
  3. package/README.md +211 -86
  4. package/README.zh-CN.md +54 -34
  5. package/SKILL.md +25 -18
  6. package/docs-release/README.md +9 -5
  7. package/docs-release/architecture/overview.md +17 -5
  8. package/docs-release/architecture/overview.zh-CN.md +9 -5
  9. package/docs-release/assets/dashboard-overview.png +0 -0
  10. package/docs-release/guides/agent-installation.en-US.md +31 -8
  11. package/docs-release/guides/agent-installation.md +34 -9
  12. package/docs-release/guides/contributing.md +100 -0
  13. package/docs-release/guides/contributing.zh-CN.md +99 -0
  14. package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
  15. package/docs-release/guides/document-audience-and-surfaces.md +3 -2
  16. package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
  17. package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
  18. package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
  19. package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
  20. package/docs-release/guides/migration-playbook.en-US.md +14 -15
  21. package/docs-release/guides/migration-playbook.md +14 -15
  22. package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
  23. package/docs-release/guides/parent-control-repository-pattern.md +7 -5
  24. package/docs-release/guides/preset-development.md +214 -0
  25. package/docs-release/guides/repository-operating-models.en-US.md +5 -4
  26. package/docs-release/guides/repository-operating-models.md +5 -4
  27. package/docs-release/guides/task-state-machine.en-US.md +207 -0
  28. package/docs-release/guides/task-state-machine.md +214 -0
  29. package/docs-release/intl/en-US.md +1 -1
  30. package/docs-release/intl/zh-CN.md +1 -1
  31. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
  32. package/package.json +8 -3
  33. package/presets/legacy-migration/checks/preset-check.mjs +3 -0
  34. package/presets/legacy-migration/preset.yaml +134 -0
  35. package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
  36. package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
  37. package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
  38. package/presets/legacy-migration/templates/findings.seed.md +17 -0
  39. package/presets/legacy-migration/templates/review.seed.md +12 -0
  40. package/presets/legacy-migration/templates/task_plan.append.md +9 -0
  41. package/presets/legacy-migration/templates/visual_map.append.md +12 -0
  42. package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
  43. package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
  44. package/presets/lesson-sedimentation/preset.yaml +23 -0
  45. package/presets/lesson-sedimentation/templates/prompt.md +23 -0
  46. package/presets/module/preset.yaml +25 -0
  47. package/presets/module/templates/execution_strategy.append.md +8 -0
  48. package/presets/module/templates/task_plan.append.md +17 -0
  49. package/presets/standard-task/preset.yaml +31 -0
  50. package/presets/standard-task/templates/task_plan.append.md +7 -0
  51. package/references/adversarial-review-standard.md +2 -2
  52. package/references/agents-md-pattern.md +2 -2
  53. package/references/delivery-operating-model-standard.md +3 -3
  54. package/references/docs-directory-standard.md +6 -7
  55. package/references/harness-ledger.md +53 -96
  56. package/references/lessons-governance.md +88 -93
  57. package/references/module-parallel-standard.md +14 -14
  58. package/references/planning-loop.md +12 -6
  59. package/references/pull-request-standard.md +118 -0
  60. package/references/repo-governance-standard.md +11 -2
  61. package/references/review-routing-standard.md +7 -1
  62. package/references/ssot-governance.md +67 -59
  63. package/references/taskr-gap-analysis.md +600 -0
  64. package/references/walkthrough-closeout.md +7 -7
  65. package/scripts/check-harness.mjs +40 -301
  66. package/scripts/commands/dashboard-command.mjs +67 -0
  67. package/scripts/commands/migration-command.mjs +96 -0
  68. package/scripts/commands/preset-command.mjs +73 -0
  69. package/scripts/commands/task-command.mjs +327 -0
  70. package/scripts/harness.mjs +55 -260
  71. package/scripts/lib/capability-registry.mjs +66 -8
  72. package/scripts/lib/check-module-parallel.mjs +237 -0
  73. package/scripts/lib/check-profiles.mjs +61 -153
  74. package/scripts/lib/check-task-contracts.mjs +47 -0
  75. package/scripts/lib/core-shared.mjs +10 -0
  76. package/scripts/lib/dashboard-data.mjs +29 -6
  77. package/scripts/lib/dashboard-workbench.mjs +52 -12
  78. package/scripts/lib/dashboard-writer.mjs +14 -2
  79. package/scripts/lib/git-status-summary.mjs +46 -0
  80. package/scripts/lib/governance-index-generator.mjs +174 -0
  81. package/scripts/lib/governance-sync.mjs +514 -0
  82. package/scripts/lib/governance-table-boundary.mjs +175 -0
  83. package/scripts/lib/harness-core.mjs +5 -0
  84. package/scripts/lib/lesson-maintenance.mjs +36 -29
  85. package/scripts/lib/migration-support.mjs +1 -1
  86. package/scripts/lib/preset-audit-contracts.mjs +37 -0
  87. package/scripts/lib/preset-engine.mjs +497 -0
  88. package/scripts/lib/preset-registry.mjs +627 -0
  89. package/scripts/lib/preset-resource-contracts.mjs +83 -0
  90. package/scripts/lib/review-confirm-git-gate.mjs +248 -0
  91. package/scripts/lib/status-dashboard-renderer.mjs +102 -0
  92. package/scripts/lib/subagent-authorization-audit.mjs +196 -0
  93. package/scripts/lib/task-completion-consistency.mjs +16 -0
  94. package/scripts/lib/task-index.mjs +93 -0
  95. package/scripts/lib/task-lesson-candidates.mjs +242 -0
  96. package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
  97. package/scripts/lib/task-lifecycle/review-confirm.mjs +101 -0
  98. package/scripts/lib/task-lifecycle/review-gates.mjs +70 -0
  99. package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
  100. package/scripts/lib/task-lifecycle.mjs +297 -403
  101. package/scripts/lib/task-review-model.mjs +469 -0
  102. package/scripts/lib/task-scanner.mjs +130 -236
  103. package/scripts/lib/task-tombstone-commands.mjs +140 -0
  104. package/scripts/postinstall.mjs +14 -0
  105. package/skills/preset-creator/SKILL.md +179 -0
  106. package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
  107. package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
  108. package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -0
  109. package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
  110. package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
  111. package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
  112. package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
  113. package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
  114. package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
  115. package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
  116. package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
  117. package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
  118. package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
  119. package/templates/AGENTS.md.template +19 -15
  120. package/templates/dashboard/assets/app-src/00-state.js +1 -0
  121. package/templates/dashboard/assets/app-src/10-router.js +2 -1
  122. package/templates/dashboard/assets/app-src/20-overview.js +11 -5
  123. package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
  124. package/templates/dashboard/assets/app-src/35-task-detail.js +246 -0
  125. package/templates/dashboard/assets/app-src/45-review.js +241 -22
  126. package/templates/dashboard/assets/app-src/50-migration.js +24 -10
  127. package/templates/dashboard/assets/app-src/90-bindings.js +171 -29
  128. package/templates/dashboard/assets/app.css +698 -156
  129. package/templates/dashboard/assets/app.css.manifest.json +9 -0
  130. package/templates/dashboard/assets/app.js +662 -91
  131. package/templates/dashboard/assets/app.manifest.json +1 -0
  132. package/templates/dashboard/assets/css-src/00-foundation.css +342 -0
  133. package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
  134. package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
  135. package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
  136. package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
  137. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +427 -0
  138. package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
  139. package/templates/dashboard/assets/i18n.js +123 -21
  140. package/templates/ledger/Harness-Ledger.md +13 -25
  141. package/templates/lessons/lesson-arch-process-change.md +1 -1
  142. package/templates/lessons/lesson-new-doc.md +1 -1
  143. package/templates/lessons/lesson-ref-change.md +1 -1
  144. package/templates/planning/execution_strategy.md +31 -0
  145. package/templates/planning/lesson_candidates.md +18 -6
  146. package/templates/planning/optional/artifacts/INDEX.md +3 -3
  147. package/templates/planning/optional/references/INDEX.md +3 -3
  148. package/templates/planning/review.md +59 -0
  149. package/templates/planning/task_plan.md +36 -13
  150. package/templates/reference/execution-workflow-standard.md +4 -3
  151. package/templates/reference/pull-request-standard.md +80 -0
  152. package/templates/reference/repo-governance-standard.md +7 -6
  153. package/templates/reference/review-routing-standard.md +6 -0
  154. package/templates/reference/walkthrough-standard.md +2 -1
  155. package/templates/verifier/verifier-output.md +1 -1
  156. package/templates-zh-CN/AGENTS.md.template +20 -16
  157. package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
  158. package/templates-zh-CN/planning/execution_strategy.md +30 -0
  159. package/templates-zh-CN/planning/lesson_candidates.md +18 -6
  160. package/templates-zh-CN/planning/review.md +59 -1
  161. package/templates-zh-CN/planning/task_plan.md +30 -10
  162. package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
  163. package/templates-zh-CN/reference/docs-library-standard.md +1 -1
  164. package/templates-zh-CN/reference/execution-workflow-standard.md +4 -3
  165. package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
  166. package/templates-zh-CN/reference/pull-request-standard.md +106 -0
  167. package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
  168. package/templates-zh-CN/reference/review-routing-standard.md +8 -1
  169. package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
  170. package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
  171. package/docs-release/assets/dashboard-overview-en.png +0 -0
  172. package/scripts/smoke-dashboard.mjs +0 -92
  173. package/scripts/test-harness.mjs +0 -1395
  174. package/templates/ssot/Feature-SSoT.md +0 -43
  175. package/templates/ssot/Lessons-SSoT.md +0 -44
  176. package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
  177. package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
@@ -0,0 +1,627 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { builtinPresetRoot, projectPresetRoot, repoRoot, toPosix, userPresetRoot, userPresetRootForHome } from "./core-shared.mjs";
5
+
6
+ const allowedEntrypoints = new Set(["newTask", "plan", "scaffold", "check"]);
7
+ const allowedEntrypointTypes = new Set(["template", "script", "check"]);
8
+ 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"]);
9
+
10
+ export function listPresetPackages({ targetInput = "", home = "" } = {}) {
11
+ const seen = new Set();
12
+ const presets = [];
13
+ for (const { root, source } of presetSearchRoots({ targetInput, home })) {
14
+ if (!fs.existsSync(root)) continue;
15
+ for (const entry of fs.readdirSync(root, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
16
+ const id = tryNormalizePresetId(entry.name);
17
+ if (!id) continue;
18
+ if (seen.has(id)) continue;
19
+ seen.add(id);
20
+ presets.push(readPresetPackage(id, { targetInput, home }));
21
+ }
22
+ }
23
+ return presets;
24
+ }
25
+
26
+ export function readPresetPackage(id, { targetInput = "", home = "" } = {}) {
27
+ const normalizedId = normalizePresetId(id);
28
+ const found = findPresetManifest(normalizedId, { targetInput, home });
29
+ const manifestPath = found?.manifestPath || "";
30
+ if (!fs.existsSync(manifestPath)) {
31
+ const known = listPresetIds({ targetInput, home });
32
+ throw new Error(`Invalid task preset: ${id}. Expected one of: ${known.join(", ") || "(none)"}`);
33
+ }
34
+ assertPresetDirectory(path.dirname(manifestPath));
35
+ assertPresetManifestFile(path.dirname(manifestPath), manifestPath);
36
+ const raw = fs.readFileSync(manifestPath, "utf8");
37
+ const manifest = parseSimpleYaml(raw);
38
+ const preset = normalizePresetManifest(manifest, { id: normalizedId, manifestPath, raw, source: found.source });
39
+ const report = validatePresetPackage(preset);
40
+ if (report.failures.length) throw new Error(`Invalid preset package ${normalizedId}: ${report.failures.join("; ")}`);
41
+ return preset;
42
+ }
43
+
44
+ export function inspectPresetPackage(id, { targetInput = "", home = "" } = {}) {
45
+ const preset = fs.existsSync(path.join(path.resolve(id || ""), "preset.yaml")) ? readPresetPackageFromPath(path.resolve(id)) : readPresetPackage(id, { targetInput, home });
46
+ return publicPresetShape(preset);
47
+ }
48
+
49
+ export function checkPresetPackage(id, { targetInput = "", home = "" } = {}) {
50
+ const preset = fs.existsSync(path.join(path.resolve(id || ""), "preset.yaml")) ? readPresetPackageFromPath(path.resolve(id)) : readPresetPackage(id, { targetInput, home });
51
+ const report = validatePresetPackage(preset);
52
+ return {
53
+ id: preset.id,
54
+ version: preset.version,
55
+ status: report.failures.length === 0 ? "pass" : "fail",
56
+ failures: report.failures,
57
+ warnings: report.warnings,
58
+ manifestPath: preset.manifestRelativePath,
59
+ source: preset.source,
60
+ inputs: preset.inputs,
61
+ templateValues: preset.templateValues,
62
+ metadata: preset.metadata,
63
+ resources: preset.resources,
64
+ context: preset.context,
65
+ entrypoints: preset.entrypoints,
66
+ writeScopes: preset.writeScopes,
67
+ };
68
+ }
69
+
70
+ function readPresetPackageFromPath(directory) {
71
+ const manifestPath = path.join(directory, "preset.yaml");
72
+ assertPresetDirectory(directory);
73
+ assertPresetManifestFile(directory, manifestPath);
74
+ const raw = fs.readFileSync(manifestPath, "utf8");
75
+ const manifest = parseSimpleYaml(raw);
76
+ return normalizePresetManifest(manifest, { id: normalizePresetId(manifest.id || path.basename(directory)), manifestPath, raw, source: "local" });
77
+ }
78
+
79
+ function assertPresetDirectory(directory) {
80
+ if (!fs.existsSync(directory)) throw new Error(`Preset package directory missing: ${toPosix(directory)}`);
81
+ const stat = fs.lstatSync(directory);
82
+ if (stat.isSymbolicLink()) throw new Error(`Preset package directory must not be a symlink: ${toPosix(directory)}`);
83
+ if (!stat.isDirectory()) throw new Error(`Preset package path must be a directory: ${toPosix(directory)}`);
84
+ }
85
+
86
+ function assertPresetManifestFile(directory, manifestPath) {
87
+ if (!fs.existsSync(manifestPath)) throw new Error(`Preset manifest missing: ${displayManifestPath(manifestPath)}`);
88
+ const stat = fs.lstatSync(manifestPath);
89
+ if (stat.isSymbolicLink()) throw new Error(`Preset manifest must not be a symlink: ${displayManifestPath(manifestPath)}`);
90
+ if (!stat.isFile()) throw new Error(`Preset manifest must be a file: ${displayManifestPath(manifestPath)}`);
91
+ const realRoot = fs.realpathSync(directory);
92
+ const realPath = fs.realpathSync(manifestPath);
93
+ if (!isInside(realRoot, realPath)) throw new Error(`Preset manifest real path escapes preset package: ${displayManifestPath(manifestPath)}`);
94
+ }
95
+
96
+ export function installPresetPackage(source, { force = false, scope = "user", targetInput = ".", home = "" } = {}) {
97
+ if (!source) throw new Error("Missing preset source");
98
+ const sourcePath = resolveInstallSource(source);
99
+ const stagedPreset = readPresetPackageFromPath(sourcePath);
100
+ const stagedReport = validatePresetPackage(stagedPreset);
101
+ if (stagedReport.failures.length) throw new Error(`Invalid preset package ${stagedPreset.id}: ${stagedReport.failures.join("; ")}`);
102
+ const id = stagedPreset.id;
103
+ if (!id) throw new Error("Preset manifest missing id");
104
+ const destination = scope === "project" ? projectPresetDestination(id, targetInput) : userPresetDestination(id, { home });
105
+ if (fs.existsSync(destination)) {
106
+ if (!force) throw new Error(`Preset already installed: ${id}. Re-run with --force to overwrite.`);
107
+ }
108
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
109
+ const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
110
+ fs.rmSync(tempDestination, { recursive: true, force: true });
111
+ copyDirectory(sourcePath, tempDestination);
112
+ try {
113
+ const tempPreset = readPresetPackageFromPath(tempDestination);
114
+ const tempReport = validatePresetPackage(tempPreset);
115
+ if (tempReport.failures.length) throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
116
+ fs.rmSync(destination, { recursive: true, force: true });
117
+ fs.renameSync(tempDestination, destination);
118
+ const preset = readPresetPackage(id, scope === "project" ? { targetInput, home } : { home });
119
+ return {
120
+ installed: true,
121
+ id: preset.id,
122
+ version: preset.version,
123
+ source: preset.source,
124
+ destination: toPosix(destination),
125
+ manifestPath: preset.manifestRelativePath,
126
+ };
127
+ } catch (error) {
128
+ fs.rmSync(tempDestination, { recursive: true, force: true });
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ export function uninstallPresetPackage(id, { scope = "user", targetInput = ".", home = "" } = {}) {
134
+ const normalizedId = normalizePresetId(id);
135
+ if (!normalizedId) throw new Error("Missing preset id");
136
+ const destination = scope === "project" ? projectPresetDestination(normalizedId, targetInput) : userPresetDestination(normalizedId, { home });
137
+ const existed = fs.existsSync(destination);
138
+ if (existed) fs.rmSync(destination, { recursive: true, force: true });
139
+ return { removed: existed, id: normalizedId, destination: toPosix(destination) };
140
+ }
141
+
142
+ export function listBundledPresetIds() {
143
+ if (!fs.existsSync(builtinPresetRoot)) return [];
144
+ return fs.readdirSync(builtinPresetRoot, { withFileTypes: true })
145
+ .filter((entry) => entry.isDirectory())
146
+ .map((entry) => tryNormalizePresetId(entry.name))
147
+ .filter(Boolean)
148
+ .filter((id) => fs.existsSync(path.join(builtinPresetRoot, id, "preset.yaml")))
149
+ .sort();
150
+ }
151
+
152
+ export function seedBundledPresets({ force = false, scope = "user", targetInput = ".", home = "", dryRun = false } = {}) {
153
+ const presets = listBundledPresetIds().map((id) => {
154
+ const sourcePath = path.join(builtinPresetRoot, id);
155
+ const stagedPreset = readPresetPackageFromPath(sourcePath);
156
+ const destination = scope === "project" ? projectPresetDestination(stagedPreset.id, targetInput) : userPresetDestination(stagedPreset.id, { home });
157
+ const existsAlready = fs.existsSync(destination);
158
+ const action = existsAlready ? (force ? (dryRun ? "would-overwrite" : "overwrite") : "skip-existing") : dryRun ? "would-create" : "create";
159
+ if (!dryRun && (!existsAlready || force)) copyPresetPackage(sourcePath, destination, stagedPreset.id);
160
+ return {
161
+ id: stagedPreset.id,
162
+ version: stagedPreset.version,
163
+ source: "builtin",
164
+ destination: toPosix(destination),
165
+ action,
166
+ };
167
+ });
168
+ return {
169
+ operation: "preset-seed",
170
+ scope,
171
+ target: scope === "project" ? toPosix(projectPresetRoot(targetInput)) : toPosix(userPresetRootForHome(home)),
172
+ dryRun,
173
+ force,
174
+ presets,
175
+ created: presets.filter((preset) => ["create", "would-create"].includes(preset.action)).length,
176
+ overwritten: presets.filter((preset) => ["overwrite", "would-overwrite"].includes(preset.action)).length,
177
+ skipped: presets.filter((preset) => preset.action === "skip-existing").length,
178
+ };
179
+ }
180
+
181
+ export function validatePresetPackage(preset) {
182
+ const failures = [];
183
+ const warnings = [];
184
+ if (!preset.id) failures.push("missing id");
185
+ if (!Number.isInteger(preset.version)) failures.push("missing numeric version");
186
+ if (!preset.compatibleBudgets.length) failures.push("missing compatibleBudgets");
187
+ if (!preset.audit.manifestRequired) failures.push("audit.manifestRequired must be true");
188
+ if (!preset.writeScopes.length) failures.push("missing writeScopes");
189
+ for (const [name, input] of Object.entries(preset.inputs)) {
190
+ if (!["text", "flag", "json-file"].includes(input.type)) failures.push(`${name} has unsupported input type: ${input.type || "(missing)"}`);
191
+ if (!input.flag && input.type !== "flag") warnings.push(`${name} input has no CLI flag`);
192
+ }
193
+ if (preset.evidence?.bundleDir && unsafeRelativePresetPath(preset.evidence.bundleDir)) failures.push(`evidence.bundleDir escapes task directory: ${preset.evidence.bundleDir}`);
194
+ if (preset.evidence?.files && (Array.isArray(preset.evidence.files) || typeof preset.evidence.files !== "object")) {
195
+ failures.push("evidence.files must be a mapping");
196
+ }
197
+ for (const [name, evidence] of Object.entries(preset.evidence?.files || {})) {
198
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
199
+ failures.push(`evidence file ${name} must be a mapping`);
200
+ continue;
201
+ }
202
+ if (evidence.path && unsafeRelativePresetPath(evidence.path)) failures.push(`evidence file ${name} path escapes evidence bundle: ${evidence.path}`);
203
+ if (evidence.type && !allowedEvidenceTypes.has(String(evidence.type))) failures.push(`evidence file ${name} has unsupported type: ${evidence.type}`);
204
+ }
205
+ validateAuditEvidenceFiles(preset, failures);
206
+ const resourcePaths = new Set();
207
+ validateResourceCollection(preset, "reference", "references", "references/", resourcePaths, failures);
208
+ validateResourceCollection(preset, "artifact", "artifacts", "artifacts/", resourcePaths, failures);
209
+ const referenceIds = new Set(Object.values(preset.resources?.references || {}).map((resource) => resource.index.id).filter(Boolean));
210
+ for (const requiredRead of preset.context?.requiredReads || []) {
211
+ if (!referenceIds.has(requiredRead)) failures.push(`required read ${requiredRead} does not match a declared reference`);
212
+ }
213
+ for (const [name, entrypoint] of Object.entries(preset.entrypoints)) {
214
+ if (!allowedEntrypoints.has(name)) failures.push(`unsupported entrypoint: ${name}`);
215
+ if (!allowedEntrypointTypes.has(entrypoint.type)) failures.push(`${name} has unsupported type: ${entrypoint.type || "(missing)"}`);
216
+ if (!entrypoint.writes.length) failures.push(`${name} missing write scope manifest`);
217
+ for (const writeScope of entrypoint.writes) {
218
+ if (!preset.writeScopes.some((scope) => scope.path === writeScope)) {
219
+ failures.push(`${name} writes undeclared scope: ${writeScope}`);
220
+ }
221
+ }
222
+ if (["script", "check"].includes(entrypoint.type)) {
223
+ const entryPath = path.join(preset.directory, entrypoint.command || "");
224
+ if (!entrypoint.command) failures.push(`${name} missing command`);
225
+ else if (!isInside(preset.directory, entryPath)) failures.push(`${name} command escapes preset package`);
226
+ else validatePresetPackageFile(preset, entrypoint.command, `${name} command`, failures);
227
+ }
228
+ }
229
+ for (const templatePath of Object.values(preset.newTaskTemplates)) {
230
+ const absolute = path.join(preset.directory, templatePath);
231
+ if (!isInside(preset.directory, absolute)) failures.push(`template escapes preset package: ${templatePath}`);
232
+ else validatePresetPackageFile(preset, templatePath, "template", failures);
233
+ }
234
+ return { failures, warnings };
235
+ }
236
+
237
+ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [] } = {}) {
238
+ const entrypoints = {
239
+ [entrypoint]: preset.entrypoints[entrypoint],
240
+ };
241
+ const scopes = writeScopes.length ? writeScopes : preset.entrypoints[entrypoint]?.writes || preset.writeScopes.map((scope) => scope.path);
242
+ return {
243
+ preset: preset.id,
244
+ version: preset.version,
245
+ manifestPath: preset.manifestRelativePath,
246
+ manifestSha256: preset.manifestSha256,
247
+ entrypoints,
248
+ writeScopes: scopes,
249
+ taskId,
250
+ targetRoot,
251
+ generatedAt: new Date().toISOString(),
252
+ };
253
+ }
254
+
255
+ export function renderPresetTemplate(preset, templatePath, values) {
256
+ if (!templatePath) return "";
257
+ const absolute = path.join(preset.directory, templatePath);
258
+ if (!isInside(preset.directory, absolute)) throw new Error(`Preset template escapes package: ${templatePath}`);
259
+ const content = fs.readFileSync(absolute, "utf8");
260
+ return content.replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_match, key) => {
261
+ const value = getValue(values, key);
262
+ return value == null ? "" : String(value);
263
+ });
264
+ }
265
+
266
+ function normalizePresetId(id) {
267
+ const normalized = String(id || "").trim().toLowerCase().replaceAll("_", "-");
268
+ if (!normalized) return "";
269
+ if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(normalized)) {
270
+ throw new Error(`Invalid preset id: ${id}. Use lowercase letters, numbers, and hyphens only.`);
271
+ }
272
+ return normalized;
273
+ }
274
+
275
+ function tryNormalizePresetId(id) {
276
+ try {
277
+ return normalizePresetId(id);
278
+ } catch {
279
+ return "";
280
+ }
281
+ }
282
+
283
+ function userPresetDestination(id, { home = "" } = {}) {
284
+ const root = home ? userPresetRootForHome(home) : userPresetRoot;
285
+ const destination = path.resolve(root, normalizePresetId(id));
286
+ if (!isInside(path.resolve(root), destination) || destination === path.resolve(root)) {
287
+ throw new Error(`Preset destination escapes user preset root: ${id}`);
288
+ }
289
+ return destination;
290
+ }
291
+
292
+ function projectPresetDestination(id, targetInput) {
293
+ const root = path.resolve(projectPresetRoot(targetInput));
294
+ const destination = path.resolve(root, normalizePresetId(id));
295
+ if (!isInside(root, destination) || destination === root) {
296
+ throw new Error(`Preset destination escapes project preset root: ${id}`);
297
+ }
298
+ return destination;
299
+ }
300
+
301
+ function normalizePresetManifest(manifest, { id, manifestPath, raw, source }) {
302
+ const directory = path.dirname(manifestPath);
303
+ const entrypoints = normalizeEntryPoints(manifest.entrypoints || {});
304
+ const writeScopes = Object.entries(manifest.writeScopes || {}).map(([name, value]) => ({
305
+ name,
306
+ path: String(value.path || value || "").trim(),
307
+ access: String(value.access || "write").trim(),
308
+ })).filter((scope) => scope.path);
309
+ return {
310
+ id: normalizePresetId(manifest.id || id),
311
+ version: Number.parseInt(manifest.version, 10),
312
+ purpose: String(manifest.purpose || ""),
313
+ compatibleBudgets: asArray(manifest.compatibleBudgets),
314
+ localeSupport: asArray(manifest.localeSupport),
315
+ task: manifest.task || {},
316
+ inputs: normalizeInputs(manifest.inputs || {}),
317
+ templateValues: normalizeTemplateValues(manifest.templateValues || {}),
318
+ metadata: normalizeTemplateValues(manifest.metadata || {}),
319
+ resources: normalizeResources(manifest.resources || {}),
320
+ context: normalizeContext(manifest.context || {}),
321
+ entrypoints,
322
+ workbench: manifest.workbench || {},
323
+ evidence: manifest.evidence || {},
324
+ review: manifest.review || {},
325
+ audit: {
326
+ manifestRequired: asBoolean(manifest.audit?.manifestRequired),
327
+ evidenceFiles: asArray(manifest.audit?.evidenceFiles),
328
+ },
329
+ writeScopes,
330
+ newTaskTemplates: manifest.entrypoints?.newTask?.templates || {},
331
+ directory,
332
+ source,
333
+ manifestPath,
334
+ manifestRelativePath: displayManifestPath(manifestPath),
335
+ manifestSha256: crypto.createHash("sha256").update(raw).digest("hex"),
336
+ };
337
+ }
338
+
339
+ function normalizeInputs(rawInputs) {
340
+ return Object.fromEntries(Object.entries(rawInputs || {}).map(([name, value]) => [name, {
341
+ type: String(value.type || "text").trim(),
342
+ flag: String(value.flag || "").trim(),
343
+ required: asBoolean(value.required),
344
+ default: value.default,
345
+ validateOperation: String(value.validateOperation || "").trim(),
346
+ rejectPlanOnly: asBoolean(value.rejectPlanOnly),
347
+ requireTarget: asBoolean(value.requireTarget),
348
+ targetFromSession: asBoolean(value.targetFromSession),
349
+ }]));
350
+ }
351
+
352
+ function normalizeTemplateValues(rawValues) {
353
+ return Object.fromEntries(Object.entries(rawValues || {}).map(([name, value]) => [name, typeof value === "object" && value !== null ? value : { value }]));
354
+ }
355
+
356
+ function normalizeResources(rawResources) {
357
+ return {
358
+ references: normalizeResourceGroup(rawResources.references || {}),
359
+ artifacts: normalizeResourceGroup(rawResources.artifacts || {}),
360
+ };
361
+ }
362
+
363
+ function normalizeResourceGroup(rawGroup) {
364
+ return Object.fromEntries(Object.entries(rawGroup || {}).map(([name, value]) => [name, {
365
+ name,
366
+ path: String(value.path || "").trim(),
367
+ source: String(value.source || "").trim(),
368
+ template: String(value.template || "").trim(),
369
+ index: {
370
+ id: String(value.index?.id || "").trim(),
371
+ type: String(value.index?.type || "").trim(),
372
+ summary: String(value.index?.summary || "").trim(),
373
+ usedBy: String(value.index?.usedBy || "").trim(),
374
+ producedBy: String(value.index?.producedBy || "").trim(),
375
+ },
376
+ }]));
377
+ }
378
+
379
+ function normalizeContext(rawContext) {
380
+ return {
381
+ requiredReads: asArray(rawContext.requiredReads),
382
+ };
383
+ }
384
+
385
+ function normalizeEntryPoints(rawEntryPoints) {
386
+ const result = {};
387
+ for (const [name, value] of Object.entries(rawEntryPoints || {})) {
388
+ result[name] = {
389
+ type: String(value.type || "").trim(),
390
+ command: value.command ? String(value.command).trim() : "",
391
+ templates: value.templates || {},
392
+ writes: asArray(value.writes),
393
+ reads: asArray(value.reads),
394
+ audit: asBoolean(value.audit),
395
+ };
396
+ }
397
+ return result;
398
+ }
399
+
400
+ function publicPresetShape(preset) {
401
+ return {
402
+ id: preset.id,
403
+ version: preset.version,
404
+ purpose: preset.purpose,
405
+ compatibleBudgets: preset.compatibleBudgets,
406
+ localeSupport: preset.localeSupport,
407
+ task: preset.task,
408
+ entrypoints: preset.entrypoints,
409
+ workbench: preset.workbench,
410
+ evidence: preset.evidence,
411
+ review: preset.review,
412
+ audit: preset.audit,
413
+ writeScopes: preset.writeScopes,
414
+ inputs: preset.inputs,
415
+ templateValues: preset.templateValues,
416
+ metadata: preset.metadata,
417
+ resources: preset.resources,
418
+ context: preset.context,
419
+ source: preset.source,
420
+ manifestPath: preset.manifestRelativePath,
421
+ manifestSha256: preset.manifestSha256,
422
+ };
423
+ }
424
+
425
+ function validateResourceCollection(preset, label, groupName, requiredPrefix, resourcePaths, failures) {
426
+ const seen = new Set();
427
+ for (const [name, resource] of Object.entries(preset.resources?.[groupName] || {})) {
428
+ const normalizedPath = toPosix(path.normalize(resource.path || ""));
429
+ if (!resource.path) failures.push(`${label} resource ${name} missing path`);
430
+ else if (hasMarkdownTableDelimiter(resource.path)) failures.push(`${label} resource ${name} path cannot contain Markdown table delimiters: ${resource.path}`);
431
+ else if (unsafeRelativePresetPath(resource.path)) failures.push(`resource ${name} path escapes task directory: ${resource.path}`);
432
+ else if (String(resource.path).endsWith("/") || String(resource.path).endsWith("\\") || normalizedPath.endsWith("/")) {
433
+ failures.push(`${label} resource ${name} path must be a file under ${requiredPrefix}: ${resource.path}`);
434
+ }
435
+ else if (!normalizedPath.startsWith(requiredPrefix) || normalizedPath === requiredPrefix.slice(0, -1) || normalizedPath === `${requiredPrefix}INDEX.md`) {
436
+ failures.push(`${label} resource ${name} path must be under ${requiredPrefix}: ${resource.path}`);
437
+ } else if (resourcePaths.has(normalizedPath)) {
438
+ failures.push(`duplicate resource path: ${normalizedPath}`);
439
+ } else {
440
+ resourcePaths.add(normalizedPath);
441
+ }
442
+ if (!resource.source && !resource.template) failures.push(`${label} resource ${name} missing source or template`);
443
+ if (resource.source && resource.template) failures.push(`${label} resource ${name} cannot declare both source and template`);
444
+ for (const field of ["source", "template"]) {
445
+ if (!resource[field]) continue;
446
+ const resourcePath = path.join(preset.directory, resource[field]);
447
+ if (!isInside(preset.directory, resourcePath)) failures.push(`${label} resource ${name} ${field} escapes preset package`);
448
+ else validatePresetPackageFile(preset, resource[field], `${label} resource ${name} ${field}`, failures);
449
+ }
450
+ const id = resource.index?.id || "";
451
+ if (!id) failures.push(`${label} resource ${name} missing index.id`);
452
+ if (id && hasMarkdownTableDelimiter(id)) failures.push(`${label} resource ${name} index.id cannot contain Markdown table delimiters: ${id}`);
453
+ if (id && seen.has(id)) failures.push(`duplicate ${label} resource id: ${id}`);
454
+ if (id) seen.add(id);
455
+ }
456
+ }
457
+
458
+ function validateAuditEvidenceFiles(preset, failures) {
459
+ const seen = new Set();
460
+ for (const name of preset.audit?.evidenceFiles || []) {
461
+ const raw = String(name || "").trim();
462
+ const normalized = toPosix(path.normalize(raw));
463
+ if (!raw) failures.push("audit evidence file name is empty");
464
+ else if (hasMarkdownTableDelimiter(raw)) failures.push(`audit evidence file cannot contain Markdown table delimiters: ${raw}`);
465
+ else if (unsafeRelativePresetPath(raw) || raw.includes("/") || raw.includes("\\") || normalized !== path.basename(normalized)) {
466
+ failures.push(`audit evidence file must be a basename within evidence bundle: ${raw}`);
467
+ } else if (seen.has(normalized)) {
468
+ failures.push(`duplicate audit evidence file: ${normalized}`);
469
+ } else {
470
+ seen.add(normalized);
471
+ }
472
+ }
473
+ }
474
+
475
+ function validatePresetPackageFile(preset, relativePath, label, failures) {
476
+ const filePath = path.join(preset.directory, relativePath || "");
477
+ if (!isInside(preset.directory, filePath)) {
478
+ failures.push(`${label} escapes preset package`);
479
+ return;
480
+ }
481
+ if (!fs.existsSync(filePath)) {
482
+ failures.push(`${label} missing: ${relativePath}`);
483
+ return;
484
+ }
485
+ const stat = fs.lstatSync(filePath);
486
+ if (stat.isSymbolicLink()) {
487
+ failures.push(`${label} must not be a symlink: ${relativePath}`);
488
+ return;
489
+ }
490
+ if (!stat.isFile()) {
491
+ failures.push(`${label} must be a file: ${relativePath}`);
492
+ return;
493
+ }
494
+ const realRoot = fs.realpathSync(preset.directory);
495
+ const realPath = fs.realpathSync(filePath);
496
+ if (!isInside(realRoot, realPath)) failures.push(`${label} real path escapes preset package: ${relativePath}`);
497
+ }
498
+
499
+ export function parseSimpleYaml(source) {
500
+ const root = {};
501
+ const stack = [{ indent: -1, object: root }];
502
+ for (const rawLine of String(source).split(/\r?\n/)) {
503
+ if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue;
504
+ const indent = rawLine.match(/^\s*/)[0].length;
505
+ const line = rawLine.trim();
506
+ const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
507
+ if (!match) throw new Error(`Unsupported preset YAML line: ${rawLine}`);
508
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
509
+ const parent = stack[stack.length - 1].object;
510
+ const key = match[1];
511
+ const rawValue = match[2] || "";
512
+ if (!rawValue) {
513
+ parent[key] = {};
514
+ stack.push({ indent, object: parent[key] });
515
+ } else {
516
+ parent[key] = parseYamlScalar(rawValue);
517
+ }
518
+ }
519
+ return root;
520
+ }
521
+
522
+ function parseYamlScalar(rawValue) {
523
+ const value = String(rawValue || "").trim();
524
+ if (value === "true") return true;
525
+ if (value === "false") return false;
526
+ if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
527
+ if (value.startsWith("[") && value.endsWith("]")) {
528
+ const body = value.slice(1, -1).trim();
529
+ if (!body) return [];
530
+ return body.split(",").map((item) => item.trim().replace(/^['"]|['"]$/g, ""));
531
+ }
532
+ return value.replace(/^['"]|['"]$/g, "");
533
+ }
534
+
535
+ function asArray(value) {
536
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
537
+ if (!value) return [];
538
+ return [String(value).trim()].filter(Boolean);
539
+ }
540
+
541
+ function asBoolean(value) {
542
+ return value === true || String(value || "").toLowerCase() === "true";
543
+ }
544
+
545
+ function isInside(root, candidate) {
546
+ const relative = path.relative(root, candidate);
547
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
548
+ }
549
+
550
+ function unsafeRelativePresetPath(value) {
551
+ const raw = String(value || "");
552
+ const normalized = toPosix(path.normalize(raw));
553
+ return path.isAbsolute(raw) || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../");
554
+ }
555
+
556
+ function hasMarkdownTableDelimiter(value) {
557
+ return /[|\r\n]/.test(String(value || ""));
558
+ }
559
+
560
+ function getValue(values, key) {
561
+ return String(key).split(".").reduce((cursor, part) => (cursor && Object.prototype.hasOwnProperty.call(cursor, part) ? cursor[part] : undefined), values);
562
+ }
563
+
564
+ function displayManifestPath(manifestPath) {
565
+ const relative = path.relative(repoRoot, manifestPath);
566
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) return toPosix(relative);
567
+ return toPosix(manifestPath);
568
+ }
569
+
570
+ function findPresetManifest(id, { targetInput = "", home = "" } = {}) {
571
+ const candidates = presetSearchRoots({ targetInput, home }).map(({ source, root }) => ({ source, manifestPath: path.join(root, id, "preset.yaml") }));
572
+ return candidates.find((candidate) => fs.existsSync(candidate.manifestPath)) || null;
573
+ }
574
+
575
+ function listPresetIds({ targetInput = "", home = "" } = {}) {
576
+ const ids = new Set();
577
+ for (const { root } of presetSearchRoots({ targetInput, home })) {
578
+ if (!fs.existsSync(root)) continue;
579
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
580
+ if (entry.isDirectory()) ids.add(entry.name);
581
+ }
582
+ }
583
+ return [...ids].sort();
584
+ }
585
+
586
+ function presetSearchRoots({ targetInput = "", home = "" } = {}) {
587
+ const roots = [];
588
+ if (targetInput) roots.push({ source: "project", root: projectPresetRoot(targetInput) });
589
+ roots.push({ source: "user", root: home ? userPresetRootForHome(home) : userPresetRoot });
590
+ roots.push({ source: "builtin", root: builtinPresetRoot });
591
+ return roots;
592
+ }
593
+
594
+ function resolveInstallSource(source) {
595
+ const localPath = path.resolve(source);
596
+ if (fs.existsSync(path.join(localPath, "preset.yaml"))) return localPath;
597
+ const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
598
+ if (fs.existsSync(path.join(builtinPath, "preset.yaml"))) return builtinPath;
599
+ throw new Error(`Preset source not found: ${source}`);
600
+ }
601
+
602
+ function copyDirectory(source, destination) {
603
+ fs.mkdirSync(destination, { recursive: true });
604
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
605
+ const sourcePath = path.join(source, entry.name);
606
+ const destinationPath = path.join(destination, entry.name);
607
+ if (entry.isDirectory()) copyDirectory(sourcePath, destinationPath);
608
+ else if (entry.isFile()) fs.copyFileSync(sourcePath, destinationPath);
609
+ }
610
+ }
611
+
612
+ function copyPresetPackage(sourcePath, destination, id) {
613
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
614
+ const tempDestination = path.join(path.dirname(destination), `.${id}.install-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
615
+ fs.rmSync(tempDestination, { recursive: true, force: true });
616
+ copyDirectory(sourcePath, tempDestination);
617
+ try {
618
+ const tempPreset = readPresetPackageFromPath(tempDestination);
619
+ const tempReport = validatePresetPackage(tempPreset);
620
+ if (tempReport.failures.length) throw new Error(`Invalid preset package ${id}: ${tempReport.failures.join("; ")}`);
621
+ fs.rmSync(destination, { recursive: true, force: true });
622
+ fs.renameSync(tempDestination, destination);
623
+ } catch (error) {
624
+ fs.rmSync(tempDestination, { recursive: true, force: true });
625
+ throw error;
626
+ }
627
+ }