From 8a881fc6e7e830ee5bba22b4068883add58d91be Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:00:45 -0700 Subject: [PATCH 1/5] fix(release): sync package-lock.json via script, not release-please extra-files release-please's extra-files JSON-path updater didn't reach package-lock.json's per-workspace version fields (keys containing "/", nested under a manifest-mode component block) -- confirmed empirically twice: the engine-v0.2.0 dry run's package-lock.json stayed at 0.1.0 after package.json bumped to 0.2.0, breaking npm ci with "Missing: @jsonbored/gittensory-engine@0.1.0 from lock file" on every downstream job (Build UI preview artifact confirmed; Workers Builds: gittensory-ui likely the same root cause manifesting differently). Replaces it with scripts/sync-release-lockfile-versions.mjs -- the same single-line string replacement proven correct by hand for both packages earlier in this release cycle, not a full JSON.parse/stringify round-trip on a multi-thousand-line file. A new workflow step runs it directly against whichever release-please branch(es) exist (release-please--branches--main-- components--, the same naming convention release-please's own commits already rely on), commits if anything changed, before the tag/ publish-dispatch steps that follow. Removed the two now-dead extra-files blocks from release-please-config.json. --- .github/workflows/mcp-release-please.yml | 33 +++++++++++++++++ release-please-config.json | 18 +-------- scripts/sync-release-lockfile-versions.mjs | 43 ++++++++++++++++++++++ 3 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 scripts/sync-release-lockfile-versions.mjs diff --git a/.github/workflows/mcp-release-please.yml b/.github/workflows/mcp-release-please.yml index 2b3b0c17c3..3145169bab 100644 --- a/.github/workflows/mcp-release-please.yml +++ b/.github/workflows/mcp-release-please.yml @@ -52,6 +52,39 @@ jobs: manifest-file: .release-please-manifest.json token: ${{ secrets.GITHUB_TOKEN }} + # release-please's extra-files JSON-path updater doesn't reliably reach package-lock.json's + # per-workspace version fields (keys containing "/", nested under a manifest-mode component + # block) -- confirmed empirically (both the mcp-v0.7.0 dry run's precursor and the first two + # engine-v0.2.0 dry runs left it stale, breaking npm ci with "Missing: @ from + # lock file"). Patches it directly on whichever release branch(es) release-please just + # created/updated, using the same branch naming convention its own commits already rely on. + - name: Sync package-lock.json on any release branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" + gh auth setup-git + for component in mcp engine; do + branch="release-please--branches--main--components--${component}" + if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "No release branch for $component, skipping." + continue + fi + git fetch origin "$branch" + git checkout -B "sync-check-${component}" "origin/$branch" + node scripts/sync-release-lockfile-versions.mjs packages/gittensory-mcp packages/gittensory-engine + if git diff --quiet package-lock.json; then + echo "package-lock.json already in sync on $branch." + else + git add package-lock.json + git commit -m "chore(release): sync package-lock.json" + git push origin "HEAD:$branch" + fi + done + # release-please creates the component tag + GitHub Release with GITHUB_TOKEN, which (by # GitHub's recursion-prevention rule) does NOT fire push/tag-based workflows. workflow_dispatch # IS exempt from that rule, so dispatch the OIDC publish workflow explicitly, telling it diff --git a/release-please-config.json b/release-please-config.json index de61be1350..0583fc66ba 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,26 +4,12 @@ "packages/gittensory-mcp": { "release-type": "node", "component": "mcp", - "package-name": "@jsonbored/gittensory-mcp", - "extra-files": [ - { - "type": "json", - "path": "package-lock.json", - "jsonpath": "$.packages['packages/gittensory-mcp'].version" - } - ] + "package-name": "@jsonbored/gittensory-mcp" }, "packages/gittensory-engine": { "release-type": "node", "component": "engine", - "package-name": "@jsonbored/gittensory-engine", - "extra-files": [ - { - "type": "json", - "path": "package-lock.json", - "jsonpath": "$.packages['packages/gittensory-engine'].version" - } - ] + "package-name": "@jsonbored/gittensory-engine" } }, "include-component-in-tag": true, diff --git a/scripts/sync-release-lockfile-versions.mjs b/scripts/sync-release-lockfile-versions.mjs new file mode 100644 index 0000000000..cb7c0712c0 --- /dev/null +++ b/scripts/sync-release-lockfile-versions.mjs @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// release-please's `extra-files` JSON-path updater doesn't reliably reach package-lock.json's +// per-workspace version fields, whose keys contain slashes (e.g. "packages/gittensory-engine") +// nested under a manifest-mode component's own release-please-config.json block -- confirmed +// empirically (mcp-v0.7.0/engine-v0.2.0 dry runs both left package-lock.json un-synced, breaking +// `npm ci` with "Missing: @jsonbored/gittensory-engine@0.1.0 from lock file"). This does the same +// single-line replacement a human would make by hand: find the workspace's own manifest-mirror +// entry, replace just its "version" value. No JSON.parse/stringify round-trip on the whole +// multi-thousand-line lockfile, which would risk reordering/reformatting far beyond the one line +// that actually changed. +import { readFileSync, writeFileSync } from "node:fs"; + +const targets = process.argv.slice(2); +if (targets.length === 0) { + console.error("Usage: node sync-release-lockfile-versions.mjs [ ...]"); + process.exit(1); +} + +const lockPath = "package-lock.json"; +let content = readFileSync(lockPath, "utf8"); +let changed = false; + +for (const workspacePath of targets) { + const version = JSON.parse(readFileSync(`${workspacePath}/package.json`, "utf8")).version; + const escapedKey = workspacePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + // Anchors on the workspace's own block header + its "name" line (both stable, unique) so this + // can't accidentally match a different package's "version" line elsewhere in the file. + const pattern = new RegExp(`("${escapedKey}":\\s*\\{\\s*\\n\\s*"name":[^\\n]*\\n\\s*"version":\\s*")[^"]*(")`); + if (!pattern.test(content)) { + console.error(`${workspacePath}: pattern not found in ${lockPath} -- nothing changed.`); + continue; + } + const updated = content.replace(pattern, `$1${version}$2`); + if (updated === content) { + console.log(`${workspacePath}: already at ${version}.`); + } else { + content = updated; + changed = true; + console.log(`${workspacePath}: synced to ${version}.`); + } +} + +if (changed) writeFileSync(lockPath, content); From 811f4d23cc4ac2f8913f1277d2b6750a43317773 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:07:03 -0700 Subject: [PATCH 2/5] fix: widen gittensory-engine dependency ranges past caret's 0.x ceiling Found while validating the lockfile-sync fix on a real dry run: semver caret ranges for 0.x versions only allow patch bumps (^0.1.0 means >=0.1.0 <0.2.0), so gittensory-mcp's "^0.1.0" and gittensory-miner's exact "0.1.0" pin both break on gittensory-engine's very first real release (0.1.0 -> 0.2.0, reproduced live on the engine-v0.2.0 dry-run branch -- npm ci failed with "Missing: @jsonbored/gittensory-engine@0.1.0 from lock file"). Widened both to ">=0.1.0" -- these are internal, same-monorepo dependencies whose real compatibility guarantee comes from this repo's own test suite, not from strict semver-range enforcement against a pre-1.0 package expected to move through minor versions often. --- package-lock.json | 4 ++-- packages/gittensory-mcp/package.json | 2 +- packages/gittensory-miner/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98c7ab69dc..7e44535e23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15899,7 +15899,7 @@ "version": "0.7.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": "^0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, @@ -15915,7 +15915,7 @@ "version": "0.1.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": "0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0" }, "bin": { "gittensory-miner": "bin/gittensory-miner.js" diff --git a/packages/gittensory-mcp/package.json b/packages/gittensory-mcp/package.json index 20ffe8a77f..a151506f8a 100644 --- a/packages/gittensory-mcp/package.json +++ b/packages/gittensory-mcp/package.json @@ -38,7 +38,7 @@ "build": "node --check bin/gittensory-mcp.js && node --check lib/local-branch.js && node --check scripts/gittensor-score-preview.mjs" }, "dependencies": { - "@jsonbored/gittensory-engine": "^0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index fa11aa7ad3..5edb6da7f3 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -34,7 +34,7 @@ "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js" }, "dependencies": { - "@jsonbored/gittensory-engine": "0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0" }, "engines": { "node": ">=22.13.0" From 099cdc7d76f4b15994b1db3c80349208c870212a Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:19:15 -0700 Subject: [PATCH 3/5] fix(engine): derive ENGINE_VERSION instead of hand-syncing it Same class of bug as the two fixes already in this PR, caught by the same engine-v0.2.0 dry-run branch's CI: ENGINE_VERSION was a hardcoded literal "0.1.0" that test/unit/engine-version.test.ts correctly asserts must match package.json -- release-please bumped package.json to 0.2.0 but had no way to touch this hand-synced constant, so the (already-correct, unchanged) test failed exactly as designed. Derives ENGINE_VERSION from package.json at runtime instead (works identically from src/version.ts under vitest and the compiled dist/version.js, since package.json is the direct parent of both directories either way). URL imported explicitly from node:url rather than relying on the ambient global -- they've subtly diverged in this @types/node version (Symbol.dispose on URLSearchParamsIterator), which fileURLToPath's overloads reject otherwise. Also drops gittensory-engine-scaffold.test.ts's hardcoded version literal in favor of a format check, for the same self-updating reason as this session's earlier gittensory-mcp test fixes. Verified genuinely self-updating the same way: bumped package.json to 0.2.0, rebuilt, confirmed both tests pass unmodified; reverted. --- packages/gittensory-engine/src/version.ts | 19 ++++++++++++++----- test/unit/gittensory-engine-scaffold.test.ts | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/gittensory-engine/src/version.ts b/packages/gittensory-engine/src/version.ts index be6f0b62b7..600d231e01 100644 --- a/packages/gittensory-engine/src/version.ts +++ b/packages/gittensory-engine/src/version.ts @@ -1,5 +1,14 @@ -/** - * Published semver of `@jsonbored/gittensory-engine`. Keep in lockstep with `package.json` `version` - * (enforced by `test/unit/engine-version.test.ts`). - */ -export const ENGINE_VERSION = "0.1.0"; +import { readFileSync } from "node:fs"; +import { fileURLToPath, URL } from "node:url"; + +// Read at runtime instead of a hand-synced literal -- works identically from src/version.ts +// (source, under vitest) and dist/version.js (compiled output): package.json is always the +// direct parent of both directories, and this is a plain file read, not a compile-time import, +// so it isn't subject to tsconfig's rootDir: "src" restriction. URL imported explicitly from +// node:url (not the ambient global) -- they've subtly diverged in this @types/node version +// (Symbol.dispose on URLSearchParamsIterator), which fileURLToPath's overloads reject otherwise. +const ownPackageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url)); +const ownPackageJson = JSON.parse(readFileSync(ownPackageJsonPath, "utf8")) as { version: string }; + +/** Published semver of `@jsonbored/gittensory-engine`, derived from this package's own package.json. */ +export const ENGINE_VERSION: string = ownPackageJson.version; diff --git a/test/unit/gittensory-engine-scaffold.test.ts b/test/unit/gittensory-engine-scaffold.test.ts index 5307c6ca31..b7da0b3fbb 100644 --- a/test/unit/gittensory-engine-scaffold.test.ts +++ b/test/unit/gittensory-engine-scaffold.test.ts @@ -7,7 +7,7 @@ import enginePkg from "../../packages/gittensory-engine/package.json"; describe("gittensory-engine package scaffold", () => { it("declares the published package identity", () => { expect(enginePkg.name).toBe("@jsonbored/gittensory-engine"); - expect(enginePkg.version).toBe("0.1.0"); + expect(enginePkg.version).toMatch(/^\d+\.\d+\.\d+$/); expect(enginePkg.type).toBe("module"); expect(enginePkg.license).toBe("AGPL-3.0-only"); expect(enginePkg.publishConfig?.access).toBe("public"); From 299d4e9770670dd18904bd7a686a022ed025e1ad Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:33:00 -0700 Subject: [PATCH 4/5] fix(engine): use a compile-time JSON import, not readFileSync, for ENGINE_VERSION The previous commit's readFileSync(fileURLToPath(...)) approach broke the Workers runtime test suite: packages/gittensory-engine gets bundled into the Cloudflare Worker itself (src/mcp/find-opportunities.ts -> gittensory- miner's opportunity-fanout.js -> @jsonbored/gittensory-engine), and the workerd sandbox (both the real deployment and vitest-pool-workers' emulation of it) has no real filesystem for an arbitrary readFileSync to read from -- reproduced live: "ENOENT: no such file or directory ... packages/gittensory-engine/package.json" from inside test/workers/worker-runtime.test.ts. Switched to `import ownPackageJson from "../package.json" with { type: "json" }` instead -- the same mechanism src/services/mcp-compatibility.ts already uses for the same reason. tsc preserves this as a real import statement in the compiled dist/version.js (Node's own ESM loader resolves it correctly for real npm installs, where package.json is genuinely present on disk), while the Worker's own esbuild-based bundling step inlines the JSON content directly into the bundle at build time -- no runtime file I/O in that path at all. The earlier rootDir: "src" concern that motivated readFileSync in the first place turned out to be unfounded: tsc compiled the direct import without complaint. Re-verified everything from the previous commit still holds (typecheck, engine-version.test.ts, gittensory-engine-scaffold.test.ts, full engine suite, self-updating stress test at a bumped version) plus the previously-broken test/workers/worker-runtime.test.ts now passes. --- packages/gittensory-engine/src/version.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/gittensory-engine/src/version.ts b/packages/gittensory-engine/src/version.ts index 600d231e01..2a4c8fbbab 100644 --- a/packages/gittensory-engine/src/version.ts +++ b/packages/gittensory-engine/src/version.ts @@ -1,14 +1,4 @@ -import { readFileSync } from "node:fs"; -import { fileURLToPath, URL } from "node:url"; - -// Read at runtime instead of a hand-synced literal -- works identically from src/version.ts -// (source, under vitest) and dist/version.js (compiled output): package.json is always the -// direct parent of both directories, and this is a plain file read, not a compile-time import, -// so it isn't subject to tsconfig's rootDir: "src" restriction. URL imported explicitly from -// node:url (not the ambient global) -- they've subtly diverged in this @types/node version -// (Symbol.dispose on URLSearchParamsIterator), which fileURLToPath's overloads reject otherwise. -const ownPackageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url)); -const ownPackageJson = JSON.parse(readFileSync(ownPackageJsonPath, "utf8")) as { version: string }; +import ownPackageJson from "../package.json" with { type: "json" }; /** Published semver of `@jsonbored/gittensory-engine`, derived from this package's own package.json. */ export const ENGINE_VERSION: string = ownPackageJson.version; From 59ee1120102a0349dc84e8ab232224358b567f8d Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:36:20 -0700 Subject: [PATCH 5/5] fix(release): cap gittensory-engine dependency ranges below 1.0.0 Superagent correctly flagged >=0.1.0 as overly permissive (P2): unbounded above, it would silently accept a future major-version bump too -- including a compromised or dependency-confusion-attacked release -- with no explicit review. Its suggested fix (^0.2.0) isn't right either: that reintroduces the exact bug this range change was fixing (caret ranges for 0.x versions only span patches, so it would break again on the very next minor release, e.g. 0.3.0). The correct middle ground has both a floor and a ceiling: ">=0.1.0 <1.0.0" tolerates every current and future 0.x release (verified via node's semver package: 0.1.0/0.2.0/0.99.0 all satisfy it) while still rejecting anything at or above 1.0.0 -- a deliberate stability milestone that should require a real, reviewed dependency bump (and at which point a normal ^1.0.0 caret becomes the right long-term range anyway). --- package-lock.json | 4 ++-- packages/gittensory-mcp/package.json | 2 +- packages/gittensory-miner/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e44535e23..7ac44d3355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15899,7 +15899,7 @@ "version": "0.7.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": ">=0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, @@ -15915,7 +15915,7 @@ "version": "0.1.0", "license": "AGPL-3.0-only", "dependencies": { - "@jsonbored/gittensory-engine": ">=0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0" }, "bin": { "gittensory-miner": "bin/gittensory-miner.js" diff --git a/packages/gittensory-mcp/package.json b/packages/gittensory-mcp/package.json index a151506f8a..f623d7bca7 100644 --- a/packages/gittensory-mcp/package.json +++ b/packages/gittensory-mcp/package.json @@ -38,7 +38,7 @@ "build": "node --check bin/gittensory-mcp.js && node --check lib/local-branch.js && node --check scripts/gittensor-score-preview.mjs" }, "dependencies": { - "@jsonbored/gittensory-engine": ">=0.1.0", + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0", "@modelcontextprotocol/sdk": "1.29.0", "zod": "^4.4.3" }, diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index 5edb6da7f3..f05833fcc8 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -34,7 +34,7 @@ "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/event-ledger-cli.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/plan-store-cli.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/governor-ledger-cli.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js && node --check lib/laptop-init.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/calibration-types.js && node --check lib/calibration.js" }, "dependencies": { - "@jsonbored/gittensory-engine": ">=0.1.0" + "@jsonbored/gittensory-engine": ">=0.1.0 <1.0.0" }, "engines": { "node": ">=22.13.0"