@multipliers-dev/renovate-workflow 0.0.0-stage → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Truong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,173 @@
1
- # Temporary Holding Version
1
+ # renovate-workflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Governed Renovate merge ladder with an Agent Plugins 1.0 portable core and Cursor-specific integration: classify dependency PRs, investigate high-risk changes, and merge only when repo-local policy and human gates allow.
4
+
5
+ Renovate opens many dependency PRs. This product turns that queue into a repeatable **classify → route → (investigate) → maintainer** path with explicit policy, instead of ad-hoc merges.
6
+
7
+ **Background:** [Upgrades don't have to be a blind trust exercise](https://dev.to/michaeltruong/upgrades-dont-have-to-be-a-blind-trust-exercise-13mj) (DEV Community) — the problem framing, four-step evidence checklist, and review path that led to this repository.
8
+
9
+ ```
10
+ classify (/renovate-classifier)
11
+ → route by packet
12
+ → low risk → maintainer (auto path)
13
+ → high-touch / unlisted → investigator → human audit → maintainer --approved
14
+ → optional loop (/renovate-loop) for repeated cycles
15
+ ```
16
+
17
+ **Ownership split**
18
+
19
+ | Layer | Where it lives |
20
+ | --- | --- |
21
+ | Skills, agents, runbook, portable rubric | This repo (Cursor plugin + [Agent Plugins 1.0](https://agent-plugins.org/specification) `skills/`) |
22
+ | Executable scripts (freshness poll, guardrails) | This repo (npm/git devDependency) |
23
+ | Policy facts, Renovate bot config, CI workflow | Your repo |
24
+
25
+ Portable vs Cursor manifest layers: [docs/adopt.md#portable-vs-cursor-layers-this-repo](docs/adopt.md#portable-vs-cursor-layers-this-repo).
26
+
27
+ ---
28
+
29
+ ## Install (Cursor plugin)
30
+
31
+ Cursor imports this GitHub repository as a **local marketplace**, then installs the listed plugin.
32
+
33
+ **Step 1 — import marketplace**
34
+
35
+ Customize → **Plugins** → **+ Add** → **From GitHub Repository** → `https://github.com/multipliers-dev/renovate-workflow`
36
+
37
+ Or in Agent chat:
38
+
39
+ ```text
40
+ /add-plugin https://github.com/multipliers-dev/renovate-workflow
41
+ ```
42
+
43
+ **Step 2 — install plugin**
44
+
45
+ Customize → **Plugins** → open the imported **renovate-workflow** marketplace → install the **renovate-workflow** plugin.
46
+
47
+ Full adoption guide: [docs/adopt.md](docs/adopt.md)
48
+
49
+ ---
50
+
51
+ ## Quick start (classifier only)
52
+
53
+ Minimum path to evaluate the ladder — **no `package.json` changes required**.
54
+
55
+ 1. Import marketplace (step 1 above)
56
+ 2. Install the **renovate-workflow** plugin (step 2 above)
57
+ 3. Confirm `/renovate-classifier` appears under Customize → Plugins
58
+ 4. Copy [.agents/renovate-policy.template.yml](.agents/renovate-policy.template.yml) to `.agents/renovate-policy.yml` in your repo and customize package lists, checks, and paths — see [docs/policy-setup.md](docs/policy-setup.md) and [examples/example-repo/](examples/example-repo/)
59
+ 5. Ensure GitHub access (`gh` CLI or GitHub MCP)
60
+ 6. Run `/renovate-classifier`
61
+
62
+ Success is `queue_empty` or a classification packet. The classifier **never merges**.
63
+
64
+ ---
65
+
66
+ ## Enable loop / babysit helpers
67
+
68
+ Required for `/renovate-loop --babysit` and the freshness poll CLI. Not required to try the classifier.
69
+
70
+ Add a git devDependency and script in your consumer `package.json`:
71
+
72
+ ```json
73
+ {
74
+ "devDependencies": {
75
+ "renovate-workflow": "github:multipliers-dev/renovate-workflow",
76
+ "tsx": "^4.23.12"
77
+ },
78
+ "scripts": {
79
+ "renovate:freshness-poll": "tsx node_modules/renovate-workflow/scripts/renovate-freshness-poll.ts"
80
+ }
81
+ }
82
+ ```
83
+
84
+ The `scripts/` tree in the installable package is a **temporary legacy surface** for git consumers running `tsx` on TypeScript sources. The stable npm API is the compiled `renovate-workflow` CLI (`renovate-workflow freshness-poll`); see [`.cursor/plans/2026-10-07-npm-package-distribution.plan.md`](.cursor/plans/2026-10-07-npm-package-distribution.plan.md).
85
+
86
+ See [examples/adopt-stub/package.json](examples/adopt-stub/package.json) and [docs/adopt.md](docs/adopt.md).
87
+
88
+ ---
89
+
90
+ ## What this does not do
91
+
92
+ - Fully autonomous merging — human gates remain for high-risk and investigation paths
93
+ - Scheduled or webhook-driven automation — manual invocation only
94
+ - Classifier or investigator merge authority — only maintainer may merge, and only with a valid packet
95
+ - Loop with `--approved` — investigation-approved merges require a separate maintainer step
96
+ - Replace the Renovate bot — you still configure `renovate.json` and a Renovate workflow in your repo
97
+ - Cursor public marketplace or org Team Marketplace catalog — install via GitHub import of this repo
98
+
99
+ ---
100
+
101
+ ## Documentation
102
+
103
+ Primary adopter path:
104
+
105
+ 1. [docs/adopt.md](docs/adopt.md) — plugin install + npm helpers + consumer layout
106
+ 2. [docs/policy-setup.md](docs/policy-setup.md) — consumer policy sync model
107
+ 3. [docs/renovate-workflow.md](docs/renovate-workflow.md) — operator runbook
108
+
109
+ Also:
110
+
111
+ - [Upgrades don't have to be a blind trust exercise](https://dev.to/michaeltruong/upgrades-dont-have-to-be-a-blind-trust-exercise-13mj) — background essay (DEV Community)
112
+ - [docs/versioning.md](docs/versioning.md) — version alignment and git dependency pinning
113
+ - [docs/distribution-discovery.md](docs/distribution-discovery.md) — architecture rationale (maintainer deep-dive)
114
+ - [AGENTS.md](AGENTS.md) — agent command reference
115
+
116
+ ---
117
+
118
+ ## Publishing (maintainers)
119
+
120
+ This repository uses a **two-step release** — version bump in a reviewed PR, then manual publish. Merging to `main` never publishes.
121
+
122
+ | Step | Who | What |
123
+ | --- | --- | --- |
124
+ | **A — Version bump** | Reviewed PR | Bump aligned manifests (`package.json`, `plugin.json`, `.cursor-plugin/plugin.json`). For `0.3.0`, also scope-rename to `@multipliers-dev/renovate-workflow` and remove `private`. |
125
+ | **B — Publish** | Maintainer | Dispatch [`.github/workflows/release.yml`](.github/workflows/release.yml) on `main` with confirmation `publish X.Y.Z`. |
126
+
127
+ **Before first release:**
128
+
129
+ 1. `version-bump-0.3.0` merged on `main` (scoped name, `0.3.0`, not private).
130
+ 2. **npm CLI preflight** — `sh scripts/npm-stage-cli-preflight.sh` (staged publishing requires npm **≥ 11.15.0**; Node version alone does not guarantee `npm stage` exists).
131
+ 3. **One-time registry bootstrap** — validate `0.3.0` in the checkout (`npm ci`, `npm test`, `npm run typecheck`, `npm run build`, `npm pack`), then `npm run bootstrap:tarball` to build/preflight a **disposable `0.0.1` tarball** (npm pack in an isolated temp dir — not manual `tar -czf`; canonical checkout stays at `0.3.0`), then `npm stage publish` that tarball. **Do not** stage `0.3.0` and leave it pending. See [docs/versioning.md § Trusted Publishing bootstrap](docs/versioning.md#trusted-publishing-bootstrap).
132
+ 4. Configure npm **Trusted Publisher** for workflow `release.yml` (`multipliers-dev/renovate-workflow`). See [docs/versioning.md § npm Trusted Publisher settings](docs/versioning.md#npm-trusted-publisher-settings).
133
+ 5. Dispatch **Release** within **48 hours** of creating the Trusted Publisher configuration; after successful OIDC `0.3.0`, reject the pending bootstrap stage with `npm stage reject <stage-id>` (2FA).
134
+
135
+ Publishing uses **OIDC** (no `NPM_TOKEN`). Provenance is automatic for public packages from this public repo.
136
+
137
+ The release workflow reads **name and version from the checked-out `package.json` only** (`npm publish --access public` with no CLI coordinate override). It does not bump versions or commit to `main`. npm and plugin always share version *V* at the same commit. Only one release dispatch runs at a time (`concurrency: release`).
138
+
139
+ If `npm publish` succeeds but tag/GitHub Release creation fails, **do not republish** — verify the registry version matches the intended commit, then create the missing tag and GitHub Release manually. See [docs/versioning.md § Partial release recovery](docs/versioning.md#partial-release-recovery).
140
+
141
+ Full policy: [docs/versioning.md](docs/versioning.md)
142
+
143
+ ---
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ npm install
149
+ npm test
150
+ npm run typecheck
151
+ npm run build
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Layout
157
+
158
+ ```
159
+ plugin.json Agent Plugins 1.0 portable manifest (metadata only)
160
+ skills/ Five renovate skills + verification assets (fixed discovery path)
161
+ .cursor-plugin/ Cursor marketplace + extension manifest (agents, skill paths)
162
+ .agents/ Agent prompts, rubric base, policy template, report templates
163
+ .cursor/ Repo-local Cursor dev (hooks, Cloud VM) — not plugin-distributed
164
+ docs/ Adoption guides and runbook
165
+ scripts/ Guardrails, freshness poll, tests/fixtures
166
+ examples/example-repo/ Synthetic consumer stubs for policy schema tests
167
+ ```
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { runRenovateFreshnessPollCli } from "./renovate-freshness-poll.js";
3
+ function printRootUsage() {
4
+ console.error(`Usage: renovate-workflow <command>
5
+
6
+ Commands:
7
+ freshness-poll Run the Renovate post-update babysit helper
8
+
9
+ Options:
10
+ -h, --help Show this help
11
+
12
+ Run \`renovate-workflow freshness-poll --help\` for freshness-poll options.
13
+ `);
14
+ }
15
+ async function main() {
16
+ const args = process.argv.slice(2);
17
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
18
+ printRootUsage();
19
+ process.exit(0);
20
+ }
21
+ const [command, ...rest] = args;
22
+ switch (command) {
23
+ case "freshness-poll":
24
+ await runRenovateFreshnessPollCli(rest);
25
+ return;
26
+ default:
27
+ console.error(`unknown command: ${command}`);
28
+ printRootUsage();
29
+ process.exit(2);
30
+ }
31
+ }
32
+ main().catch((error) => {
33
+ console.error(error instanceof Error ? error.message : String(error));
34
+ process.exit(1);
35
+ });
@@ -0,0 +1,100 @@
1
+ export const AGENT_PLUGINS_SCHEMA_BASE = "https://agent-plugins.org/schemas";
2
+ export const PUBLISHED_SPEC_MD_URL = "https://agent-plugins.org/specification.md";
3
+ const DECLARED_SCHEMA_PATTERN = /^https:\/\/agent-plugins\.org\/schemas\/(\d+\.\d+\.\d+)\/plugin\.schema\.json$/;
4
+ const PUBLISHED_SPEC_VERSION_PATTERN = /^\*\*Spec Version:\s*(\d+\.\d+\.\d+)\s*\*\*$/m;
5
+ const PUBLISHED_STATUS_PATTERN = /^\*\*Status:\s*Published\s*\*\*$/m;
6
+ export function buildPublishedSchemaUrl(version) {
7
+ return `${AGENT_PLUGINS_SCHEMA_BASE}/${version}/plugin.schema.json`;
8
+ }
9
+ export function parseAgentPluginsSchemaUrl(schemaUrl) {
10
+ const match = DECLARED_SCHEMA_PATTERN.exec(schemaUrl);
11
+ if (!match) {
12
+ throw new Error(`malformed Agent Plugins schema URL: expected https://agent-plugins.org/schemas/X.Y.Z/plugin.schema.json`);
13
+ }
14
+ return match[1];
15
+ }
16
+ export function parseDeclaredSpecVersion($schema) {
17
+ try {
18
+ return parseAgentPluginsSchemaUrl($schema);
19
+ }
20
+ catch {
21
+ throw new Error(`malformed $schema URL: expected https://agent-plugins.org/schemas/X.Y.Z/plugin.schema.json`);
22
+ }
23
+ }
24
+ export function parsePublishedSchemaDocumentVersion(schemaDocument) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(schemaDocument);
28
+ }
29
+ catch {
30
+ throw new Error("published schema document is not valid JSON");
31
+ }
32
+ if (typeof parsed !== "object" || parsed === null || !("$id" in parsed)) {
33
+ throw new Error("published schema document is missing $id");
34
+ }
35
+ const schemaId = parsed.$id;
36
+ if (typeof schemaId !== "string") {
37
+ throw new Error("published schema document has invalid $id");
38
+ }
39
+ return parseAgentPluginsSchemaUrl(schemaId);
40
+ }
41
+ export function parsePublishedSpecVersion(specMarkdown) {
42
+ if (!PUBLISHED_STATUS_PATTERN.test(specMarkdown)) {
43
+ throw new Error("upstream specification.md is missing Status: Published");
44
+ }
45
+ const versionMatch = PUBLISHED_SPEC_VERSION_PATTERN.exec(specMarkdown);
46
+ if (!versionMatch) {
47
+ throw new Error("upstream specification.md is missing Spec Version");
48
+ }
49
+ return versionMatch[1];
50
+ }
51
+ export function compareSemver(a, b) {
52
+ const aParts = a.split(".").map((part) => Number(part));
53
+ const bParts = b.split(".").map((part) => Number(part));
54
+ if (aParts.length !== 3 || bParts.length !== 3) {
55
+ throw new Error(`invalid semver: ${a} or ${b}`);
56
+ }
57
+ for (let index = 0; index < 3; index += 1) {
58
+ const aPart = aParts[index];
59
+ const bPart = bParts[index];
60
+ if (!Number.isInteger(aPart) || !Number.isInteger(bPart)) {
61
+ throw new Error(`invalid semver: ${a} or ${b}`);
62
+ }
63
+ if (aPart < bPart) {
64
+ return -1;
65
+ }
66
+ if (aPart > bPart) {
67
+ return 1;
68
+ }
69
+ }
70
+ return 0;
71
+ }
72
+ export function confirmPublishedUpstreamVersion(signals) {
73
+ const { markdownVersion, schemaVersion, schemaHttpStatus } = signals;
74
+ if (schemaHttpStatus !== 200) {
75
+ throw new Error(`published schema URL returned HTTP ${schemaHttpStatus} for version ${schemaVersion}`);
76
+ }
77
+ if (markdownVersion !== schemaVersion) {
78
+ throw new Error(`published signal mismatch: specification.md reports ${markdownVersion} but published schema document $id reports ${schemaVersion}`);
79
+ }
80
+ return markdownVersion;
81
+ }
82
+ export function evaluateSpecRelationship(options) {
83
+ const comparison = compareSemver(options.declared, options.published);
84
+ let relationship;
85
+ if (comparison < 0) {
86
+ relationship = "behind";
87
+ }
88
+ else if (comparison > 0) {
89
+ relationship = "ahead";
90
+ }
91
+ else {
92
+ relationship = "current";
93
+ }
94
+ return {
95
+ declaredVersion: options.declared,
96
+ latestPublishedVersion: options.published,
97
+ relationship,
98
+ driftDetected: relationship === "behind",
99
+ };
100
+ }
@@ -0,0 +1,175 @@
1
+ import { execSync } from "node:child_process";
2
+ import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ export const BOOTSTRAP_VERSION = "0.0.1";
6
+ const FORBIDDEN_ENTRY_PATTERNS = [
7
+ {
8
+ label: "directory entry package/ (trailing slash)",
9
+ test: (entry) => entry === "package/" || entry === "package",
10
+ },
11
+ {
12
+ label: "AppleDouble (._*)",
13
+ test: (entry) => /(^|\/)\._[^/]+/.test(entry),
14
+ },
15
+ {
16
+ label: ".git",
17
+ test: (entry) => entry.includes(".git"),
18
+ },
19
+ {
20
+ label: "test files",
21
+ test: (entry) => entry.endsWith(".test.ts") || entry.endsWith(".test.js"),
22
+ },
23
+ {
24
+ label: "fixtures",
25
+ test: (entry) => entry.includes("/fixtures/"),
26
+ },
27
+ {
28
+ label: "credentials",
29
+ test: (entry) => /(^|\/)\.env($|\.)/.test(entry) ||
30
+ entry.includes("credentials.json") ||
31
+ entry.includes("id_rsa"),
32
+ },
33
+ ];
34
+ export function listTarballEntries(tarballPath) {
35
+ const listing = execSync(`tar -tzf "${tarballPath}"`, { encoding: "utf8" });
36
+ return listing
37
+ .trim()
38
+ .split("\n")
39
+ .filter(Boolean);
40
+ }
41
+ export function readPackedManifest(tarballPath) {
42
+ const raw = execSync(`tar -xOf "${tarballPath}" package/package.json`, { encoding: "utf8" });
43
+ return JSON.parse(raw);
44
+ }
45
+ export function parsePackTarballName(packOutput) {
46
+ const tarballName = packOutput
47
+ .split(/\r?\n/)
48
+ .map((line) => line.trim())
49
+ .filter((line) => line.endsWith(".tgz"))
50
+ .at(-1);
51
+ if (!tarballName) {
52
+ throw new Error(`npm pack did not report a .tgz filename:\n${packOutput}`);
53
+ }
54
+ return tarballName;
55
+ }
56
+ export function assertBootstrapTarballEntries(entries) {
57
+ const violations = [];
58
+ for (const entry of entries) {
59
+ for (const pattern of FORBIDDEN_ENTRY_PATTERNS) {
60
+ if (pattern.test(entry)) {
61
+ violations.push(`${entry} (${pattern.label})`);
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ if (violations.length > 0) {
67
+ throw new Error(`bootstrap tarball failed preflight:\n${violations.map((v) => ` - ${v}`).join("\n")}`);
68
+ }
69
+ const normalized = entries.map((entry) => entry.replace(/^package\//, ""));
70
+ if (!normalized.some((entry) => entry.startsWith("dist/"))) {
71
+ throw new Error("bootstrap tarball missing dist/ publish surface");
72
+ }
73
+ if (!normalized.includes("package.json")) {
74
+ throw new Error("bootstrap tarball missing package/package.json");
75
+ }
76
+ if (!normalized.includes("README.md")) {
77
+ throw new Error("bootstrap tarball missing package/README.md");
78
+ }
79
+ }
80
+ /** Lifecycle hooks that reference dev-only paths excluded from the packed `files` list. */
81
+ const ISOLATED_PACK_SCRIPT_KEYS = ["prepare", "prepack"];
82
+ function stripIsolatedPackLifecycleScripts(packageDir) {
83
+ const packageJsonPath = join(packageDir, "package.json");
84
+ const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
85
+ if (!manifest.scripts) {
86
+ return;
87
+ }
88
+ for (const key of ISOLATED_PACK_SCRIPT_KEYS) {
89
+ delete manifest.scripts[key];
90
+ }
91
+ writeFileSync(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
92
+ }
93
+ function packDirectoryToDestination(packageDir, packDestination) {
94
+ mkdirSync(packDestination, { recursive: true });
95
+ const packOutput = execSync(`npm pack --ignore-scripts --loglevel error --pack-destination "${packDestination}"`, {
96
+ cwd: packageDir,
97
+ encoding: "utf8",
98
+ stdio: ["ignore", "pipe", "pipe"],
99
+ });
100
+ const packedName = parsePackTarballName(packOutput);
101
+ return join(packDestination, packedName);
102
+ }
103
+ /**
104
+ * Build a disposable bootstrap tarball from an already-validated `npm pack` artifact.
105
+ *
106
+ * Uses `npm pack --ignore-scripts` in an isolated temp directory so the tarball
107
+ * matches libnpmpack layout (file entries only under package/..., no bare package/
108
+ * directory entry). Manual `tar -czf` is rejected by the npm registry (E415 invalid
109
+ * path: package/) because BSD tar emits a trailing-slash directory entry.
110
+ */
111
+ export function createBootstrapTarballFromValidatedPack(validatedTarballPath, options = {}) {
112
+ const absoluteValidated = resolve(validatedTarballPath);
113
+ if (!existsSync(absoluteValidated)) {
114
+ throw new Error(`validated pack tarball not found: ${absoluteValidated}`);
115
+ }
116
+ const workRoot = mkdtempSync(join(tmpdir(), "renovate-workflow-bootstrap-work-"));
117
+ const extractDir = join(workRoot, "extract");
118
+ const isolatedPackageDir = join(workRoot, "isolated-package");
119
+ // Default: durable artifact dir outside workRoot so the returned .tgz survives work cleanup.
120
+ const artifactDir = options.outputDir
121
+ ? resolve(options.outputDir)
122
+ : mkdtempSync(join(tmpdir(), "renovate-workflow-bootstrap-artifact-"));
123
+ try {
124
+ mkdirSync(extractDir, { recursive: true });
125
+ cpSync(absoluteValidated, join(workRoot, "validated.tgz"));
126
+ execSync(`tar -xzf "${join(workRoot, "validated.tgz")}" -C "${extractDir}"`, {
127
+ stdio: "pipe",
128
+ });
129
+ const extractedPackageDir = join(extractDir, "package");
130
+ if (!existsSync(join(extractedPackageDir, "package.json"))) {
131
+ throw new Error("validated pack tarball missing package/package.json");
132
+ }
133
+ cpSync(extractedPackageDir, isolatedPackageDir, { recursive: true });
134
+ execSync(`npm pkg set version=${BOOTSTRAP_VERSION}`, {
135
+ cwd: isolatedPackageDir,
136
+ stdio: "pipe",
137
+ });
138
+ // Packed artifact omits scripts/*.sh; npm pack may still run `prepare` unless removed.
139
+ stripIsolatedPackLifecycleScripts(isolatedPackageDir);
140
+ const packDir = join(workRoot, "packed");
141
+ const packedPath = packDirectoryToDestination(isolatedPackageDir, packDir);
142
+ const packedName = basename(packedPath);
143
+ const finalPath = join(artifactDir, packedName);
144
+ copyFileSync(packedPath, finalPath);
145
+ const entries = listTarballEntries(finalPath);
146
+ assertBootstrapTarballEntries(entries);
147
+ const manifest = readPackedManifest(finalPath);
148
+ if (manifest.version !== BOOTSTRAP_VERSION) {
149
+ throw new Error(`bootstrap tarball version must be ${BOOTSTRAP_VERSION}, got ${manifest.version}`);
150
+ }
151
+ return { tarballPath: finalPath, entries, manifest };
152
+ }
153
+ finally {
154
+ if (!options.keepWorkdirs) {
155
+ rmSync(workRoot, { recursive: true, force: true });
156
+ }
157
+ }
158
+ }
159
+ export function packValidatedReleaseArtifact(repoRoot) {
160
+ const packDir = mkdtempSync(join(tmpdir(), "renovate-workflow-validated-pack-"));
161
+ return packDirectoryToDestination(repoRoot, packDir);
162
+ }
163
+ export function removePackedArtifactTree(tarballPath) {
164
+ rmSync(dirname(tarballPath), { recursive: true, force: true });
165
+ }
166
+ /** Remove the temp directory that owns a default-path bootstrap tarball. */
167
+ export function removeBootstrapArtifactTree(tarballPath) {
168
+ rmSync(dirname(tarballPath), { recursive: true, force: true });
169
+ }
170
+ export function assertCheckoutVersion(repoRoot, expectedVersion) {
171
+ const manifest = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
172
+ if (manifest.version !== expectedVersion) {
173
+ throw new Error(`Expected package version ${expectedVersion}, got ${manifest.version}`);
174
+ }
175
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Single derivation table for classifier emission and guardrails evaluation.
3
+ * Keys must match the reference table in the renovate investigation lane plan.
4
+ */
5
+ export const TRIGGER_WATCH_KEYS = [
6
+ "implementation_changes_required",
7
+ "lockfile_threshold_exceeded",
8
+ "ci_failure_unexplained",
9
+ "runtime_behavior_affected",
10
+ ];
11
+ export const KNOWN_STOP_CAUSES = [
12
+ "decision_human_required",
13
+ "stop_flag_expected_human_required",
14
+ "triggered_implementation_changes_required",
15
+ "triggered_lockfile_threshold_exceeded",
16
+ "triggered_ci_failure_unexplained",
17
+ "triggered_runtime_behavior_affected",
18
+ "triggered_runtime_behavior_affected_sole",
19
+ "triggered_human_required_unmapped",
20
+ "decision_defer",
21
+ ];
22
+ const KNOWN_STOP_CAUSE_SET = new Set(KNOWN_STOP_CAUSES);
23
+ const TRIGGER_WATCH_KEY_SET = new Set(TRIGGER_WATCH_KEYS);
24
+ export function isKnownStopCause(value) {
25
+ return KNOWN_STOP_CAUSE_SET.has(value);
26
+ }
27
+ function deriveTriggerStopCauses(triggered) {
28
+ const causes = [];
29
+ if (triggered.some((key) => !TRIGGER_WATCH_KEY_SET.has(key))) {
30
+ causes.push("triggered_human_required_unmapped");
31
+ }
32
+ if (triggered.includes("implementation_changes_required")) {
33
+ causes.push("triggered_implementation_changes_required");
34
+ }
35
+ if (triggered.includes("lockfile_threshold_exceeded")) {
36
+ causes.push("triggered_lockfile_threshold_exceeded");
37
+ }
38
+ if (triggered.includes("ci_failure_unexplained")) {
39
+ causes.push("triggered_ci_failure_unexplained");
40
+ }
41
+ if (triggered.includes("runtime_behavior_affected")) {
42
+ if (triggered.length === 1) {
43
+ causes.push("triggered_runtime_behavior_affected_sole");
44
+ }
45
+ else {
46
+ causes.push("triggered_runtime_behavior_affected");
47
+ }
48
+ }
49
+ return causes;
50
+ }
51
+ export function deriveStopCauses(packet) {
52
+ const causes = [];
53
+ const decision = packet.classification?.decision;
54
+ const triggered = packet.triggered_human_required ?? [];
55
+ if (decision === "defer") {
56
+ causes.push("decision_defer");
57
+ }
58
+ if (decision === "human_required") {
59
+ causes.push("decision_human_required");
60
+ }
61
+ if (packet.stop === true && decision === "human_required") {
62
+ causes.push("stop_flag_expected_human_required");
63
+ }
64
+ causes.push(...deriveTriggerStopCauses(triggered));
65
+ return causes;
66
+ }
67
+ export function stopCausesMatchDerivation(packet) {
68
+ const expectedCauses = deriveStopCauses(packet);
69
+ const packetCauses = packet.stop_causes ?? [];
70
+ if (packetCauses.length !== expectedCauses.length) {
71
+ return false;
72
+ }
73
+ const sortedPacketCauses = [...packetCauses].sort();
74
+ const sortedExpectedCauses = [...expectedCauses].sort();
75
+ return sortedPacketCauses.every((cause, index) => cause === sortedExpectedCauses[index]);
76
+ }
77
+ export function validateStopCausesDerivation(packet) {
78
+ if (packet.stop !== true) {
79
+ return null;
80
+ }
81
+ if (!stopCausesMatchDerivation(packet)) {
82
+ return "packet stop_causes do not match deriveStopCauses derivation";
83
+ }
84
+ return null;
85
+ }