From 3a5b8df807d0157222ad9e843333de849a4b01ed Mon Sep 17 00:00:00 2001 From: cortex-agent Date: Sat, 10 Oct 2026 13:36:08 +0800 Subject: [PATCH] fix(release): stop bypassing the L3 boundary gate when publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1eb3d98 added an author-local absolute path to templates/{en,zh}/.agent/ workflows/briefing.md. The distribution gate introduced nine days earlier in 79f3677 never fired for it. The cause is not the gate. gitCommitAndTag() commits with `--no-verify`, which disables every pre-commit hook, and local-publish-validate is the framework's own release path. The gate was installed, active and correct the whole time — it was simply never consulted on the one path that publishes. The boundary checker now runs explicitly before that commit. `--no-verify` is left in place because removing it would also re-enable the memory-integrity hard-fail on a release commit, which is not what this change is about. The check sits in step 3, ahead of npm pack in step 4 and volta install in step 5, so a failure exits before anything is packed or installed. The briefing.md leak is removed as well. Its parenthetical pointed at lib/commands/doctor.js by absolute path; lib/ is not shipped, so the reference was unusable to a reader and only leaked the author's machine layout. The line now credits the user-facing `cortex-agent doctor --json` shown directly above it. Also completes the frontmatter templates/en/.agent/workflows/briefing.md was missing, which the pre-commit gate rejects on any commit that stages the file. That debt predates this change and the zh copy already carried the fields. Co-Authored-By: Claude --- bin/local-publish-validate.cjs | 18 +++ templates/en/.agent/workflows/briefing.md | 8 +- templates/zh/.agent/workflows/briefing.md | 2 +- .../local-publish-validate-l3-gate.test.js | 114 ++++++++++++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/commands/local-publish-validate-l3-gate.test.js diff --git a/bin/local-publish-validate.cjs b/bin/local-publish-validate.cjs index b1b7b01d..5b20385b 100644 --- a/bin/local-publish-validate.cjs +++ b/bin/local-publish-validate.cjs @@ -169,6 +169,24 @@ function gitCommitAndTag(cwd, version, message, opts) { return; } runInherit('git', ['add', '-u'], cwd, opts); + // `--no-verify` below disables every pre-commit hook, including the L3 + // distribution boundary gate. Publishing is precisely when that gate must not + // be skipped: it is the last point before templates/ reaches every managed + // project. Run it explicitly so the release path cannot ship L3 content that + // belongs in /.agent/ only. + const gateScript = path.join(cwd, 'scripts', 'check-l3-boundary.js'); + if (fs.existsSync(gateScript)) { + const gateRun = spawnSync(process.execPath, [gateScript], { + cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], + }); + if (gateRun.status !== 0) { + process.stdout.write(gateRun.stdout || ''); + process.stderr.write(gateRun.stderr || ''); + console.error('❌ L3 distribution boundary check failed; refusing to commit and tag.'); + console.error(' L3 内容只能留在 /.agent/(标记 scope: L3),不得进入 templates/。'); + process.exit(1); + } + } runInherit('git', ['commit', '-m', message, '--no-verify'], cwd, opts); runInherit('git', ['tag', '-a', `v${version}`, '-m', `v${version}`], cwd, opts); } diff --git a/templates/en/.agent/workflows/briefing.md b/templates/en/.agent/workflows/briefing.md index a5ab2eba..4f27bb01 100644 --- a/templates/en/.agent/workflows/briefing.md +++ b/templates/en/.agent/workflows/briefing.md @@ -1,6 +1,12 @@ --- name: briefing description: Produce a read-only project briefing covering progress, active work, Decisions, Waitpoints, Inbox, and recommended next actions. +type: procedure +applicable_to: + - all +owner: Kucell +last_verified: 2026-10-10 +status: stable --- # Project Briefing Workflow (/briefing) @@ -57,7 +63,7 @@ Read-only probe of the latest published version. Never auto-upgrades. cortex-agent doctor --json ``` -`available_update` field contract (`/Users/xueyq/myworks/cortex-agent/lib/commands/doctor.js`): +`available_update` field contract (emitted by `cortex-agent doctor --json`): - `status`: `"current"` / `"outdated"` / `"unknown"` (registry unreachable) - `current`: CLI version (mirrors `package.json`) diff --git a/templates/zh/.agent/workflows/briefing.md b/templates/zh/.agent/workflows/briefing.md index 21a9449e..fe37c7b8 100644 --- a/templates/zh/.agent/workflows/briefing.md +++ b/templates/zh/.agent/workflows/briefing.md @@ -61,7 +61,7 @@ node .agent/skills/runtime-continuity/scripts/index.js resume-bundle --project " cortex-agent doctor --json ``` -`available_update` 字段契约(`/Users/xueyq/myworks/cortex-agent/lib/commands/doctor.js`): +`available_update` 字段契约(由 `cortex-agent doctor --json` 输出): - `status`: `"current"` / `"outdated"` / `"unknown"`(网络/registry 不可达) - `current`: 当前 CLI 版本(与 package.json 一致) diff --git a/tests/commands/local-publish-validate-l3-gate.test.js b/tests/commands/local-publish-validate-l3-gate.test.js new file mode 100644 index 00000000..d7b08c83 --- /dev/null +++ b/tests/commands/local-publish-validate-l3-gate.test.js @@ -0,0 +1,114 @@ +"use strict"; + +// ─── local-publish-validate must not publish past the L3 boundary gate ──────── +// +// `gitCommitAndTag()` commits with `--no-verify`, which disables every +// pre-commit hook — including the L3 distribution boundary gate added in +// 79f3677. Publishing is exactly when that gate must hold: it is the last point +// before templates/ reaches every managed project. +// +// A real leak reached main this way: 1eb3d98 added an author-local absolute +// path to templates/{en,zh}/.agent/workflows/briefing.md and the hook never +// fired, because the commit went through this script. +// +// The gate runs in step 3, before npm pack (step 4) and volta install (step 5), +// so a failure exits before anything is packed or installed. These tests +// therefore run the real script against a throwaway git repo and assert that +// nothing was committed, packed or installed. + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); +const { test, describe, beforeEach, afterEach } = require("node:test"); + +const ROOT = path.resolve(__dirname, "..", ".."); +const SCRIPT = path.join(ROOT, "bin", "local-publish-validate.cjs"); + +let repo; + +function git(args) { + return spawnSync("git", ["-C", repo, ...args], { encoding: "utf8" }); +} + +function writeStubGate(body) { + const dir = path.join(repo, "scripts"); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "check-l3-boundary.js"), body); +} + +beforeEach(() => { + repo = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-lpv-l3-")); + git(["init", "-q"]); + git(["config", "user.email", "lpv@test"]); + git(["config", "user.name", "lpv"]); + fs.writeFileSync( + path.join(repo, "package.json"), + JSON.stringify({ name: "lpv-l3-fixture", version: "1.0.0", private: true }), + ); + fs.mkdirSync(path.join(repo, "templates", "x"), { recursive: true }); + git(["add", "."]); + git(["commit", "-q", "-m", "init"]); +}); + +afterEach(() => fs.rmSync(repo, { recursive: true, force: true })); + +function run() { + return spawnSync(process.execPath, [SCRIPT, "--force", "--skip-tests"], { + cwd: repo, + encoding: "utf8", + }); +} + +describe("local-publish-validate — L3 boundary gate before commit", () => { + test("a failing gate blocks the commit and tags nothing", () => { + writeStubGate('process.stdout.write("violation found\\n"); process.exit(1);'); + const before = git(["rev-parse", "HEAD"]).stdout.trim(); + + const r = run(); + + assert.notEqual(r.status, 0, "the script must fail when the gate fails"); + assert.match(r.stdout + r.stderr, /L3 distribution boundary check failed/); + assert.equal(git(["rev-parse", "HEAD"]).stdout.trim(), before, "HEAD must not advance"); + assert.equal(git(["tag"]).stdout.trim(), "", "no tag may be created"); + }); + + test("a failing gate stops before npm pack", () => { + writeStubGate("process.exit(1);"); + const r = run(); + assert.doesNotMatch(r.stdout, /npm pack/, "pack must not run once the gate fails"); + assert.doesNotMatch(r.stdout, /volta install/, "install must not run once the gate fails"); + }); + + test("the gate is invoked at all", () => { + // A marker file proves the script actually executed the checker rather + // than merely carrying the code path. + writeStubGate( + 'require("node:fs").writeFileSync(process.env.LPV_MARKER, "ran");\n' + + "process.exit(1);" + ); + const marker = path.join(repo, "gate-ran.marker"); + spawnSync(process.execPath, [SCRIPT, "--force", "--skip-tests"], { + cwd: repo, + encoding: "utf8", + env: { ...process.env, LPV_MARKER: marker }, + }); + assert.ok(fs.existsSync(marker), "the boundary checker must be executed"); + }); + + test("the commit still passes --no-verify, which is why the explicit gate exists", () => { + const src = fs.readFileSync(SCRIPT, "utf8"); + assert.match( + src, + /'commit', '-m', message, '--no-verify'/, + "the --no-verify call is expected; the guard above it is what must hold", + ); + const gateIdx = src.indexOf("check-l3-boundary.js"); + const commitIdx = src.indexOf("'--no-verify'"); + assert.ok( + gateIdx !== -1 && gateIdx < commitIdx, + "the boundary gate must be checked before the commit that skips hooks", + ); + }); +}); \ No newline at end of file