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
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// Generic preset entrypoint runner. Domain logic belongs in preset packages.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { absoluteHarnessPathContext, harnessPathContext, normalizeTarget, readFileSafe, readJsonSafe, renderHarnessTemplate, sanitizeDeep, toPosix, walkFiles, } from "./core-shared.mjs";
|
|
8
|
+
import { beginGovernanceSync, commitGovernanceSync, releaseGovernanceSync } from "./governance-sync.mjs";
|
|
9
|
+
import { parseTaskMetadata } from "./task-metadata.mjs";
|
|
10
|
+
import { taskIdForDirectory } from "./task-scanner.mjs";
|
|
11
|
+
import { resolveTaskDirectory } from "./task-lifecycle.mjs";
|
|
12
|
+
import { evaluateTemplateValues, assertPresetWriteScope, resolvePresetScopes } from "./preset-engine.mjs";
|
|
13
|
+
import { buildPresetAudit, buildPresetScriptPolicy, presetScriptTrustValid, readPresetPackage } from "./preset-registry.mjs";
|
|
14
|
+
const materializationSchemaVersion = "preset-materialization/v1";
|
|
15
|
+
const maxMaterializedFileBytes = 10 * 1024 * 1024;
|
|
16
|
+
const maxMaterializedWrites = 500;
|
|
17
|
+
export function runPresetEntrypoint(presetId, entrypointName, { taskRef = "", targetInput = ".", json = false, allowScripts = false, useCurrentPreset = false, reason = "" } = {}) {
|
|
18
|
+
void json;
|
|
19
|
+
const target = normalizeTarget(targetInput);
|
|
20
|
+
const preset = readPresetPackage(presetId, { targetInput });
|
|
21
|
+
const entrypoint = preset.entrypoints?.[entrypointName];
|
|
22
|
+
if (!entrypoint)
|
|
23
|
+
throw new Error(`Preset ${preset.id} does not declare entrypoint: ${entrypointName}`);
|
|
24
|
+
if (!["script", "check"].includes(entrypoint.type))
|
|
25
|
+
throw new Error(`Preset entrypoint ${entrypointName} is not runnable by preset run`);
|
|
26
|
+
const scriptPolicy = buildPresetScriptPolicy(preset);
|
|
27
|
+
if (scriptPolicy.requiresTrustedSource && !allowScripts && !presetScriptTrustValid(preset)) {
|
|
28
|
+
throw new Error(`Preset entrypoint ${preset.id}.${entrypointName} executes trusted local code. Re-run with --allow-scripts if you trust this preset source.`);
|
|
29
|
+
}
|
|
30
|
+
if (!taskRef)
|
|
31
|
+
throw new Error("preset run requires --task <task-id>");
|
|
32
|
+
const taskDir = resolveTaskDirectory(target, taskRef);
|
|
33
|
+
const taskPlan = readFileSafe(path.join(taskDir, "task_plan.md"));
|
|
34
|
+
const metadata = parseTaskMetadata(taskPlan);
|
|
35
|
+
if (metadata.preset !== preset.id)
|
|
36
|
+
throw new Error(`Task ${taskRef} was created by preset ${metadata.preset || "none"}, not ${preset.id}`);
|
|
37
|
+
const taskId = taskIdForDirectory(target, taskDir);
|
|
38
|
+
const audit = readPresetAudit(target, metadata);
|
|
39
|
+
const presetDrift = assessPresetDrift(audit, preset, { useCurrentPreset, reason });
|
|
40
|
+
const resolvedInputs = asRecord(audit.resolvedInputs);
|
|
41
|
+
const values = evaluateTemplateValues(preset, resolvedInputs, { taskId, taskTitle: taskId, moduleKey: "", target });
|
|
42
|
+
const resolvedScopes = resolvePresetScopes(preset, target);
|
|
43
|
+
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), `harness-preset-${preset.id}-${entrypointName}-`));
|
|
44
|
+
const manifestPath = path.join(outputRoot, "materialization-manifest.json");
|
|
45
|
+
const contextPath = path.join(outputRoot, "preset-context.json");
|
|
46
|
+
const beforeSnapshot = targetSnapshot(target.projectRoot);
|
|
47
|
+
try {
|
|
48
|
+
const context = {
|
|
49
|
+
schemaVersion: "preset-run-context/v1",
|
|
50
|
+
preset: { id: preset.id, version: preset.version, source: preset.source },
|
|
51
|
+
entrypoint: entrypointName,
|
|
52
|
+
task: {
|
|
53
|
+
id: taskId,
|
|
54
|
+
ref: taskRef,
|
|
55
|
+
dir: toPosix(path.relative(target.projectRoot, taskDir)),
|
|
56
|
+
taskPlanPath: toPosix(path.relative(target.projectRoot, path.join(taskDir, "task_plan.md"))),
|
|
57
|
+
},
|
|
58
|
+
targetRoot: target.projectRoot,
|
|
59
|
+
targetRootPolicy: "read-only; direct target mutation before manifest materialization is a hard failure",
|
|
60
|
+
runtime: {
|
|
61
|
+
coreModule: new URL("./harness-core.mjs", import.meta.url).href,
|
|
62
|
+
},
|
|
63
|
+
outputRoot,
|
|
64
|
+
materializationManifestPath: manifestPath,
|
|
65
|
+
paths: harnessPathContext(target),
|
|
66
|
+
absolutePaths: absoluteHarnessPathContext(target),
|
|
67
|
+
inputs: sanitizeDeep(resolvedInputs),
|
|
68
|
+
values: sanitizeDeep(values),
|
|
69
|
+
audit: buildPresetAudit(preset, {
|
|
70
|
+
taskId,
|
|
71
|
+
targetRoot: target.projectRoot,
|
|
72
|
+
entrypoint: entrypointName,
|
|
73
|
+
writeScopes: resolvedScopes.entrypoints[entrypointName] || entrypoint.writes,
|
|
74
|
+
resolvedInputs,
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`);
|
|
78
|
+
const commandPath = path.join(preset.directory, entrypoint.command || "");
|
|
79
|
+
const script = spawnSync(process.execPath, [commandPath], {
|
|
80
|
+
cwd: outputRoot,
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
env: presetScriptEnv(contextPath),
|
|
83
|
+
timeout: 120000,
|
|
84
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
85
|
+
});
|
|
86
|
+
if (script.error)
|
|
87
|
+
throw script.error;
|
|
88
|
+
if (script.status !== 0) {
|
|
89
|
+
throw new Error(`Preset entrypoint ${preset.id}.${entrypointName} failed with ${script.status}\n${script.stderr || script.stdout || ""}`.trim());
|
|
90
|
+
}
|
|
91
|
+
const afterScriptSnapshot = targetSnapshot(target.projectRoot);
|
|
92
|
+
assertSnapshotsEqual(beforeSnapshot, afterScriptSnapshot, "Preset script mutated target before materialization");
|
|
93
|
+
const manifest = readMaterializationManifest(manifestPath);
|
|
94
|
+
const materialization = validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, target, entrypointName });
|
|
95
|
+
const governanceContext = beginGovernanceSync(target, {
|
|
96
|
+
operation: `preset-run ${preset.id}.${entrypointName}`,
|
|
97
|
+
allowDirtyWorktree: true,
|
|
98
|
+
allowedRelativePaths: materialization.map((item) => item.destination),
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
materializeWrites(target.projectRoot, materialization);
|
|
102
|
+
const commit = commitGovernanceSync(governanceContext, materialization.map((item) => item.destination), {
|
|
103
|
+
message: `chore(harness): run preset ${preset.id} ${entrypointName}`,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
preset: preset.id,
|
|
107
|
+
entrypoint: entrypointName,
|
|
108
|
+
taskId,
|
|
109
|
+
status: manifest.status || (entrypoint.type === "check" ? "pass" : "ok"),
|
|
110
|
+
materialized: materialization.map((item) => ({
|
|
111
|
+
source: item.source,
|
|
112
|
+
destination: item.destination,
|
|
113
|
+
type: item.type,
|
|
114
|
+
sha256: item.sha256,
|
|
115
|
+
})),
|
|
116
|
+
governance: { commit },
|
|
117
|
+
presetDrift,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
releaseGovernanceSync(governanceContext);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function runPresetAction(presetId, actionName, { taskRef = "", targetInput = ".", json = false, actionArgs = [], allowScripts = false, useCurrentPreset = false, reason = "" } = {}) {
|
|
129
|
+
void json;
|
|
130
|
+
const target = normalizeTarget(targetInput);
|
|
131
|
+
const preset = readPresetPackage(presetId, { targetInput });
|
|
132
|
+
const action = preset.actions?.[actionName];
|
|
133
|
+
if (!action)
|
|
134
|
+
throw new Error(`Preset ${preset.id} does not declare action: ${actionName}`);
|
|
135
|
+
if (action.type !== "script")
|
|
136
|
+
throw new Error(`Preset action ${actionName} is not runnable by preset action`);
|
|
137
|
+
if (action.taskRequired !== true)
|
|
138
|
+
throw new Error(`Preset action ${preset.id}.${actionName} requires taskRequired: true`);
|
|
139
|
+
if (!taskRef)
|
|
140
|
+
throw new Error("preset action requires --task <task-id>");
|
|
141
|
+
const scriptPolicy = buildPresetScriptPolicy(preset);
|
|
142
|
+
if (scriptPolicy.requiresTrustedSource && !allowScripts && !presetScriptTrustValid(preset)) {
|
|
143
|
+
throw new Error(`Preset action ${preset.id}.${actionName} executes trusted local code. Re-run with --allow-scripts if you trust this preset source.`);
|
|
144
|
+
}
|
|
145
|
+
const taskDir = resolveTaskDirectory(target, taskRef);
|
|
146
|
+
const taskPlan = readFileSafe(path.join(taskDir, "task_plan.md"));
|
|
147
|
+
const metadata = parseTaskMetadata(taskPlan);
|
|
148
|
+
if (metadata.preset !== preset.id)
|
|
149
|
+
throw new Error(`Task ${taskRef} was created by preset ${metadata.preset || "none"}, not ${preset.id}`);
|
|
150
|
+
const taskId = taskIdForDirectory(target, taskDir);
|
|
151
|
+
const taskPaths = taskPathContext(target, taskDir);
|
|
152
|
+
const actionInputs = resolveActionInputs(action, actionArgs);
|
|
153
|
+
const audit = readPresetAudit(target, metadata);
|
|
154
|
+
const presetDrift = assessPresetDrift(audit, preset, { useCurrentPreset, reason });
|
|
155
|
+
const creationInputs = asRecord(audit.resolvedInputs);
|
|
156
|
+
const values = evaluateTemplateValues(preset, creationInputs, { taskId, taskTitle: taskId, moduleKey: "", target });
|
|
157
|
+
const actionWriteScopes = resolveActionScopes(action.writes, { target, taskPaths, label: `${actionName} action write scope` });
|
|
158
|
+
const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), `harness-preset-${preset.id}-${actionName}-`));
|
|
159
|
+
const manifestPath = path.join(outputRoot, "materialization-manifest.json");
|
|
160
|
+
const contextPath = path.join(outputRoot, "preset-action-context.json");
|
|
161
|
+
let governanceContext = null;
|
|
162
|
+
try {
|
|
163
|
+
governanceContext = beginGovernanceSync(target, {
|
|
164
|
+
operation: `preset-action ${preset.id}.${actionName}`,
|
|
165
|
+
allowDirtyWorktree: true,
|
|
166
|
+
allowedRelativePaths: concreteActionWriteScopes(actionWriteScopes),
|
|
167
|
+
});
|
|
168
|
+
const beforeSnapshot = targetSnapshot(target.projectRoot);
|
|
169
|
+
const context = {
|
|
170
|
+
schemaVersion: "preset-action-context/v1",
|
|
171
|
+
preset: { id: preset.id, version: preset.version, source: preset.source, manifestSha256: preset.manifestSha256 },
|
|
172
|
+
action: { id: actionName, type: action.type },
|
|
173
|
+
task: {
|
|
174
|
+
id: taskId,
|
|
175
|
+
ref: taskRef,
|
|
176
|
+
dir: taskPaths.dir,
|
|
177
|
+
taskPlanPath: taskPaths.taskPlan,
|
|
178
|
+
paths: taskPaths,
|
|
179
|
+
},
|
|
180
|
+
targetRoot: target.projectRoot,
|
|
181
|
+
targetRootPolicy: "read-only; direct target mutation before manifest materialization is a hard failure",
|
|
182
|
+
outputRoot,
|
|
183
|
+
materializationManifestPath: manifestPath,
|
|
184
|
+
paths: harnessPathContext(target),
|
|
185
|
+
inputs: sanitizeDeep(actionInputs),
|
|
186
|
+
creationInputs: sanitizeDeep(creationInputs),
|
|
187
|
+
values: sanitizeDeep(values),
|
|
188
|
+
audit: buildPresetAudit(preset, {
|
|
189
|
+
taskId,
|
|
190
|
+
targetRoot: target.projectRoot,
|
|
191
|
+
entrypoint: `action:${actionName}`,
|
|
192
|
+
writeScopes: actionWriteScopes,
|
|
193
|
+
resolvedInputs: actionInputs,
|
|
194
|
+
}),
|
|
195
|
+
presetDrift,
|
|
196
|
+
};
|
|
197
|
+
fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`);
|
|
198
|
+
const commandPath = path.join(preset.directory, action.command || "");
|
|
199
|
+
const script = spawnSync(process.execPath, [commandPath], {
|
|
200
|
+
cwd: outputRoot,
|
|
201
|
+
encoding: "utf8",
|
|
202
|
+
env: presetScriptEnv(contextPath),
|
|
203
|
+
timeout: 120000,
|
|
204
|
+
maxBuffer: 128 * 1024,
|
|
205
|
+
});
|
|
206
|
+
if (script.error)
|
|
207
|
+
throw script.error;
|
|
208
|
+
if (script.status !== 0) {
|
|
209
|
+
throw new Error(`Preset action ${preset.id}.${actionName} failed with ${script.status}\n${boundedScriptOutput(script.stderr || script.stdout || "")}`.trim());
|
|
210
|
+
}
|
|
211
|
+
const afterScriptSnapshot = targetSnapshot(target.projectRoot);
|
|
212
|
+
assertSnapshotsEqual(beforeSnapshot, afterScriptSnapshot, "Preset script mutated target before materialization");
|
|
213
|
+
const manifest = readMaterializationManifest(manifestPath);
|
|
214
|
+
const materialization = validateMaterializationManifest(preset, {
|
|
215
|
+
type: action.type,
|
|
216
|
+
command: action.command,
|
|
217
|
+
templates: {},
|
|
218
|
+
writes: actionWriteScopes,
|
|
219
|
+
reads: action.reads,
|
|
220
|
+
audit: action.audit,
|
|
221
|
+
}, manifest, { outputRoot, target, entrypointName: actionName });
|
|
222
|
+
materializeWrites(target.projectRoot, materialization);
|
|
223
|
+
const commit = commitGovernanceSync(governanceContext, materialization.map((item) => item.destination), {
|
|
224
|
+
message: `chore(harness): run preset action ${preset.id} ${actionName}`,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
preset: preset.id,
|
|
228
|
+
action: actionName,
|
|
229
|
+
source: preset.source,
|
|
230
|
+
manifestSha256: preset.manifestSha256,
|
|
231
|
+
taskId,
|
|
232
|
+
status: manifest.status || "ok",
|
|
233
|
+
materialized: materialization.map((item) => ({
|
|
234
|
+
source: item.source,
|
|
235
|
+
destination: item.destination,
|
|
236
|
+
type: item.type,
|
|
237
|
+
sha256: item.sha256,
|
|
238
|
+
})),
|
|
239
|
+
governance: { commit },
|
|
240
|
+
presetDrift,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
if (governanceContext)
|
|
245
|
+
releaseGovernanceSync(governanceContext);
|
|
246
|
+
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function readPresetAudit(target, metadata) {
|
|
250
|
+
const evidenceBundle = normalizeTargetRelativePath(metadata.evidenceBundle || "", "Preset evidence bundle");
|
|
251
|
+
if (!evidenceBundle)
|
|
252
|
+
return {};
|
|
253
|
+
const auditPath = path.join(target.projectRoot, evidenceBundle, "preset-audit.json");
|
|
254
|
+
const audit = asRecord(readJsonSafe(auditPath, {}));
|
|
255
|
+
return audit;
|
|
256
|
+
}
|
|
257
|
+
function assessPresetDrift(audit, preset, { useCurrentPreset = false, reason = "" } = {}) {
|
|
258
|
+
const recorded = String(audit.manifestSha256 || "");
|
|
259
|
+
const current = String(preset.manifestSha256 || "");
|
|
260
|
+
if (!recorded || !current || recorded === current) {
|
|
261
|
+
return { detected: false, accepted: false, recordedManifestSha256: recorded, currentManifestSha256: current, reason: "" };
|
|
262
|
+
}
|
|
263
|
+
const normalizedReason = String(reason || "").trim();
|
|
264
|
+
if (!useCurrentPreset || !normalizedReason) {
|
|
265
|
+
throw new Error(`Preset manifest hash drift detected for ${preset.id}: recorded ${recorded}, current ${current}. Re-run with --use-current-preset --reason <why-current-semantics-are-intended>, or create a new task with the current preset.`);
|
|
266
|
+
}
|
|
267
|
+
return { detected: true, accepted: true, recordedManifestSha256: recorded, currentManifestSha256: current, reason: normalizedReason };
|
|
268
|
+
}
|
|
269
|
+
function resolveActionInputs(action, cliArgs) {
|
|
270
|
+
const remaining = [...cliArgs];
|
|
271
|
+
const inputs = {};
|
|
272
|
+
for (const [name, declaration] of Object.entries(action.inputs || {})) {
|
|
273
|
+
inputs[name] = resolveActionInputValue(name, declaration, remaining);
|
|
274
|
+
}
|
|
275
|
+
if (remaining.length > 0)
|
|
276
|
+
throw new Error(`Unknown action argument: ${remaining[0]}`);
|
|
277
|
+
return inputs;
|
|
278
|
+
}
|
|
279
|
+
function resolveActionInputValue(name, declaration, remaining) {
|
|
280
|
+
const flag = declaration.flag || name;
|
|
281
|
+
const index = remaining.indexOf(flag);
|
|
282
|
+
if (declaration.type === "flag") {
|
|
283
|
+
if (index >= 0) {
|
|
284
|
+
remaining.splice(index, 1);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return Boolean(declaration.default);
|
|
288
|
+
}
|
|
289
|
+
if (index < 0) {
|
|
290
|
+
if (declaration.required)
|
|
291
|
+
throw new Error(`Missing required action input ${flag}`);
|
|
292
|
+
return declaration.default ?? "";
|
|
293
|
+
}
|
|
294
|
+
const value = remaining[index + 1] || "";
|
|
295
|
+
if (!value || value.startsWith("--"))
|
|
296
|
+
throw new Error(`Missing value for action input ${flag}`);
|
|
297
|
+
remaining.splice(index, 2);
|
|
298
|
+
if (String(value).includes("\0"))
|
|
299
|
+
throw new Error(`Action input ${flag} contains NUL byte`);
|
|
300
|
+
if (declaration.type === "text") {
|
|
301
|
+
if (String(value).length > 4096)
|
|
302
|
+
throw new Error(`Action input ${flag} exceeds text length limit`);
|
|
303
|
+
return String(value);
|
|
304
|
+
}
|
|
305
|
+
if (declaration.type === "json-file") {
|
|
306
|
+
const filePath = path.resolve(String(value));
|
|
307
|
+
if (!fs.existsSync(filePath))
|
|
308
|
+
throw new Error(`Action input file not found for ${flag}: ${value}`);
|
|
309
|
+
const stat = fs.statSync(filePath);
|
|
310
|
+
if (!stat.isFile())
|
|
311
|
+
throw new Error(`Action input file must be a file for ${flag}: ${value}`);
|
|
312
|
+
if (stat.size > 1024 * 1024)
|
|
313
|
+
throw new Error(`Action input file exceeds size limit for ${flag}: ${value}`);
|
|
314
|
+
let readError = null;
|
|
315
|
+
const parsed = readJsonSafe(filePath, null, { onError: (error) => { readError = error; } });
|
|
316
|
+
if (!isRecord(parsed))
|
|
317
|
+
throw new Error(`Invalid action JSON input ${flag}: ${errorMessage(readError)}`);
|
|
318
|
+
return parsed;
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`Unsupported action input type for ${flag}: ${declaration.type}`);
|
|
321
|
+
}
|
|
322
|
+
function taskPathContext(target, taskDir) {
|
|
323
|
+
const dir = toPosix(path.relative(target.projectRoot, taskDir));
|
|
324
|
+
return {
|
|
325
|
+
dir,
|
|
326
|
+
taskPlan: toPosix(path.join(dir, "task_plan.md")),
|
|
327
|
+
progress: toPosix(path.join(dir, "progress.md")),
|
|
328
|
+
artifacts: toPosix(path.join(dir, "artifacts")),
|
|
329
|
+
artifactsIndex: toPosix(path.join(dir, "artifacts/INDEX.md")),
|
|
330
|
+
visualMap: toPosix(path.join(dir, "visual_map.md")),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function resolveActionScopes(scopes, { target, taskPaths, label }) {
|
|
334
|
+
return (scopes || []).map((scope) => {
|
|
335
|
+
const rendered = renderHarnessTemplate(String(scope || ""), { paths: harnessPathContext(target), task: { paths: taskPaths } }, { strict: true });
|
|
336
|
+
const normalized = toPosix(path.normalize(rendered));
|
|
337
|
+
if (!rendered.trim() || path.isAbsolute(rendered) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
338
|
+
throw new Error(`${label} escapes target root: ${scope}`);
|
|
339
|
+
}
|
|
340
|
+
return normalized;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function concreteActionWriteScopes(scopes) {
|
|
344
|
+
return scopes.filter((scope) => !scope.endsWith("/**"));
|
|
345
|
+
}
|
|
346
|
+
function presetScriptEnv(contextPath) {
|
|
347
|
+
const env = { HARNESS_PRESET_CONTEXT: contextPath };
|
|
348
|
+
for (const key of ["PATH", "TMPDIR", "TEMP", "TMP", "SystemRoot", "ComSpec"]) {
|
|
349
|
+
if (process.env[key])
|
|
350
|
+
env[key] = process.env[key];
|
|
351
|
+
}
|
|
352
|
+
return env;
|
|
353
|
+
}
|
|
354
|
+
function normalizeTargetRelativePath(value, label) {
|
|
355
|
+
const raw = String(value || "").replace(/^TARGET:/, "").replace(/^\/+/, "").trim();
|
|
356
|
+
if (!raw)
|
|
357
|
+
return "";
|
|
358
|
+
const normalized = toPosix(path.normalize(raw));
|
|
359
|
+
if (path.isAbsolute(raw) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
360
|
+
throw new Error(`${label} escapes target root: ${raw}`);
|
|
361
|
+
}
|
|
362
|
+
return normalized;
|
|
363
|
+
}
|
|
364
|
+
function boundedScriptOutput(output) {
|
|
365
|
+
const redacted = String(output || "")
|
|
366
|
+
.replace(/(api[_-]?key|token|secret|password)=\S+/gi, "$1=[redacted]")
|
|
367
|
+
.replace(/\/Users\/[^/\s]+/g, "/Users/[redacted]");
|
|
368
|
+
return redacted.length > 4096 ? `${redacted.slice(0, 4096)}\n[output truncated]` : redacted;
|
|
369
|
+
}
|
|
370
|
+
function readMaterializationManifest(manifestPath) {
|
|
371
|
+
if (!fs.existsSync(manifestPath))
|
|
372
|
+
throw new Error("Preset entrypoint did not emit materialization manifest");
|
|
373
|
+
const manifest = readJsonSafe(manifestPath, null);
|
|
374
|
+
if (!isRecord(manifest))
|
|
375
|
+
throw new Error("Invalid preset materialization manifest");
|
|
376
|
+
if (manifest.schemaVersion !== materializationSchemaVersion)
|
|
377
|
+
throw new Error(`Invalid preset materialization schema: ${manifest.schemaVersion || "(missing)"}`);
|
|
378
|
+
if (!Array.isArray(manifest.writes))
|
|
379
|
+
throw new Error("Preset materialization manifest writes must be an array");
|
|
380
|
+
if (manifest.writes.length > maxMaterializedWrites)
|
|
381
|
+
throw new Error(`Preset materialization manifest has too many writes: ${manifest.writes.length}`);
|
|
382
|
+
return {
|
|
383
|
+
schemaVersion: String(manifest.schemaVersion),
|
|
384
|
+
writes: manifest.writes.map((write) => asRecord(write)),
|
|
385
|
+
status: manifest.status === undefined ? undefined : String(manifest.status),
|
|
386
|
+
publicRedactionReport: isRecord(manifest.publicRedactionReport) ? { source: String(manifest.publicRedactionReport.source || "") } : undefined,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function validateMaterializationManifest(preset, entrypoint, manifest, { outputRoot, target, entrypointName }) {
|
|
390
|
+
const targetRoot = target.projectRoot;
|
|
391
|
+
const seenDestinations = new Set();
|
|
392
|
+
const writes = manifest.writes.map((write, index) => {
|
|
393
|
+
const source = normalizeManifestRelativePath(write.source, "Manifest source");
|
|
394
|
+
const destination = normalizeManifestRelativePath(write.destination, "Manifest destination");
|
|
395
|
+
if (seenDestinations.has(destination))
|
|
396
|
+
throw new Error(`Duplicate materialization destination: ${destination}`);
|
|
397
|
+
seenDestinations.add(destination);
|
|
398
|
+
assertEntrypointWriteScope(preset, entrypoint, destination, target, entrypointName);
|
|
399
|
+
if (entrypointName && preset.actions?.[entrypointName] && isDefaultTaskGovernanceFile(destination)) {
|
|
400
|
+
throw new Error(`Preset action ${entrypointName} cannot write task governance file by default: ${destination}`);
|
|
401
|
+
}
|
|
402
|
+
const sourcePath = path.join(outputRoot, source);
|
|
403
|
+
assertOutputSource(outputRoot, sourcePath, source);
|
|
404
|
+
const stat = fs.lstatSync(sourcePath);
|
|
405
|
+
if (stat.size > maxMaterializedFileBytes)
|
|
406
|
+
throw new Error(`Manifest source exceeds size limit: ${source}`);
|
|
407
|
+
assertDestinationParent(targetRoot, destination);
|
|
408
|
+
return {
|
|
409
|
+
source,
|
|
410
|
+
sourcePath,
|
|
411
|
+
destination,
|
|
412
|
+
destinationPath: path.join(targetRoot, destination),
|
|
413
|
+
type: String(write.type || "text"),
|
|
414
|
+
visibility: String(write.visibility || ""),
|
|
415
|
+
sha256: sha256File(sourcePath),
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
enforcePublicRedaction(manifest, writes, { outputRoot });
|
|
419
|
+
return writes;
|
|
420
|
+
}
|
|
421
|
+
function normalizeManifestRelativePath(value, label) {
|
|
422
|
+
const raw = String(value || "").trim();
|
|
423
|
+
const normalized = toPosix(path.normalize(raw));
|
|
424
|
+
if (!raw || path.isAbsolute(raw) || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
425
|
+
throw new Error(`${label} escapes preset output root: ${raw || "(missing)"}`);
|
|
426
|
+
}
|
|
427
|
+
return normalized;
|
|
428
|
+
}
|
|
429
|
+
function assertOutputSource(outputRoot, sourcePath, source) {
|
|
430
|
+
if (!fs.existsSync(sourcePath))
|
|
431
|
+
throw new Error(`Manifest source missing: ${source}`);
|
|
432
|
+
const stat = fs.lstatSync(sourcePath);
|
|
433
|
+
if (stat.isSymbolicLink())
|
|
434
|
+
throw new Error(`Manifest source must not be a symlink: ${source}`);
|
|
435
|
+
if (!stat.isFile())
|
|
436
|
+
throw new Error(`Manifest source must be a file: ${source}`);
|
|
437
|
+
const realRoot = fs.realpathSync(outputRoot);
|
|
438
|
+
const realSource = fs.realpathSync(sourcePath);
|
|
439
|
+
if (!isInside(realRoot, realSource))
|
|
440
|
+
throw new Error(`Manifest source escapes preset output root: ${source}`);
|
|
441
|
+
}
|
|
442
|
+
function assertDestinationParent(targetRoot, destination) {
|
|
443
|
+
let parent = path.dirname(path.join(targetRoot, destination));
|
|
444
|
+
const realTarget = fs.realpathSync(targetRoot);
|
|
445
|
+
while (!fs.existsSync(parent) && parent !== targetRoot && parent !== path.dirname(parent))
|
|
446
|
+
parent = path.dirname(parent);
|
|
447
|
+
if (fs.existsSync(parent)) {
|
|
448
|
+
const stat = fs.lstatSync(parent);
|
|
449
|
+
if (stat.isSymbolicLink())
|
|
450
|
+
throw new Error(`Manifest destination parent must not be a symlink: ${destination}`);
|
|
451
|
+
const realParent = fs.realpathSync(parent);
|
|
452
|
+
if (!isInside(realTarget, realParent))
|
|
453
|
+
throw new Error(`Manifest destination parent escapes target root: ${destination}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function assertEntrypointWriteScope(preset, entrypoint, destination, target, entrypointName) {
|
|
457
|
+
assertPresetWriteScope(preset, destination, target);
|
|
458
|
+
const resolved = resolvePresetScopes(preset, target).entrypoints[entrypointName] || entrypoint.writes;
|
|
459
|
+
if (!resolved.some((scope) => matchesScope(scope, destination))) {
|
|
460
|
+
throw new Error(`Preset write scope violation for ${destination}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function matchesScope(scope, relativePath) {
|
|
464
|
+
const normalizedScope = toPosix(path.normalize(String(scope || "")));
|
|
465
|
+
if (normalizedScope.endsWith("/**")) {
|
|
466
|
+
const prefix = normalizedScope.slice(0, -3);
|
|
467
|
+
return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
|
|
468
|
+
}
|
|
469
|
+
return relativePath === normalizedScope;
|
|
470
|
+
}
|
|
471
|
+
function isDefaultTaskGovernanceFile(destination) {
|
|
472
|
+
return /(^|\/)(review|brief|task_plan)\.md$/.test(destination);
|
|
473
|
+
}
|
|
474
|
+
function enforcePublicRedaction(manifest, writes, { outputRoot }) {
|
|
475
|
+
const publicWrites = writes.filter((write) => write.visibility === "public" || write.destination.startsWith("docs-release/"));
|
|
476
|
+
if (publicWrites.length === 0)
|
|
477
|
+
return;
|
|
478
|
+
const reportSource = normalizeManifestRelativePath(manifest.publicRedactionReport?.source || "", "Public redaction report source");
|
|
479
|
+
const reportPath = path.join(outputRoot, reportSource);
|
|
480
|
+
assertOutputSource(outputRoot, reportPath, reportSource);
|
|
481
|
+
const report = asRecord(readJsonSafe(reportPath, null));
|
|
482
|
+
if (report.status !== "pass")
|
|
483
|
+
throw new Error("Public materialization requires a passing public redaction report");
|
|
484
|
+
}
|
|
485
|
+
function materializeWrites(targetRoot, writes) {
|
|
486
|
+
const backups = [];
|
|
487
|
+
try {
|
|
488
|
+
for (const write of writes) {
|
|
489
|
+
const destinationPath = write.destinationPath;
|
|
490
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
491
|
+
const existed = fs.existsSync(destinationPath);
|
|
492
|
+
const backupPath = existed ? `${destinationPath}.backup-${process.pid}-${crypto.randomBytes(4).toString("hex")}` : "";
|
|
493
|
+
if (existed) {
|
|
494
|
+
fs.copyFileSync(destinationPath, backupPath);
|
|
495
|
+
backups.push({ destinationPath, backupPath, existed });
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
backups.push({ destinationPath, backupPath: "", existed });
|
|
499
|
+
}
|
|
500
|
+
const tempPath = `${destinationPath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
501
|
+
fs.copyFileSync(write.sourcePath, tempPath);
|
|
502
|
+
fs.renameSync(tempPath, destinationPath);
|
|
503
|
+
}
|
|
504
|
+
for (const backup of backups) {
|
|
505
|
+
if (backup.backupPath)
|
|
506
|
+
fs.rmSync(backup.backupPath, { force: true });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
for (const backup of backups.reverse()) {
|
|
511
|
+
try {
|
|
512
|
+
if (backup.existed && backup.backupPath && fs.existsSync(backup.backupPath))
|
|
513
|
+
fs.renameSync(backup.backupPath, backup.destinationPath);
|
|
514
|
+
else if (!backup.existed)
|
|
515
|
+
fs.rmSync(backup.destinationPath, { force: true });
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Preserve the original materialization failure.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function targetSnapshot(root) {
|
|
525
|
+
const entries = new Map();
|
|
526
|
+
for (const filePath of walkFiles(root)) {
|
|
527
|
+
const relative = toPosix(path.relative(root, filePath));
|
|
528
|
+
if (relative.startsWith(".harness/locks/"))
|
|
529
|
+
continue;
|
|
530
|
+
const stat = fs.lstatSync(filePath);
|
|
531
|
+
entries.set(relative, `${stat.size}:${sha256File(filePath)}`);
|
|
532
|
+
}
|
|
533
|
+
return entries;
|
|
534
|
+
}
|
|
535
|
+
function assertSnapshotsEqual(before, after, message) {
|
|
536
|
+
const changed = [];
|
|
537
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
538
|
+
for (const item of paths) {
|
|
539
|
+
if (before.get(item) !== after.get(item))
|
|
540
|
+
changed.push(item);
|
|
541
|
+
}
|
|
542
|
+
if (changed.length)
|
|
543
|
+
throw new Error(`${message}: ${changed.slice(0, 12).join(", ")}`);
|
|
544
|
+
}
|
|
545
|
+
function sha256File(filePath) {
|
|
546
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
547
|
+
}
|
|
548
|
+
function isInside(root, candidate) {
|
|
549
|
+
const relative = path.relative(root, candidate);
|
|
550
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
551
|
+
}
|
|
552
|
+
function isRecord(value) {
|
|
553
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
554
|
+
}
|
|
555
|
+
function asRecord(value) {
|
|
556
|
+
return isRecord(value) ? value : {};
|
|
557
|
+
}
|
|
558
|
+
function errorMessage(error) {
|
|
559
|
+
return error instanceof Error ? error.message : String(error);
|
|
560
|
+
}
|