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.
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +8 -4
- package/README.md +12 -2
- package/README.zh-CN.md +10 -2
- package/SKILL.md +14 -3
- package/dist/build-dist.mjs +19 -6
- package/dist/check-dist-observation.mjs +57 -29
- package/dist/check-harness.mjs +0 -1
- package/dist/check-import-graph.mjs +44 -27
- package/dist/check-lite-forbidden-surfaces.mjs +121 -0
- package/dist/check-no-ts-nocheck.mjs +7 -7
- package/dist/check-runtime-emit.mjs +10 -3
- package/dist/check-type-boundaries.mjs +51 -9
- package/dist/commands/dashboard-command.mjs +52 -14
- package/dist/commands/migration-command.mjs +18 -8
- package/dist/commands/module-command.mjs +142 -0
- package/dist/commands/preset-command.mjs +51 -12
- package/dist/commands/registry.mjs +483 -0
- package/dist/commands/task-command.mjs +109 -52
- package/dist/harness.mjs +6 -304
- package/dist/lib/capability-registry.mjs +229 -53
- package/dist/lib/check-module-parallel.mjs +1 -6
- package/dist/lib/check-profiles.mjs +39 -46
- package/dist/lib/check-task-contracts.mjs +6 -4
- package/dist/lib/command-registry.mjs +248 -0
- package/dist/lib/core-shared.mjs +78 -3
- package/dist/lib/dashboard-data.mjs +203 -22
- package/dist/lib/dashboard-workbench.mjs +245 -21
- package/dist/lib/dashboard-writer.mjs +4 -1
- package/dist/lib/git-status-summary.mjs +0 -1
- package/dist/lib/governance-index-generator.mjs +7 -5
- package/dist/lib/governance-sync.mjs +46 -121
- package/dist/lib/governance-table-boundary.mjs +1 -14
- package/dist/lib/harness-core.mjs +4 -1
- package/dist/lib/harness-paths.mjs +115 -1
- package/dist/lib/impact-classifier.mjs +420 -0
- package/dist/lib/lesson-maintenance.mjs +1 -2
- package/dist/lib/markdown-utils.mjs +50 -1
- package/dist/lib/migration-planner.mjs +31 -16
- package/dist/lib/migration-support.mjs +5 -4
- package/dist/lib/module-registry.mjs +296 -0
- package/dist/lib/preset-audit-contracts.mjs +24 -1
- package/dist/lib/preset-engine.mjs +67 -29
- package/dist/lib/preset-registry.mjs +361 -71
- package/dist/lib/preset-runner.mjs +292 -26
- package/dist/lib/review-confirm-git-gate.mjs +73 -19
- package/dist/lib/status-builder.mjs +23 -8
- package/dist/lib/structure-migration.mjs +6 -4
- package/dist/lib/subagent-authorization-audit.mjs +8 -2
- package/dist/lib/task-archive-eligibility.mjs +65 -0
- package/dist/lib/task-audit-metadata.mjs +25 -11
- package/dist/lib/task-audit-migration.mjs +21 -14
- package/dist/lib/task-discovery-contract.mjs +32 -0
- package/dist/lib/task-index.mjs +3 -2
- package/dist/lib/task-lesson-candidates.mjs +1 -2
- package/dist/lib/task-lesson-sedimentation.mjs +310 -9
- package/dist/lib/task-lifecycle/create-task-helpers.mjs +6 -3
- package/dist/lib/task-lifecycle/phase-sync.mjs +0 -1
- package/dist/lib/task-lifecycle/preset-interop.mjs +16 -0
- package/dist/lib/task-lifecycle/review-confirm.mjs +34 -2
- package/dist/lib/task-lifecycle/review-gates.mjs +12 -5
- package/dist/lib/task-lifecycle/review-submission.mjs +1 -2
- package/dist/lib/task-lifecycle/scaffold-provenance.mjs +0 -1
- package/dist/lib/task-lifecycle/template-files.mjs +2 -5
- package/dist/lib/task-lifecycle.mjs +116 -160
- package/dist/lib/task-metadata.mjs +10 -5
- package/dist/lib/task-preset-contract-drift.mjs +45 -0
- package/dist/lib/task-repository.mjs +192 -0
- package/dist/lib/task-review-model.mjs +36 -17
- package/dist/lib/task-scanner.mjs +74 -23
- package/dist/lib/task-template-materials.mjs +131 -0
- package/dist/lib/task-tombstone-commands.mjs +186 -29
- package/dist/lib/types/check-profiles.js +1 -0
- package/dist/lib/types/impact.js +1 -0
- package/dist/lib/types/preset.js +1 -0
- package/dist/lib/types/task-lifecycle.js +1 -0
- package/dist/lib/types/task-scanner.js +1 -0
- package/dist/postinstall.mjs +2 -2
- package/dist/run-built-tests.mjs +10 -3
- package/docs-release/README.md +1 -0
- package/docs-release/architecture/document-contract-kernel/README.md +150 -0
- package/docs-release/architecture/document-contract-kernel/products/full-skill-overlay.md +29 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-forbidden-surfaces.txt +26 -0
- package/docs-release/architecture/document-contract-kernel/products/lite-skill-overlay.md +37 -0
- package/docs-release/architecture/overview.md +2 -2
- package/docs-release/architecture/overview.zh-CN.md +2 -2
- package/docs-release/architecture/system-explainer/01-system-overview.md +11 -7
- package/docs-release/architecture/system-explainer/02-module-dependency.md +4 -4
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +17 -12
- package/docs-release/architecture/system-explainer/05-data-flow.md +6 -6
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/README.md +1 -1
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +12 -8
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +19 -11
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +5 -5
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +2 -2
- package/docs-release/architecture/system-explainer/en-US/README.md +1 -1
- package/docs-release/guides/agent-installation.en-US.md +4 -6
- package/docs-release/guides/agent-installation.md +11 -8
- package/docs-release/guides/contributing.md +10 -3
- package/docs-release/guides/contributing.zh-CN.md +10 -3
- package/docs-release/guides/legacy-migration-agent-prompt.md +1 -1
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +1 -1
- package/docs-release/guides/migration-playbook.en-US.md +9 -6
- package/docs-release/guides/migration-playbook.md +9 -6
- package/docs-release/guides/preset-development.md +68 -2
- package/docs-release/guides/task-state-machine.en-US.md +8 -8
- package/docs-release/guides/task-state-machine.md +7 -7
- package/docs-release/guides/typescript-runtime-migration-closeout.md +17 -13
- package/package.json +16 -12
- package/postinstall.mjs +37 -0
- package/presets/legacy-migration/preset.yaml +5 -5
- package/presets/legacy-migration/templates/execution_strategy.append.md +1 -1
- package/presets/lesson-sedimentation/preset.yaml +3 -3
- package/presets/module/preset.yaml +2 -2
- package/presets/module/templates/execution_strategy.append.md +1 -1
- package/presets/module/templates/task_plan.append.md +3 -3
- package/presets/release-closeout/checks/check-release-package.mjs +6 -1
- package/presets/release-closeout/preset.yaml +9 -9
- package/presets/release-closeout/scripts/generate-release-package.mjs +387 -25
- package/presets/release-closeout/templates/task_plan.append.md +5 -5
- package/presets/standard-task/preset.yaml +2 -2
- package/references/agents-md-pattern.md +23 -17
- package/references/lessons-governance.md +2 -2
- package/references/module-parallel-standard.md +3 -6
- package/references/ssot-governance.md +2 -2
- package/references/taskr-gap-analysis.md +3 -3
- package/run-dist.mjs +34 -0
- package/skills/preset-creator/SKILL.md +40 -8
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +32 -8
- package/skills/preset-creator/references/preset-package-skeleton.md +15 -5
- package/skills/preset-creator/references/structure-aware-paths.md +112 -0
- package/templates/AGENTS.md.template +28 -26
- package/templates/architecture/README.md +2 -2
- package/templates/architecture/service-catalog.md +2 -2
- package/templates/architecture/services/service-template.md +1 -1
- package/templates/dashboard/assets/app-src/00-state.js +5 -1
- package/templates/dashboard/assets/app-src/10-router.js +7 -0
- package/templates/dashboard/assets/app-src/20-overview.js +8 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +132 -40
- package/templates/dashboard/assets/app-src/32-task-swimlane.js +314 -0
- package/templates/dashboard/assets/app-src/35-task-detail.js +35 -5
- package/templates/dashboard/assets/app-src/40-modules.js +257 -41
- package/templates/dashboard/assets/app-src/45-review.js +127 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +185 -2
- package/templates/dashboard/assets/app.css +928 -53
- package/templates/dashboard/assets/app.css.manifest.json +2 -0
- package/templates/dashboard/assets/app.js +1071 -98
- package/templates/dashboard/assets/app.manifest.json +1 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +12 -6
- package/templates/dashboard/assets/css-src/10-panels-flow.css +2 -2
- package/templates/dashboard/assets/css-src/30-task-index.css +21 -13
- package/templates/dashboard/assets/css-src/31-archive.css +94 -0
- package/templates/dashboard/assets/css-src/32-task-swimlane.css +487 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +78 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +191 -14
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +23 -0
- package/templates/dashboard/assets/i18n.js +166 -2
- package/templates/development/README.md +9 -9
- package/templates/development/cross-repo-debugging.md +3 -3
- package/templates/development/external-context/service-template.md +1 -1
- package/templates/development/external-source-packs/README.md +2 -2
- package/templates/integrations/README.md +4 -4
- package/templates/integrations/api-contract.md +1 -1
- package/templates/integrations/event-contract.md +1 -1
- package/templates/integrations/third-party/vendor-template.md +1 -1
- package/templates/integrations/webhook-contract.md +1 -1
- package/templates/ledger/Harness-Ledger.md +1 -1
- package/templates/modules/module_brief.md +50 -0
- package/templates/modules/module_plan.md +49 -0
- package/templates/modules/registry_view.md +9 -0
- package/templates/modules/session_prompt_pack.md +55 -0
- package/templates/planning/brief.md +32 -8
- package/templates/planning/module_brief.md +28 -3
- package/templates/planning/module_plan.md +26 -11
- package/templates/planning/module_session_prompt.md +11 -2
- package/templates/planning/optional/slices/_slice-template/brief.md +28 -0
- package/templates/planning/review.md +1 -1
- package/templates/planning/visual_map.md +1 -1
- package/templates/reference/docs-library-standard.md +7 -7
- package/templates/reference/execution-workflow-standard.md +13 -0
- package/templates/reference/external-source-intake-standard.md +10 -10
- package/templates/reference/repo-governance-standard.md +1 -1
- package/templates/reference/review-routing-standard.md +4 -0
- package/templates/ssot/Module-Registry.md +4 -38
- package/templates/walkthrough/walkthrough-template.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +27 -25
- package/templates-zh-CN/CLAUDE.md.template +1 -1
- package/templates-zh-CN/architecture/README.md +2 -2
- package/templates-zh-CN/architecture/service-catalog.md +2 -2
- package/templates-zh-CN/architecture/services/service-template.md +1 -1
- package/templates-zh-CN/development/README.md +9 -9
- package/templates-zh-CN/development/cross-repo-debugging.md +3 -3
- package/templates-zh-CN/development/external-context/service-template.md +1 -1
- package/templates-zh-CN/development/external-source-packs/README.md +2 -2
- package/templates-zh-CN/integrations/README.md +4 -4
- package/templates-zh-CN/integrations/api-contract.md +1 -1
- package/templates-zh-CN/integrations/event-contract.md +1 -1
- package/templates-zh-CN/integrations/third-party/vendor-template.md +1 -1
- package/templates-zh-CN/integrations/webhook-contract.md +1 -1
- package/templates-zh-CN/ledger/Harness-Ledger.md +1 -1
- package/templates-zh-CN/lessons/lesson-arch-process-change.md +1 -1
- package/templates-zh-CN/lessons/lesson-new-doc.md +3 -3
- package/templates-zh-CN/lessons/lesson-ref-change.md +4 -4
- package/templates-zh-CN/modules/module_brief.md +47 -0
- package/templates-zh-CN/modules/module_plan.md +48 -0
- package/templates-zh-CN/modules/registry_view.md +9 -0
- package/templates-zh-CN/modules/session_prompt_pack.md +50 -0
- package/templates-zh-CN/planning/INDEX.md +1 -0
- package/templates-zh-CN/planning/brief.md +26 -7
- package/templates-zh-CN/planning/module_brief.md +24 -2
- package/templates-zh-CN/planning/module_plan.md +35 -29
- package/templates-zh-CN/planning/module_session_prompt.md +15 -11
- package/templates-zh-CN/planning/optional/slices/_slice-template/brief.md +28 -11
- package/templates-zh-CN/planning/review.md +1 -1
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/delivery-operating-model-standard.md +3 -3
- package/templates-zh-CN/reference/docs-library-standard.md +27 -27
- package/templates-zh-CN/reference/execution-workflow-standard.md +12 -2
- package/templates-zh-CN/reference/external-source-intake-standard.md +10 -10
- package/templates-zh-CN/reference/harness-ledger-standard.md +3 -3
- package/templates-zh-CN/reference/regression-ssot-governance.md +2 -2
- package/templates-zh-CN/reference/repo-governance-standard.md +1 -1
- package/templates-zh-CN/reference/review-routing-standard.md +3 -0
- package/templates-zh-CN/reference/walkthrough-standard.md +2 -2
- package/templates-zh-CN/reference/worktree-standard.md +1 -1
- package/templates-zh-CN/regression/Cadence-Ledger.md +2 -2
- package/templates-zh-CN/ssot/Delivery-SSoT.md +2 -2
- package/templates-zh-CN/ssot/Module-Registry.md +5 -44
- package/templates-zh-CN/ssot/Regression-SSoT.md +2 -2
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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(
|
|
361
|
-
version: Number.parseInt(
|
|
362
|
-
purpose: String(
|
|
363
|
-
compatibleBudgets: asArray(
|
|
364
|
-
localeSupport: asArray(
|
|
365
|
-
task
|
|
366
|
-
inputs: normalizeInputs(
|
|
367
|
-
templateValues: normalizeTemplateValues(
|
|
368
|
-
metadata: normalizeTemplateValues(
|
|
369
|
-
resources: normalizeResources(
|
|
370
|
-
context: normalizeContext(
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
532
|
+
actions,
|
|
533
|
+
workbench: asRecord(manifestRecord.workbench),
|
|
534
|
+
evidence: asEvidence(asRecord(manifestRecord.evidence)),
|
|
535
|
+
review: asRecord(manifestRecord.review),
|
|
375
536
|
audit: {
|
|
376
|
-
manifestRequired: asBoolean(
|
|
377
|
-
evidenceFiles: asArray(
|
|
537
|
+
manifestRequired: asBoolean(asRecord(manifestRecord.audit).manifestRequired),
|
|
538
|
+
evidenceFiles: asArray(asRecord(manifestRecord.audit).evidenceFiles),
|
|
378
539
|
},
|
|
379
540
|
writeScopes,
|
|
380
|
-
newTaskTemplates:
|
|
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
|
|
417
|
-
type: String(value.index
|
|
418
|
-
summary: String(value.index
|
|
419
|
-
usedBy: String(value.index
|
|
420
|
-
producedBy: String(value.index
|
|
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(
|
|
434
|
-
command:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
731
|
+
const declaredPath = resource[field];
|
|
732
|
+
if (!declaredPath)
|
|
495
733
|
continue;
|
|
496
|
-
const resourcePath = path.join(preset.directory,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
}
|