coding-agent-harness 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -5
- package/README.md +12 -2
- package/README.zh-CN.md +10 -2
- package/SKILL.md +14 -3
- package/dist/build-dist.mjs +32 -6
- package/dist/check-dist-observation.mjs +73 -28
- 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 +88 -0
- package/dist/check-runtime-emit.mjs +10 -3
- package/dist/check-type-boundaries.mjs +67 -8
- 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 +65 -4
- package/dist/commands/registry.mjs +483 -0
- package/dist/commands/task-command.mjs +111 -53
- package/dist/harness.mjs +6 -303
- 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 +5 -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 +68 -29
- package/dist/lib/preset-registry.mjs +374 -72
- package/dist/lib/preset-runner.mjs +560 -0
- 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 +4 -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 +117 -159
- 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 +38 -17
- package/dist/lib/task-scanner.mjs +75 -23
- package/dist/lib/task-template-materials.mjs +131 -0
- package/dist/lib/task-tombstone-commands.mjs +187 -18
- 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 +2 -1
- 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 +19 -11
- 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 +29 -0
- package/presets/release-closeout/preset.yaml +100 -0
- package/presets/release-closeout/scripts/generate-release-package.mjs +572 -0
- package/presets/release-closeout/templates/execution_strategy.append.md +7 -0
- package/presets/release-closeout/templates/findings.seed.md +5 -0
- package/presets/release-closeout/templates/review.seed.md +3 -0
- package/presets/release-closeout/templates/task_plan.append.md +24 -0
- 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/pull-request-standard.md +2 -2
- 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/pull-request-standard.md +2 -2
- 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/pull-request-standard.md +1 -1
- 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,9 +313,13 @@ export function validatePresetPackage(preset) {
|
|
|
255
313
|
if (!entrypoint.writes.length)
|
|
256
314
|
failures.push(`${name} missing write scope manifest`);
|
|
257
315
|
for (const writeScope of entrypoint.writes) {
|
|
316
|
+
failures.push(...validateHarnessPathTemplateTokens(writeScope, `${name} write scope`));
|
|
258
317
|
if (!preset.writeScopes.some((scope) => scope.path === writeScope)) {
|
|
259
318
|
failures.push(`${name} writes undeclared scope: ${writeScope}`);
|
|
260
319
|
}
|
|
320
|
+
if (name === "newTask" && !newTaskWriteScopeAllowed(writeScope)) {
|
|
321
|
+
failures.push("newTask entrypoint writes must stay under coding-agent-harness/planning/**");
|
|
322
|
+
}
|
|
261
323
|
}
|
|
262
324
|
if (["script", "check"].includes(entrypoint.type)) {
|
|
263
325
|
const entryPath = path.join(preset.directory, entrypoint.command || "");
|
|
@@ -265,10 +327,56 @@ export function validatePresetPackage(preset) {
|
|
|
265
327
|
failures.push(`${name} missing command`);
|
|
266
328
|
else if (!isInside(preset.directory, entryPath))
|
|
267
329
|
failures.push(`${name} command escapes preset package`);
|
|
268
|
-
else
|
|
330
|
+
else {
|
|
269
331
|
validatePresetPackageFile(preset, entrypoint.command, `${name} command`, failures);
|
|
332
|
+
warnOnRuntimePathLiterals(entryPath, `${name} command`, warnings);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const readScope of entrypoint.reads || [])
|
|
336
|
+
failures.push(...validateHarnessPathTemplateTokens(readScope, `${name} read scope`));
|
|
337
|
+
}
|
|
338
|
+
for (const [name, action] of Object.entries(preset.actions || {})) {
|
|
339
|
+
if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(name))
|
|
340
|
+
failures.push(`unsupported action id: ${name}`);
|
|
341
|
+
if (!allowedActionTypes.has(action.type))
|
|
342
|
+
failures.push(`${name} action has unsupported type: ${action.type || "(missing)"}`);
|
|
343
|
+
if (action.taskRequired !== true)
|
|
344
|
+
failures.push(`${name} action must set taskRequired: true`);
|
|
345
|
+
if (!action.writes.length)
|
|
346
|
+
failures.push(`${name} action missing write scope manifest`);
|
|
347
|
+
for (const [inputName, input] of Object.entries(action.inputs || {})) {
|
|
348
|
+
if (!["text", "flag", "json-file"].includes(input.type))
|
|
349
|
+
failures.push(`${name}.${inputName} has unsupported input type: ${input.type || "(missing)"}`);
|
|
350
|
+
if (!input.flag)
|
|
351
|
+
failures.push(`${name}.${inputName} input missing CLI flag`);
|
|
352
|
+
else if (!input.flag.startsWith("--"))
|
|
353
|
+
failures.push(`${name}.${inputName} input flag must start with --`);
|
|
354
|
+
}
|
|
355
|
+
for (const writeScope of action.writes) {
|
|
356
|
+
failures.push(...validateActionPathTemplateTokens(writeScope, `${name} action write scope`));
|
|
357
|
+
if (isBroadTaskRootScope(writeScope))
|
|
358
|
+
failures.push(`${name} action writes must be task-local; avoid broad task-root scope: ${writeScope}`);
|
|
359
|
+
if (!usesTaskLocalPath(writeScope))
|
|
360
|
+
warnings.push(`${name} action write scope is not task-local: ${writeScope}`);
|
|
361
|
+
}
|
|
362
|
+
for (const readScope of action.reads || [])
|
|
363
|
+
failures.push(...validateActionPathTemplateTokens(readScope, `${name} action read scope`));
|
|
364
|
+
if (action.type === "script") {
|
|
365
|
+
const actionPath = path.join(preset.directory, action.command || "");
|
|
366
|
+
if (!action.command)
|
|
367
|
+
failures.push(`${name} action missing command`);
|
|
368
|
+
else if (!action.command.endsWith(".mjs"))
|
|
369
|
+
failures.push(`${name} action command must be a .mjs file: ${action.command}`);
|
|
370
|
+
else if (!isInside(preset.directory, actionPath))
|
|
371
|
+
failures.push(`${name} action command escapes preset package`);
|
|
372
|
+
else {
|
|
373
|
+
validatePresetPackageFile(preset, action.command, `${name} action command`, failures);
|
|
374
|
+
warnOnRuntimePathLiterals(actionPath, `${name} action command`, warnings);
|
|
375
|
+
}
|
|
270
376
|
}
|
|
271
377
|
}
|
|
378
|
+
for (const scope of preset.writeScopes)
|
|
379
|
+
failures.push(...validateHarnessPathTemplateTokens(scope.path, `${scope.name || "write scope"} path`));
|
|
272
380
|
for (const [templateKey, templatePath] of Object.entries(preset.newTaskTemplates)) {
|
|
273
381
|
if (!allowedNewTaskTemplateKeys.has(templateKey)) {
|
|
274
382
|
failures.push(`unsupported newTask template: ${templateKey}`);
|
|
@@ -282,7 +390,7 @@ export function validatePresetPackage(preset) {
|
|
|
282
390
|
}
|
|
283
391
|
return { failures, warnings };
|
|
284
392
|
}
|
|
285
|
-
export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [] } = {}) {
|
|
393
|
+
export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypoint = "newTask", writeScopes = [], resolvedInputs = {} } = {}) {
|
|
286
394
|
const entrypoints = {
|
|
287
395
|
[entrypoint]: preset.entrypoints[entrypoint],
|
|
288
396
|
};
|
|
@@ -292,13 +400,64 @@ export function buildPresetAudit(preset, { taskId = "", targetRoot = "", entrypo
|
|
|
292
400
|
version: preset.version,
|
|
293
401
|
manifestPath: preset.manifestRelativePath,
|
|
294
402
|
manifestSha256: preset.manifestSha256,
|
|
403
|
+
scriptSha256s: preset.scriptSha256s,
|
|
295
404
|
entrypoints,
|
|
296
405
|
writeScopes: scopes,
|
|
406
|
+
resolvedInputs,
|
|
297
407
|
taskId,
|
|
298
408
|
targetRoot,
|
|
299
409
|
generatedAt: new Date().toISOString(),
|
|
300
410
|
};
|
|
301
411
|
}
|
|
412
|
+
export function buildPresetScriptPolicy(preset) {
|
|
413
|
+
const scriptCommands = [
|
|
414
|
+
...Object.entries(preset.entrypoints || {})
|
|
415
|
+
.filter(([, entrypoint]) => entrypoint.type === "script")
|
|
416
|
+
.map(([name, entrypoint]) => `entrypoint:${name}:${entrypoint.command}`),
|
|
417
|
+
...Object.entries(preset.actions || {})
|
|
418
|
+
.filter(([, action]) => action.type === "script")
|
|
419
|
+
.map(([name, action]) => `action:${name}:${action.command}`),
|
|
420
|
+
];
|
|
421
|
+
const unsupportedCommands = Object.entries(preset.actions || {})
|
|
422
|
+
.filter(([, action]) => action.type === "script" && !String(action.command || "").endsWith(".mjs"))
|
|
423
|
+
.map(([name, action]) => `action:${name}:${action.command || "(missing)"}`);
|
|
424
|
+
const requiresTrustedSource = scriptCommands.length > 0 && preset.source !== "builtin";
|
|
425
|
+
return {
|
|
426
|
+
hasScripts: scriptCommands.length > 0,
|
|
427
|
+
scriptCommands,
|
|
428
|
+
scriptSha256s: preset.scriptSha256s,
|
|
429
|
+
riskLevel: scriptCommands.length > 0 ? "trusted-code" : "none",
|
|
430
|
+
requiresTrustedSource,
|
|
431
|
+
unsupportedCommands,
|
|
432
|
+
warnings: requiresTrustedSource
|
|
433
|
+
? ["Script entrypoints and actions execute trusted local Node.js code; use --allow-scripts only for sources you trust."]
|
|
434
|
+
: [],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export function presetScriptTrustValid(preset) {
|
|
438
|
+
if (preset.source === "builtin")
|
|
439
|
+
return true;
|
|
440
|
+
const trustPath = path.join(preset.directory, presetScriptTrustFile);
|
|
441
|
+
const trust = asRecord(readJsonSafe(trustPath, {}));
|
|
442
|
+
const trustedScriptSha256s = asRecord(trust.scriptSha256s);
|
|
443
|
+
return (trust.schemaVersion === "preset-script-trust/v1" &&
|
|
444
|
+
trust.preset === preset.id &&
|
|
445
|
+
trust.manifestSha256 === preset.manifestSha256 &&
|
|
446
|
+
recordsEqual(trustedScriptSha256s, preset.scriptSha256s) &&
|
|
447
|
+
trust.trusted === true);
|
|
448
|
+
}
|
|
449
|
+
function writePresetScriptTrustMarker(destination, preset, scriptCommands) {
|
|
450
|
+
fs.writeFileSync(path.join(destination, presetScriptTrustFile), `${JSON.stringify({
|
|
451
|
+
schemaVersion: "preset-script-trust/v1",
|
|
452
|
+
preset: preset.id,
|
|
453
|
+
version: preset.version,
|
|
454
|
+
manifestSha256: preset.manifestSha256,
|
|
455
|
+
scriptCommands,
|
|
456
|
+
scriptSha256s: preset.scriptSha256s,
|
|
457
|
+
trusted: true,
|
|
458
|
+
trustedAt: new Date().toISOString(),
|
|
459
|
+
}, null, 2)}\n`);
|
|
460
|
+
}
|
|
302
461
|
export function renderPresetTemplate(preset, templatePath, values) {
|
|
303
462
|
if (!templatePath)
|
|
304
463
|
return "";
|
|
@@ -306,10 +465,7 @@ export function renderPresetTemplate(preset, templatePath, values) {
|
|
|
306
465
|
if (!isInside(preset.directory, absolute))
|
|
307
466
|
throw new Error(`Preset template escapes package: ${templatePath}`);
|
|
308
467
|
const content = fs.readFileSync(absolute, "utf8");
|
|
309
|
-
return content
|
|
310
|
-
const value = getValue(values, key);
|
|
311
|
-
return value == null ? "" : String(value);
|
|
312
|
-
});
|
|
468
|
+
return renderHarnessTemplate(content, values, { missing: "empty" });
|
|
313
469
|
}
|
|
314
470
|
function normalizePresetId(id) {
|
|
315
471
|
const normalized = String(id || "").trim().toLowerCase().replaceAll("_", "-");
|
|
@@ -346,74 +502,140 @@ function projectPresetDestination(id, targetInput) {
|
|
|
346
502
|
}
|
|
347
503
|
function normalizePresetManifest(manifest, { id, manifestPath, raw, source }) {
|
|
348
504
|
const directory = path.dirname(manifestPath);
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
505
|
+
const manifestRecord = asRecord(manifest);
|
|
506
|
+
const entrypoints = normalizeEntryPoints(asRecord(manifestRecord.entrypoints));
|
|
507
|
+
const actions = normalizeActions(asRecord(manifestRecord.actions));
|
|
508
|
+
const writeScopes = Object.entries(asRecord(manifestRecord.writeScopes)).map(([name, value]) => {
|
|
509
|
+
const scope = asRecord(value);
|
|
510
|
+
return {
|
|
511
|
+
name,
|
|
512
|
+
path: String(scope.path || value || "").trim(),
|
|
513
|
+
access: String(scope.access || "write").trim(),
|
|
514
|
+
};
|
|
515
|
+
}).filter((scope) => scope.path);
|
|
516
|
+
const task = asRecord(manifestRecord.task);
|
|
517
|
+
const entrypointRecord = asRecord(manifestRecord.entrypoints);
|
|
518
|
+
const newTask = asRecord(entrypointRecord.newTask);
|
|
355
519
|
return {
|
|
356
|
-
id: normalizePresetId(
|
|
357
|
-
version: Number.parseInt(
|
|
358
|
-
purpose: String(
|
|
359
|
-
compatibleBudgets: asArray(
|
|
360
|
-
localeSupport: asArray(
|
|
361
|
-
task
|
|
362
|
-
inputs: normalizeInputs(
|
|
363
|
-
templateValues: normalizeTemplateValues(
|
|
364
|
-
metadata: normalizeTemplateValues(
|
|
365
|
-
resources: normalizeResources(
|
|
366
|
-
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)),
|
|
367
531
|
entrypoints,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
532
|
+
actions,
|
|
533
|
+
workbench: asRecord(manifestRecord.workbench),
|
|
534
|
+
evidence: asEvidence(asRecord(manifestRecord.evidence)),
|
|
535
|
+
review: asRecord(manifestRecord.review),
|
|
371
536
|
audit: {
|
|
372
|
-
manifestRequired: asBoolean(
|
|
373
|
-
evidenceFiles: asArray(
|
|
537
|
+
manifestRequired: asBoolean(asRecord(manifestRecord.audit).manifestRequired),
|
|
538
|
+
evidenceFiles: asArray(asRecord(manifestRecord.audit).evidenceFiles),
|
|
374
539
|
},
|
|
375
540
|
writeScopes,
|
|
376
|
-
newTaskTemplates:
|
|
541
|
+
newTaskTemplates: stringRecord(newTask.templates),
|
|
377
542
|
directory,
|
|
378
543
|
source,
|
|
379
544
|
manifestPath,
|
|
380
545
|
manifestRelativePath: displayManifestPath(manifestPath),
|
|
381
546
|
manifestSha256: crypto.createHash("sha256").update(raw).digest("hex"),
|
|
547
|
+
scriptSha256s: collectPresetScriptSha256s({ directory, entrypoints, actions }),
|
|
382
548
|
};
|
|
383
549
|
}
|
|
550
|
+
function collectPresetScriptSha256s({ directory, entrypoints, actions }) {
|
|
551
|
+
const scripts = {};
|
|
552
|
+
for (const [name, entrypoint] of Object.entries(entrypoints || {})) {
|
|
553
|
+
if (entrypoint.type !== "script")
|
|
554
|
+
continue;
|
|
555
|
+
const command = String(entrypoint.command || "");
|
|
556
|
+
if (!command)
|
|
557
|
+
continue;
|
|
558
|
+
const absolute = path.join(directory, command);
|
|
559
|
+
if (isInside(directory, absolute) && fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) {
|
|
560
|
+
scripts[`entrypoint:${name}:${command}`] = sha256File(absolute);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (const [name, action] of Object.entries(actions || {})) {
|
|
564
|
+
if (action.type !== "script")
|
|
565
|
+
continue;
|
|
566
|
+
const command = String(action.command || "");
|
|
567
|
+
if (!command)
|
|
568
|
+
continue;
|
|
569
|
+
const absolute = path.join(directory, command);
|
|
570
|
+
if (isInside(directory, absolute) && fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) {
|
|
571
|
+
scripts[`action:${name}:${command}`] = sha256File(absolute);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return scripts;
|
|
575
|
+
}
|
|
576
|
+
function sha256File(filePath) {
|
|
577
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
578
|
+
}
|
|
579
|
+
function recordsEqual(left, right) {
|
|
580
|
+
const leftEntries = Object.entries(left).map(([key, value]) => [key, String(value)]).sort(([a], [b]) => a.localeCompare(b));
|
|
581
|
+
const rightEntries = Object.entries(right).map(([key, value]) => [key, String(value)]).sort(([a], [b]) => a.localeCompare(b));
|
|
582
|
+
return JSON.stringify(leftEntries) === JSON.stringify(rightEntries);
|
|
583
|
+
}
|
|
584
|
+
function asRecord(value) {
|
|
585
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
586
|
+
}
|
|
587
|
+
function stringRecord(value) {
|
|
588
|
+
return Object.fromEntries(Object.entries(asRecord(value)).map(([key, item]) => [key, String(item || "")]));
|
|
589
|
+
}
|
|
590
|
+
function asEvidence(value) {
|
|
591
|
+
if (value.files === undefined)
|
|
592
|
+
return value;
|
|
593
|
+
if (typeof value.files !== "object" || value.files === null || Array.isArray(value.files))
|
|
594
|
+
return value;
|
|
595
|
+
const rawFiles = asRecord(value.files);
|
|
596
|
+
const files = Object.fromEntries(Object.entries(rawFiles).map(([name, item]) => {
|
|
597
|
+
const record = asRecord(item);
|
|
598
|
+
return [name, {
|
|
599
|
+
path: record.path === undefined ? undefined : String(record.path),
|
|
600
|
+
type: record.type === undefined ? undefined : String(record.type),
|
|
601
|
+
value: record.value === undefined ? undefined : String(record.value),
|
|
602
|
+
}];
|
|
603
|
+
}));
|
|
604
|
+
return { ...value, files };
|
|
605
|
+
}
|
|
384
606
|
function normalizeInputs(rawInputs) {
|
|
385
607
|
return Object.fromEntries(Object.entries(rawInputs || {}).map(([name, value]) => [name, {
|
|
386
|
-
type: String(value.type || "text").trim(),
|
|
387
|
-
flag: String(value.flag || "").trim(),
|
|
388
|
-
required: asBoolean(value.required),
|
|
389
|
-
default: value.default,
|
|
390
|
-
validateOperation: String(value.validateOperation || "").trim(),
|
|
391
|
-
rejectPlanOnly: asBoolean(value.rejectPlanOnly),
|
|
392
|
-
requireTarget: asBoolean(value.requireTarget),
|
|
393
|
-
targetFromSession: asBoolean(value.targetFromSession),
|
|
608
|
+
type: String(asRecord(value).type || "text").trim(),
|
|
609
|
+
flag: String(asRecord(value).flag || "").trim(),
|
|
610
|
+
required: asBoolean(asRecord(value).required),
|
|
611
|
+
default: asRecord(value).default,
|
|
612
|
+
validateOperation: String(asRecord(value).validateOperation || "").trim(),
|
|
613
|
+
rejectPlanOnly: asBoolean(asRecord(value).rejectPlanOnly),
|
|
614
|
+
requireTarget: asBoolean(asRecord(value).requireTarget),
|
|
615
|
+
targetFromSession: asBoolean(asRecord(value).targetFromSession),
|
|
394
616
|
}]));
|
|
395
617
|
}
|
|
396
618
|
function normalizeTemplateValues(rawValues) {
|
|
397
|
-
return Object.fromEntries(Object.entries(rawValues || {}).map(([name, value]) => [name, typeof value === "object" && value !== null ? value : { value }]));
|
|
619
|
+
return Object.fromEntries(Object.entries(rawValues || {}).map(([name, value]) => [name, typeof value === "object" && value !== null ? asRecord(value) : { value }]));
|
|
398
620
|
}
|
|
399
621
|
function normalizeResources(rawResources) {
|
|
400
622
|
return {
|
|
401
|
-
references: normalizeResourceGroup(rawResources.references
|
|
402
|
-
artifacts: normalizeResourceGroup(rawResources.artifacts
|
|
623
|
+
references: normalizeResourceGroup(asRecord(rawResources.references)),
|
|
624
|
+
artifacts: normalizeResourceGroup(asRecord(rawResources.artifacts)),
|
|
403
625
|
};
|
|
404
626
|
}
|
|
405
627
|
function normalizeResourceGroup(rawGroup) {
|
|
406
628
|
return Object.fromEntries(Object.entries(rawGroup || {}).map(([name, value]) => [name, {
|
|
407
629
|
name,
|
|
408
|
-
path: String(value.path || "").trim(),
|
|
409
|
-
source: String(value.source || "").trim(),
|
|
410
|
-
template: String(value.template || "").trim(),
|
|
630
|
+
path: String(asRecord(value).path || "").trim(),
|
|
631
|
+
source: String(asRecord(value).source || "").trim(),
|
|
632
|
+
template: String(asRecord(value).template || "").trim(),
|
|
411
633
|
index: {
|
|
412
|
-
id: String(value.index
|
|
413
|
-
type: String(value.index
|
|
414
|
-
summary: String(value.index
|
|
415
|
-
usedBy: String(value.index
|
|
416
|
-
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(),
|
|
417
639
|
},
|
|
418
640
|
}]));
|
|
419
641
|
}
|
|
@@ -425,13 +647,30 @@ function normalizeContext(rawContext) {
|
|
|
425
647
|
function normalizeEntryPoints(rawEntryPoints) {
|
|
426
648
|
const result = {};
|
|
427
649
|
for (const [name, value] of Object.entries(rawEntryPoints || {})) {
|
|
650
|
+
const entrypoint = asRecord(value);
|
|
428
651
|
result[name] = {
|
|
429
|
-
type: String(
|
|
430
|
-
command:
|
|
431
|
-
templates:
|
|
432
|
-
writes: asArray(
|
|
433
|
-
reads: asArray(
|
|
434
|
-
audit: asBoolean(
|
|
652
|
+
type: String(entrypoint.type || "").trim(),
|
|
653
|
+
command: entrypoint.command ? String(entrypoint.command).trim() : "",
|
|
654
|
+
templates: stringRecord(entrypoint.templates),
|
|
655
|
+
writes: asArray(entrypoint.writes),
|
|
656
|
+
reads: asArray(entrypoint.reads),
|
|
657
|
+
audit: asBoolean(entrypoint.audit),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
function normalizeActions(rawActions) {
|
|
663
|
+
const result = {};
|
|
664
|
+
for (const [name, value] of Object.entries(rawActions || {})) {
|
|
665
|
+
const action = asRecord(value);
|
|
666
|
+
result[name] = {
|
|
667
|
+
type: String(action.type || "").trim(),
|
|
668
|
+
command: action.command ? String(action.command).trim() : "",
|
|
669
|
+
taskRequired: asBoolean(action.taskRequired),
|
|
670
|
+
inputs: normalizeInputs(asRecord(action.inputs)),
|
|
671
|
+
writes: asArray(action.writes),
|
|
672
|
+
reads: asArray(action.reads),
|
|
673
|
+
audit: asBoolean(action.audit),
|
|
435
674
|
};
|
|
436
675
|
}
|
|
437
676
|
return result;
|
|
@@ -445,6 +684,7 @@ function publicPresetShape(preset) {
|
|
|
445
684
|
localeSupport: preset.localeSupport,
|
|
446
685
|
task: preset.task,
|
|
447
686
|
entrypoints: preset.entrypoints,
|
|
687
|
+
actions: preset.actions,
|
|
448
688
|
workbench: preset.workbench,
|
|
449
689
|
evidence: preset.evidence,
|
|
450
690
|
review: preset.review,
|
|
@@ -458,6 +698,7 @@ function publicPresetShape(preset) {
|
|
|
458
698
|
source: preset.source,
|
|
459
699
|
manifestPath: preset.manifestRelativePath,
|
|
460
700
|
manifestSha256: preset.manifestSha256,
|
|
701
|
+
scriptPolicy: buildPresetScriptPolicy(preset),
|
|
461
702
|
};
|
|
462
703
|
}
|
|
463
704
|
function validateResourceCollection(preset, label, groupName, requiredPrefix, resourcePaths, failures) {
|
|
@@ -487,13 +728,14 @@ function validateResourceCollection(preset, label, groupName, requiredPrefix, re
|
|
|
487
728
|
if (resource.source && resource.template)
|
|
488
729
|
failures.push(`${label} resource ${name} cannot declare both source and template`);
|
|
489
730
|
for (const field of ["source", "template"]) {
|
|
490
|
-
|
|
731
|
+
const declaredPath = resource[field];
|
|
732
|
+
if (!declaredPath)
|
|
491
733
|
continue;
|
|
492
|
-
const resourcePath = path.join(preset.directory,
|
|
734
|
+
const resourcePath = path.join(preset.directory, declaredPath);
|
|
493
735
|
if (!isInside(preset.directory, resourcePath))
|
|
494
736
|
failures.push(`${label} resource ${name} ${field} escapes preset package`);
|
|
495
737
|
else
|
|
496
|
-
validatePresetPackageFile(preset,
|
|
738
|
+
validatePresetPackageFile(preset, declaredPath, `${label} resource ${name} ${field}`, failures);
|
|
497
739
|
}
|
|
498
740
|
const id = resource.index?.id || "";
|
|
499
741
|
if (!id)
|
|
@@ -526,6 +768,57 @@ function validateAuditEvidenceFiles(preset, failures) {
|
|
|
526
768
|
}
|
|
527
769
|
}
|
|
528
770
|
}
|
|
771
|
+
function validateActionPathTemplateTokens(content, label) {
|
|
772
|
+
const failures = validateHarnessPathTemplateTokens(content, label);
|
|
773
|
+
const allowedTaskTokens = new Set([
|
|
774
|
+
"task.paths.dir",
|
|
775
|
+
"task.paths.taskPlan",
|
|
776
|
+
"task.paths.progress",
|
|
777
|
+
"task.paths.artifacts",
|
|
778
|
+
"task.paths.artifactsIndex",
|
|
779
|
+
"task.paths.visualMap",
|
|
780
|
+
]);
|
|
781
|
+
for (const match of String(content || "").matchAll(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g)) {
|
|
782
|
+
const key = match[1];
|
|
783
|
+
if (key.startsWith("paths."))
|
|
784
|
+
continue;
|
|
785
|
+
if (key.startsWith("task.") && !allowedTaskTokens.has(key))
|
|
786
|
+
failures.push(`${label} uses unknown task token: ${key}`);
|
|
787
|
+
}
|
|
788
|
+
return failures;
|
|
789
|
+
}
|
|
790
|
+
function isBroadTaskRootScope(scope) {
|
|
791
|
+
const normalized = toPosix(path.normalize(String(scope || "")));
|
|
792
|
+
return normalized === "{{paths.tasksRoot}}/**" ||
|
|
793
|
+
normalized === "coding-agent-harness/planning/tasks/**" ||
|
|
794
|
+
normalized === ["docs", "09-PLANNING", "TASKS", "**"].join("/");
|
|
795
|
+
}
|
|
796
|
+
function usesTaskLocalPath(scope) {
|
|
797
|
+
return String(scope || "").includes("{{task.paths.");
|
|
798
|
+
}
|
|
799
|
+
function newTaskWriteScopeAllowed(writeScope) {
|
|
800
|
+
const normalized = toPosix(path.normalize(String(writeScope || "")));
|
|
801
|
+
const legacyPlanningScope = ["docs", "09-PLANNING"].join("/");
|
|
802
|
+
return (normalized === "coding-agent-harness/planning/**" ||
|
|
803
|
+
normalized.startsWith("coding-agent-harness/planning/") ||
|
|
804
|
+
normalized === "{{paths.planningRoot}}/**" ||
|
|
805
|
+
normalized.startsWith("{{paths.planningRoot}}/") ||
|
|
806
|
+
normalized === "{{paths.tasksRoot}}/**" ||
|
|
807
|
+
normalized.startsWith("{{paths.tasksRoot}}/") ||
|
|
808
|
+
normalized === "{{paths.modulesRoot}}/**" ||
|
|
809
|
+
normalized.startsWith("{{paths.modulesRoot}}/") ||
|
|
810
|
+
normalized === `${legacyPlanningScope}/**` ||
|
|
811
|
+
normalized.startsWith(`${legacyPlanningScope}/`));
|
|
812
|
+
}
|
|
813
|
+
function warnOnRuntimePathLiterals(filePath, label, warnings) {
|
|
814
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
815
|
+
return;
|
|
816
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
817
|
+
const runtimeLiteralPattern = /coding-agent-harness\/(?:planning|governance|context)\//;
|
|
818
|
+
if (runtimeLiteralPattern.test(content) && !content.includes("context.paths") && !content.includes("context.absolutePaths")) {
|
|
819
|
+
warnings.push(`${label} contains default harness path literals; prefer context.paths from the preset runner`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
529
822
|
function validatePresetPackageFile(preset, relativePath, label, failures) {
|
|
530
823
|
const filePath = path.join(preset.directory, relativePath || "");
|
|
531
824
|
if (!isInside(preset.directory, filePath)) {
|
|
@@ -556,7 +849,8 @@ export function parseSimpleYaml(source) {
|
|
|
556
849
|
for (const rawLine of String(source).split(/\r?\n/)) {
|
|
557
850
|
if (!rawLine.trim() || rawLine.trimStart().startsWith("#"))
|
|
558
851
|
continue;
|
|
559
|
-
const
|
|
852
|
+
const indentMatch = rawLine.match(/^\s*/);
|
|
853
|
+
const indent = indentMatch ? indentMatch[0].length : 0;
|
|
560
854
|
const line = rawLine.trim();
|
|
561
855
|
const match = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
562
856
|
if (!match)
|
|
@@ -568,7 +862,7 @@ export function parseSimpleYaml(source) {
|
|
|
568
862
|
const rawValue = match[2] || "";
|
|
569
863
|
if (!rawValue) {
|
|
570
864
|
parent[key] = {};
|
|
571
|
-
stack.push({ indent, object: parent[key] });
|
|
865
|
+
stack.push({ indent, object: asRecord(parent[key]) });
|
|
572
866
|
}
|
|
573
867
|
else {
|
|
574
868
|
parent[key] = parseYamlScalar(rawValue);
|
|
@@ -615,7 +909,14 @@ function hasMarkdownTableDelimiter(value) {
|
|
|
615
909
|
return /[|\r\n]/.test(String(value || ""));
|
|
616
910
|
}
|
|
617
911
|
function getValue(values, key) {
|
|
618
|
-
|
|
912
|
+
let cursor = values;
|
|
913
|
+
for (const part of String(key).split(".")) {
|
|
914
|
+
if (!cursor || typeof cursor !== "object" || Array.isArray(cursor))
|
|
915
|
+
return undefined;
|
|
916
|
+
const record = cursor;
|
|
917
|
+
cursor = Object.prototype.hasOwnProperty.call(record, part) ? record[part] : undefined;
|
|
918
|
+
}
|
|
919
|
+
return cursor;
|
|
619
920
|
}
|
|
620
921
|
function displayManifestPath(manifestPath) {
|
|
621
922
|
const relative = path.relative(repoRoot, manifestPath);
|
|
@@ -650,7 +951,7 @@ function presetSearchRoots({ targetInput = "", home = "" } = {}) {
|
|
|
650
951
|
function resolveInstallSource(source) {
|
|
651
952
|
const localPath = path.resolve(source);
|
|
652
953
|
if (fs.existsSync(path.join(localPath, "preset.yaml")))
|
|
653
|
-
return { path: localPath, cleanup: () => { } };
|
|
954
|
+
return { path: localPath, source: "local", cleanup: () => { } };
|
|
654
955
|
if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) {
|
|
655
956
|
if (!localPath.toLowerCase().endsWith(".zip"))
|
|
656
957
|
throw new Error(`Preset source file must be a .zip archive: ${toPosix(localPath)}`);
|
|
@@ -658,7 +959,7 @@ function resolveInstallSource(source) {
|
|
|
658
959
|
}
|
|
659
960
|
const builtinPath = path.join(builtinPresetRoot, normalizePresetId(source));
|
|
660
961
|
if (fs.existsSync(path.join(builtinPath, "preset.yaml")))
|
|
661
|
-
return { path: builtinPath, cleanup: () => { } };
|
|
962
|
+
return { path: builtinPath, source: "builtin", cleanup: () => { } };
|
|
662
963
|
throw new Error(`Preset source not found: ${source}`);
|
|
663
964
|
}
|
|
664
965
|
function resolveZipInstallSource(sourcePath) {
|
|
@@ -667,6 +968,7 @@ function resolveZipInstallSource(sourcePath) {
|
|
|
667
968
|
extractPresetZip(sourcePath, tempRoot);
|
|
668
969
|
return {
|
|
669
970
|
path: presetRootFromExtractedArchive(tempRoot),
|
|
971
|
+
source: "archive",
|
|
670
972
|
cleanup: () => fs.rmSync(tempRoot, { recursive: true, force: true }),
|
|
671
973
|
};
|
|
672
974
|
}
|