Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .clagentic/loadout/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# .clagentic/loadout/config.yaml — clagentic-console repo-local loadout config
# (lr-0d45: migration off the dead .crew/naomi.yaml merge path onto the LIVE
# loadout-merge path; see merge/verb.py's post_merge_config.load_post_merge_steps,
# which reads ONLY this file's merge: section — .crew/naomi.yaml is never read by
# loadout-merge).
#
# Authored directly from clagentic_loadout's starter template
# (src/clagentic_loadout/loadout_init/starter_config.yaml) rather than through the
# interactive /loadout-init elicitation flow -- that skill is a live back-and-forth
# with an operator and this session has no such channel; the dispatching lead
# (holden, lr-0d45) already supplied this repo's real merge-gate values in the
# task brief, which is what step 2's elicitation would otherwise collect. No
# deployment-specific HOME or forge-URL value is present here (post_merge_steps
# below runs a repo-local npm script and a repo-local node script only -- no
# machine value needed).
#
# Mirrors clagentic-console's actual live merge policy as of this migration:
# CI (pr-checks.yml) must pass, tests must pass, PEACHES (reviewer) and BOBBIE
# (security) must both post a clean verdict, NAOMI (merger) holds merge authority.

roles:
builder:
- push
reviewer:
- git-host-api
- review-post
- stage-body
security:
- git-host-api
- review-post
merger:
- merge
- push
- release-dispatch
- release-detect
lead:
- git-host-api

merge:
merge_requirements:
# ci_pass: true -- pr-checks.yml (GitHub Actions) runs syntax + import-
# resolution checks on every PR and gates merges today (mirrors
# .crew/naomi.yaml's own ci_pass: true).
ci_pass: true
tests_pass: true

# required_reviewer_roles: BOTH a clean PEACHES (reviewer) verdict and a
# clean BOBBIE (security) verdict are required before a merge is
# authorized -- clagentic-console's live gate today per this task's
# dispatch brief.
required_reviewer_roles:
- reviewer
- security

authorized_roles:
- merger

# post_merge_steps: runs in THIS repo's own working tree after loadout-merge
# merges a PR here. Two ordered steps (never one shell-operator string --
# loadout-merge's cmd parser rejects &&/||/|/;/>/< tokens in a plain cmd
# string at parse time; the `sh -c '...'` wrapper below is one invocation, no
# top-level shell operator, so it is fine as-is).
#
# Step 1 replaces npm run install:local-test's old .crew/naomi.yaml
# on_failure: warn with on_failure: fail -- a failed post-merge global
# install must now fail the merge loudly (EXIT_POST_MERGE_FAILED=28,
# PostMergeStepFailedError), not pass silently.
#
# Step 2 is new: asserts the just-installed @clagentic/console build
# actually reflects the merged commit, rather than trusting npm install -g's
# zero exit code alone. package.json's version field is npm-semver
# (1.7.0-beta.1), not a git SHA, so it cannot answer "did this install land
# the commit that was just merged" -- there was no build-identity signal in
# the package before this migration. scripts/write-build-sha.js (wired as
# npm's prepack lifecycle hook) now embeds the current git HEAD SHA into
# lib/build-sha.json at pack time, so it travels inside the tarball
# install:local-test builds and installs. scripts/verify-installed-build.js
# reads that installed build-sha.json and compares it against this tree's
# own HEAD SHA (the merged commit, since post_merge_steps runs after the
# merge), exiting non-zero on any mismatch.
post_merge_steps:
- cmd: "npm run install:local-test"
description: "Install latest build globally for developer test (npm pack + npm install -g). Test-tier only -- not production."
on_failure: fail
- cmd: "npm run verify:installed-build"
description: "Assert the globally-installed build's embedded SHA (lib/build-sha.json, written at npm prepack time) matches this repo's merged HEAD."
on_failure: fail
1 change: 1 addition & 0 deletions .crew/amos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ scope:
- "bin/**"
- "lib/**"
- ".crew/**"
- ".clagentic/**"
- "test/**"
- "deploy/**"
- "scripts/**"
Expand Down
29 changes: 12 additions & 17 deletions .crew/naomi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,15 @@ avoid:
- "Do not merge PRs that touch bin/cli.js or lib/ without green CI"
- "Do not merge if PEACHES review is absent or blocking"

# post_merge_steps — ordered list of steps NAOMI runs after a successful merge.
#
# install:local-test was added in PR #289 (main). It packs the repo into a
# tarball, installs it globally with `npm install -g`, and removes the tarball.
# This is a developer/test-tier operation, not a production deploy. It is
# intentionally on_failure: warn — a failed local test-install must not roll
# back or block a merge that already cleared all CI and review gates. The
# install touches the operator's global npm environment, which may vary; a
# failure here is surfaced for awareness but is not a correctness signal about
# the merged code itself.
post_merge_steps:
- cmd: "npm run install:local-test"
description: "Install latest build globally for developer test (npm pack + npm install -g). Test-tier only — not production."
# on_failure: warn — local test-install failure must not block or roll back
# a merge that already passed tests, CI, and PEACHES. Environment variance
# (global npm state, disk space, platform) should not gate code quality.
on_failure: warn
# post_merge_steps — REMOVED here (lr-0d45). This key was DEAD CONFIG: NAOMI's
# live merge path for this repo is loadout-merge (crew_merge.py's GitHub path
# is hard-refused, EXIT_GITHUB_PATH_DEPRECATED, operator ruling lr-d9729d,
# 2026-07-13), and loadout-merge reads post_merge_steps EXCLUSIVELY from
# `<repo_root>/.clagentic/loadout/config.yaml`'s `merge:` section
# (post_merge_config.load_post_merge_steps in clagentic-loadout) — it never
# reads this file. Leaving a post_merge_steps key here risked being read as a
# second, stale source of truth for a step nothing actually executes from this
# location. The equivalent (and now hardened — on_failure: fail, plus a new
# build-SHA verification step) post_merge_steps now lives at
# .clagentic/loadout/config.yaml's merge.post_merge_steps. See that file's own
# header comment for the full migration rationale.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
node_modules/
lib/certs/*.pem
# lib/build-sha.json is a generated pack-time artifact (scripts/write-build-sha.js,
# npm's prepack lifecycle hook) — never hand-authored, never committed. It ships in
# published tarballs because it's written before `npm pack` runs, not because it's
# tracked in git (lr-0d45).
lib/build-sha.json
.claude/
.claude-local/
.claude-relay/
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"scripts": {
"dev": "node bin/cli.js --dev",
"postinstall": "node scripts/postinstall.js",
"prepack": "node scripts/write-build-sha.js",
"test": "node --test --test-force-exit test/*.test.js",
"install:local-test": "sh -c 'f=$(npm pack) && npm install -g \"$f\" && rm -f \"$f\"'",
"verify:installed-build": "node scripts/verify-installed-build.js",
"semantic-release": "semantic-release"
},
"files": [
Expand Down
80 changes: 80 additions & 0 deletions scripts/verify-installed-build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
'use strict';

// verify-installed-build.js — post-merge install assertion (lr-0d45).
//
// Confirms the globally-installed @clagentic/console build (installed by the
// preceding `npm run install:local-test` post_merge_steps entry) actually
// reflects the merged HEAD commit, rather than trusting a zero exit code from
// `npm install -g` alone. Compares the SHA embedded by scripts/write-build-sha.js
// (baked into the tarball at `prepack` time, so it travels with the package
// that `npm install -g` unpacked) against this working tree's own HEAD SHA at
// the moment loadout-merge runs post_merge_steps (i.e. the merged commit).
//
// Exit codes:
// 0 — installed build-sha.json matches this tree's HEAD.
// 1 — mismatch, or build-sha.json missing/unreadable, or HEAD unresolvable.
// Reported on stderr with both SHAs so the exact drift is visible in
// loadout-merge's captured step output.
//
// Run as a SEPARATE post_merge_steps entry (on_failure: fail) after
// `npm run install:local-test` — never combined into one shell string, per
// loadout-merge's shell-operator-token rejection (merge/post_merge.py).

const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

function resolveHeadSha() {
return execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: path.join(__dirname, '..'),
stdio: ['ignore', 'pipe', 'pipe'],
}).toString().trim();
}

function resolveInstalledSha() {
// `npm install -g` resolves the global node_modules root via `npm root -g`;
// read the build-sha.json that write-build-sha.js baked into the tarball
// at pack time (lib/ is in package.json's `files`, so it ships).
const globalRoot = execFileSync('npm', ['root', '-g'], {
stdio: ['ignore', 'pipe', 'pipe'],
}).toString().trim();
const installedPath = path.join(globalRoot, '@clagentic', 'console', 'lib', 'build-sha.json');
const raw = fs.readFileSync(installedPath, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed.sha !== 'string') {
throw new Error(`${installedPath} does not contain a "sha" field`);
}
return { sha: parsed.sha, path: installedPath };
}

function main() {
let headSha;
try {
headSha = resolveHeadSha();
} catch (err) {
console.error(`[verify-installed-build] ERROR: could not resolve this tree's HEAD SHA: ${err.message}`);
process.exit(1);
}

let installed;
try {
installed = resolveInstalledSha();
} catch (err) {
console.error(`[verify-installed-build] ERROR: could not read installed build-sha.json: ${err.message}`);
process.exit(1);
}

if (installed.sha !== headSha) {
console.error(
`[verify-installed-build] MISMATCH: installed build (${installed.path}) is at ` +
`${installed.sha}, but merged HEAD is ${headSha}. The post-merge install did ` +
`not land the merged commit — investigate before trusting the running build.`
);
process.exit(1);
}

console.log(`[verify-installed-build] ok: installed build matches merged HEAD (${headSha})`);
}

main();
53 changes: 53 additions & 0 deletions scripts/write-build-sha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env node
'use strict';

// write-build-sha.js — embeds the current git commit SHA into lib/build-sha.json
// so a later install of this package can be checked against the source tree it
// was built from (lr-0d45: post-merge install verification).
//
// Runs as npm's `prepack` lifecycle hook — fires before `npm pack` builds the
// tarball, so `lib/build-sha.json` is always baked into what install:local-test
// ships. Also runs standalone via `node scripts/write-build-sha.js` for local
// debugging.
//
// Fails loudly (non-zero exit) if the SHA cannot be determined — a package
// packed without a resolvable SHA would make the post-install verification
// step (scripts/verify-installed-build.js) meaningless, so refuse to produce
// a stale/empty build-sha.json rather than pack one silently.

const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

const OUT_PATH = path.join(__dirname, '..', 'lib', 'build-sha.json');

function resolveSha() {
try {
return execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: path.join(__dirname, '..'),
stdio: ['ignore', 'pipe', 'pipe'],
}).toString().trim();
} catch (err) {
const stderr = err.stderr ? err.stderr.toString().trim() : err.message;
throw new Error(`could not resolve git HEAD SHA: ${stderr}`);
}
}

function main() {
let sha;
try {
sha = resolveSha();
} catch (err) {
console.error(`[write-build-sha] ERROR: ${err.message}`);
process.exit(1);
}
if (!/^[0-9a-f]{40}$/i.test(sha)) {
console.error(`[write-build-sha] ERROR: resolved SHA does not look like a git commit hash: ${JSON.stringify(sha)}`);
process.exit(1);
}
const payload = { sha, writtenAt: new Date().toISOString() };
fs.writeFileSync(OUT_PATH, JSON.stringify(payload, null, 2) + '\n');
console.log(`[write-build-sha] wrote ${OUT_PATH} (sha=${sha})`);
}

main();
Loading