From 7410cf57d9e6abaabe056b9b7cc0cf492cea3cb6 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 7 Nov 2026 16:28:20 -0500 Subject: [PATCH 1/4] feat(inspect): ask for the tier when run has no --tier - interactive terminal: T0/T1 menu, re-asks up to 3 times, Ctrl-D stops - no terminal, and start always: E_TIER_REQUIRED (exit 2) names --tier - a T1 pick is not Gate B consent; a T0 pick records Gate B by prompt - --tier with no value is a usage error --- inspect/src/cli/main.mjs | 17 +++- inspect/src/cli/run.mjs | 5 +- inspect/src/cli/tier.mjs | 41 +++++++++ inspect/src/io/tty.mjs | 53 +++++++++-- inspect/test/cli/check-share.test.mjs | 4 +- inspect/test/cli/tier-prompt.test.mjs | 122 ++++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 inspect/src/cli/tier.mjs create mode 100644 inspect/test/cli/tier-prompt.test.mjs diff --git a/inspect/src/cli/main.mjs b/inspect/src/cli/main.mjs index 51783a71..005e1cb0 100644 --- a/inspect/src/cli/main.mjs +++ b/inspect/src/cli/main.mjs @@ -6,9 +6,10 @@ import { purgeCommand } from './purge.mjs'; import { checkShareCommand } from './checkshare.mjs'; import { GRANT_ENV, resumeCommand, startCommand, statusCommand } from './background.mjs'; import { TOOL_VERSION } from '../scorer/score.mjs'; +import { askTier, tierRequired } from './tier.mjs'; const USAGE = `usage: - shrine-inspect run --tier T0|T1 [--no-history] [--session-id ] [--json] + shrine-inspect run [--tier T0|T1] [--no-history] [--session-id ] [--json] shrine-inspect start --tier T0|T1 [--no-history] [--session-id ] [--json] shrine-inspect status [] [--json] shrine-inspect approve [choice] [--background] @@ -17,6 +18,7 @@ const USAGE = `usage: shrine-inspect purge [--json] shrine-inspect version shrine-inspect --help +run with no --tier asks for one in an interactive terminal; start always needs --tier. Any command takes --skill-version : a mismatch with this CLI stops before any read.`; // Every command, for the skill test: the skill names only these. @@ -45,7 +47,7 @@ export function parseArgs(argv) { out.sessionId = a.includes('=') ? a.slice(13) : argv[++i]; if (!out.sessionId || !/^[A-Za-z0-9][\w-]*$/.test(out.sessionId)) throw new InspectError('E_USAGE', 'bad --session-id', { hint: USAGE, exit: EXIT.USAGE }); } - else if (a === '--tier') out.tier = argv[++i]; + else if (a === '--tier') out.tier = value(++i, '--tier'); else if (a.startsWith('--tier=')) out.tier = a.slice(7); else if (a.startsWith('--')) throw new InspectError('E_USAGE', `unknown option ${a}`, { hint: USAGE, exit: EXIT.USAGE }); else out.positional.push(a); @@ -81,8 +83,15 @@ export async function main(argv, io) { io.stdout.write(json ? `${JSON.stringify({ version: TOOL_VERSION })}\n` : `shrine-inspect ${TOOL_VERSION}\n`); return EXIT.OK; } - if (args.cmd === 'run') return (await runCommand(args, cio)).code; - if (args.cmd === 'start') return startCommand(args, cio, { createRun }).code; + if (args.cmd === 'run') { + // No --tier: a person picks one in their terminal; the pick is not Gate B consent. + const opts = args.tier === null ? { ...args, tier: await askTier(cio), tierBy: 'prompt' } : args; + return (await runCommand(opts, cio)).code; + } + if (args.cmd === 'start') { + if (args.tier === null) throw tierRequired('start'); + return startCommand(args, cio, { createRun }).code; + } if (args.cmd === 'status') return statusCommand({ runId: args.positional[0] }, cio, deps).code; if (args.cmd === 'resume') return (await resumeCommand({ runId: args.positional[0], worker: args.worker, grant: args.worker ? grant ?? null : null }, cio, deps)).code; if (args.cmd === 'approve') { diff --git a/inspect/src/cli/run.mjs b/inspect/src/cli/run.mjs index 4af1087d..9c4dcffa 100644 --- a/inspect/src/cli/run.mjs +++ b/inspect/src/cli/run.mjs @@ -85,8 +85,9 @@ export function createRun(opts, io) { // The sample seed, drawn once per run (T1 only) and printed in the report footer. // io.seed is an in-process test seam (the bin never sets it). seed: opts.tier === 'T1' ? (Number.isInteger(io.seed) ? io.seed : newSeed()) : null, - // Gate B: --tier T0 sets it; --tier T1 asks after the scan, T0 pre-selected. - gates: { A: opts.noHistory ? { choice: 'no-history', by: 'flag', at } : null, B: opts.tier === 'T0' ? { choice: 'T0', by: 'flag', at } : null }, + // Gate B: T0 (by --tier, or picked at the tier prompt) sets it; T1 asks + // after the scan, T0 pre-selected. A T1 pick never sets it. + gates: { A: opts.noHistory ? { choice: 'no-history', by: 'flag', at } : null, B: opts.tier === 'T0' ? { choice: 'T0', by: opts.tierBy ?? 'flag', at } : null }, }; folder.writeJson('state.json', state); return { folder, ctx, state }; diff --git a/inspect/src/cli/tier.mjs b/inspect/src/cli/tier.mjs new file mode 100644 index 00000000..55e1ffdf --- /dev/null +++ b/inspect/src/cli/tier.mjs @@ -0,0 +1,41 @@ +// `run` with no --tier: asks which tier, only in an interactive terminal (the +// same check as the gates). The answer picks the tier only; it is never Gate B +// consent, so a T1 run still stops at Gate A and Gate B as with --tier T1. +// `start`, piped stdin, and skills get a usage error that names the flag: no +// silent default, no hang. +import { askUntil, isTerminal } from '../io/tty.mjs'; +import { InspectError, EXIT } from './errors.mjs'; + +export const TIER_MENU = [ + 'Pick a tier for this run:', + ' T0: scan only. No model calls, nothing leaves your machine.', + ' T1: T0 plus up to 12 judged episodes, sent with your own Claude Code login. Gate B shows the estimate before anything is sent.', + 'Picking T1 here is not consent: Gate B still asks for a typed yes.', +]; +const QUESTION = 'Type T0 or T1: '; +const ATTEMPTS = 3; + +export const tierRequired = (cmd) => + new InspectError('E_TIER_REQUIRED', `${cmd} needs --tier T0|T1 here`, { + hint: cmd === 'start' + ? 'start runs in the background, so it never asks: shrine-inspect start --tier T0' + : 'the tier is asked only in an interactive terminal; else pass it: shrine-inspect run --tier T0', + exit: EXIT.USAGE, + }); + +// Returns 'T0' or 'T1'. Throws E_TIER_REQUIRED with no terminal, and +// E_TIER_NOT_CHOSEN on Ctrl-D or after ATTEMPTS wrong answers. +export async function askTier(io) { + if (!isTerminal(io.stdin)) throw tierRequired('run'); + io.stderr.write(`${TIER_MENU.join('\n')}\n`); + const tier = await askUntil(QUESTION, (a) => (/^t[01]$/i.test(a) ? a.toUpperCase() : null), { + input: io.stdin, + output: io.stderr, + attempts: ATTEMPTS, + again: (a) => (a ? `not ${a}.` : 'no answer.'), + }); + if (!tier) { + throw new InspectError('E_TIER_NOT_CHOSEN', 'no tier chosen; no run was created', { hint: 'run again and type T0 or T1, or pass --tier T0|T1', exit: EXIT.USAGE }); + } + return tier; +} diff --git a/inspect/src/io/tty.mjs b/inspect/src/io/tty.mjs index 868d49d8..460399c3 100644 --- a/inspect/src/io/tty.mjs +++ b/inspect/src/io/tty.mjs @@ -3,13 +3,17 @@ import { createInterface } from 'node:readline/promises'; import { InspectError, EXIT } from '../cli/errors.mjs'; +// The one interactive-terminal check: every question this CLI asks uses it. +export const isTerminal = (input) => Boolean(input?.isTTY); + +const notTty = () => + new InspectError('E_NOT_TTY', 'gates are answered only in your own terminal', { + hint: 'run `shrine-inspect approve ` in an interactive terminal', + exit: EXIT.GATE_WAITING, + }); + export async function prompt(question, { input = process.stdin, output = process.stderr } = {}) { - if (!input.isTTY) { - throw new InspectError('E_NOT_TTY', 'gates are answered only in your own terminal', { - hint: 'run `shrine-inspect approve ` in an interactive terminal', - exit: EXIT.GATE_WAITING, - }); - } + if (!isTerminal(input)) throw notTty(); // Silence is no answer: input that already ended, or ends (Ctrl-D) before a // line, resolves to '' so the caller leaves the gate paused. A pending // question on an ended stream would otherwise never settle. @@ -28,3 +32,40 @@ export async function prompt(question, { input = process.stdin, output = process rl.close(); } } + +// Asks until accept(answer) returns a value, at most `attempts` times, on one +// readline so no typed line is lost between asks. Returns null when input ends +// (Ctrl-D) or no answer was accepted. Same terminal rule as prompt. +export async function askUntil(question, accept, { input = process.stdin, output = process.stderr, attempts = 3, again = (a) => a } = {}) { + if (!isTerminal(input)) throw notTty(); + if (input.readableEnded || input.destroyed) { + output.write(`${question}\n`); + return null; + } + const rl = createInterface({ input, output }); + const queued = []; + let waiting = null; + let ended = false; + rl.on('line', (l) => (waiting ? waiting(l.trim()) : queued.push(l.trim()))); + rl.once('close', () => { + ended = true; + if (waiting) waiting(null); + }); + const next = () => (queued.length ? queued.shift() : ended ? null : new Promise((r) => (waiting = (v) => ((waiting = null), r(v))))); + try { + for (let i = 0; i < attempts; i++) { + output.write(question); + const answer = await next(); + if (answer === null) { + output.write('\n'); + return null; + } + const v = accept(answer); + if (v) return v; + output.write(`${again(answer)}\n`); + } + return null; + } finally { + rl.close(); + } +} diff --git a/inspect/test/cli/check-share.test.mjs b/inspect/test/cli/check-share.test.mjs index f56a4f44..3bfad234 100644 --- a/inspect/test/cli/check-share.test.mjs +++ b/inspect/test/cli/check-share.test.mjs @@ -63,7 +63,9 @@ test('bin --help exits 0 as a child process', () => { const state = mkdtempSync(join(tmpdir(), 'si-state-')); const r = spawnSync(process.execPath, [BIN, '--help'], { env: env(h, state), encoding: 'utf8' }); assert.equal(r.status, 0, r.stderr); - assert.match(r.stdout, /shrine-inspect run --tier/); + assert.match(r.stdout, /shrine-inspect run \[--tier T0\|T1\]/); + assert.match(r.stdout, /shrine-inspect start --tier T0\|T1/); + assert.match(r.stdout, /no --tier asks for one in an interactive terminal/); h.cleanup(); }); diff --git a/inspect/test/cli/tier-prompt.test.mjs b/inspect/test/cli/tier-prompt.test.mjs new file mode 100644 index 00000000..39be280b --- /dev/null +++ b/inspect/test/cli/tier-prompt.test.mjs @@ -0,0 +1,122 @@ +// `run` with no --tier asks for one in an interactive terminal, and only there. +// Anywhere else (piped stdin, `start`, a skill) it stops with a usage error that +// names the flag: never a silent default, never a hang. Picking T1 here is not +// Gate B consent. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, mkdtempSync, readdirSync, readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Readable } from 'node:stream'; +import { fileURLToPath } from 'node:url'; +import { main, parseArgs } from '../../src/cli/main.mjs'; +import { makeHome, FIXED_NOW } from '../synth/home.mjs'; + +const BIN = fileURLToPath(new URL('../../bin/shrine-inspect.mjs', import.meta.url)); + +function sink() { + const chunks = []; + return { write: (s) => chunks.push(String(s)), text: () => chunks.join('') }; +} +// A terminal that types these lines, then ends (Ctrl-D). +function tty(...lines) { + const s = Readable.from(lines.map((l) => `${l}\n`)); + s.isTTY = true; + return s; +} +function setup() { + const h = makeHome('good'); + const state = mkdtempSync(join(tmpdir(), 'si-state-')); + const env = { ...process.env, SHRINE_INSPECT_STATE_DIR: join(state, 'state'), SHRINE_INSPECT_CACHE_DIR: join(state, 'cache'), SHRINE_INSPECT_NOW: FIXED_NOW, SHRINE_INSPECT_TEMP_PATTERNS: JSON.stringify(['^/nowhere/']), HOME: h.home }; + return { h, state, env }; +} +async function inproc(argv, { h, env }, stdin) { + const stdout = sink(); + const stderr = sink(); + const code = await main(argv, { env, home: h.home, stdin, stdout, stderr }); + return { code, stdout: stdout.text(), stderr: stderr.text() }; +} +const runs = (state) => (existsSync(join(state, 'state', 'runs')) ? readdirSync(join(state, 'state', 'runs')) : []); +const stateOf = (state) => JSON.parse(readFileSync(join(state, 'state', 'runs', runs(state)[0], 'state.json'), 'utf8')); + +test('run with no --tier and no terminal: usage error naming --tier, no run created', async () => { + const s = setup(); + const r = await inproc(['run', '--json'], s, { isTTY: false }); + assert.equal(r.code, 2); + const err = JSON.parse(r.stderr).error; + assert.equal(err.code, 'E_TIER_REQUIRED'); + assert.match(`${err.message} ${err.hint}`, /--tier T0\|T1/); + assert.deepEqual(runs(s.state), []); + s.h.cleanup(); +}); + +test('start with no --tier is refused even in a terminal: no worker, no run', async () => { + const s = setup(); + const r = await inproc(['start'], s, tty('T0')); + assert.equal(r.code, 2); + assert.match(r.stderr, /--tier T0\|T1/); + assert.doesNotMatch(r.stderr, /Pick a tier/); + assert.deepEqual(runs(s.state), []); + s.h.cleanup(); +}); + +test('piped stdin to the bin is refused at once, never read as a tier', () => { + const s = setup(); + const r = spawnSync(process.execPath, [BIN, 'run', '--no-history'], { env: s.env, input: 'T0\n', encoding: 'utf8', timeout: 20_000 }); + assert.equal(r.status, 2); + assert.match(r.stderr, /--tier T0\|T1/); + assert.deepEqual(runs(s.state), []); + s.h.cleanup(); +}); + +test('terminal: the menu names both tiers; t0 (any case) runs the scan only', async () => { + const s = setup(); + const r = await inproc(['run', '--no-history', '--json'], s, tty('t0')); + assert.equal(r.code, 0, r.stderr); + assert.match(r.stderr, /T0: scan only/); + assert.match(r.stderr, /T1: /); + assert.match(r.stderr, /Gate B/); + const st = stateOf(s.state); + assert.equal(st.tier, 'T0'); + assert.equal(st.gates.B.choice, 'T0'); + assert.equal(st.gates.B.by, 'prompt'); + s.h.cleanup(); +}); + +test('terminal: a wrong answer asks again; T1 is not Gate B consent', async () => { + const s = setup(); + // T1, then the Gate A question gets no answer (input ends): the run pauses. + const r = await inproc(['run'], s, tty('T2', 'T1')); + assert.equal(r.code, 3, r.stderr); + assert.match(r.stderr, /not T2|T0 or T1/); + assert.equal((r.stderr.match(/Type T0 or T1/g) ?? []).length, 2); + const st = stateOf(s.state); + assert.equal(st.tier, 'T1'); + assert.equal(st.gates.B, null); + s.h.cleanup(); +}); + +test('terminal: three wrong or empty answers stop with no run created', async () => { + const s = setup(); + const r = await inproc(['run', '--json'], s, tty('', 'yes', 'T2', 'T1')); + assert.equal(r.code, 2); + assert.equal(JSON.parse(r.stderr.trim().split('\n').at(-1)).error.code, 'E_TIER_NOT_CHOSEN'); + assert.deepEqual(runs(s.state), []); + s.h.cleanup(); +}); + +test('terminal: input that ends (Ctrl-D) stops at once with no run created', async () => { + const s = setup(); + const r = await inproc(['run'], s, tty()); + assert.equal(r.code, 2); + assert.match(r.stderr, /no tier chosen/); + assert.equal((r.stderr.match(/Type T0 or T1/g) ?? []).length, 1); + assert.deepEqual(runs(s.state), []); + s.h.cleanup(); +}); + +test('--tier with no value is a usage error, not a prompt', () => { + assert.throws(() => parseArgs(['run', '--tier']), (e) => e.code === 'E_USAGE'); + assert.throws(() => parseArgs(['run', '--tier', '--json']), (e) => e.code === 'E_USAGE'); +}); From 1c7b079de120fa2fb42c1efe362fd2390e071045 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 7 Nov 2026 16:29:24 -0500 Subject: [PATCH 2/4] docs(inspect): document the tier prompt and required --tier for start --- docs/src/content/docs/guide/inspect.mdx | 6 ++++++ inspect/README.md | 6 +++++- inspect/TRIAL.md | 9 +++++++++ inspect/skill/skills/shrine-inspect/SKILL.md | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/guide/inspect.mdx b/docs/src/content/docs/guide/inspect.mdx index 3078b370..e24782ec 100644 --- a/docs/src/content/docs/guide/inspect.mdx +++ b/docs/src/content/docs/guide/inspect.mdx @@ -106,6 +106,12 @@ It checks that it changed nothing it read. If anything changed, the report shows shrine-inspect run --tier T0 --no-history ``` +- Not sure which tier? Leave out `--tier`; in an interactive terminal it asks `Type T0 or T1:`. Picking T1 there is not consent: Gate B still asks for a typed `yes`. Elsewhere, and always for `start`, it stops (exit 2) and names `--tier T0|T1` + + ```sh + shrine-inspect run + ``` + - T1 run: type `history` at Gate A, then `yes` at Gate B after reading its estimate ```sh diff --git a/inspect/README.md b/inspect/README.md index 850c5bdf..48c1c6d7 100644 --- a/inspect/README.md +++ b/inspect/README.md @@ -15,6 +15,10 @@ Status: pre-release trial (background runs, a thin skill, a checksummed release) ## Run - `node bin/shrine-inspect.mjs run --tier T0 --no-history`: setup and repos only +- `run` with no `--tier` prints a T0/T1 menu and asks `Type T0 or T1:` (case-insensitive) in an interactive terminal; there is no default + - 3 wrong or empty answers, or Ctrl-D, stop with `E_TIER_NOT_CHOSEN` (exit 2) and create no run + - No terminal (piped stdin, a skill, a worker): `E_TIER_REQUIRED` (exit 2), naming `--tier T0|T1`; nothing is read from stdin + - A T1 pick is not Gate B consent; Gate A and Gate B ask as with `--tier T1` - Without `--no-history`, the run pauses at Gate A (exit 3) until you answer in your own terminal: `node bin/shrine-inspect.mjs approve A ` - Gate A lists projects, session files with the dates they last changed, size on disk, and a size-based time estimate; every run asks again - The report counts sessions, not files: the appendix says how many files were forks with nothing new, this run's own session, unreadable, or housekeeping @@ -59,7 +63,7 @@ Status: pre-release trial (background runs, a thin skill, a checksummed release) ## Background runs -- `shrine-inspect start --tier T0|T1 [--session-id ]`: creates the run and continues it in a detached worker, so closing the terminal does not stop it; prints the run id, pid, and log +- `shrine-inspect start --tier T0|T1 [--session-id ]`: creates the run and continues it in a detached worker, so closing the terminal does not stop it; prints the run id, pid, and log; `--tier` is required (`E_TIER_REQUIRED`, exit 2), since `start` never asks - The worker has no terminal, so it pauses at every gate; it never answers one - `shrine-inspect status [] [--json]`: `running` (with judging progress), `gate` (with the exact `approve` line to type), `done` (report paths), `died` (no worker, no report), or `stopped` - `approve [choice] --background`: you answer in your terminal, then a worker does the rest diff --git a/inspect/TRIAL.md b/inspect/TRIAL.md index a7818fb0..120928df 100644 --- a/inspect/TRIAL.md +++ b/inspect/TRIAL.md @@ -79,6 +79,15 @@ A local health check of your Claude Code setup and session history, measured aga shrine-inspect run --tier T0 ``` +- Without `--tier`, `run` asks which tier, then goes on as if you had passed it: + + ``` + Type T0 or T1: + ``` + + - It asks only in an interactive terminal. Anywhere else, and always for `start`, it stops (exit 2, `E_TIER_REQUIRED`) and names `--tier T0|T1`. + - Picking T1 there is not consent. Gate B still asks for a typed `yes`. + - It stops at Gate A and asks whether to read your session history too: ``` diff --git a/inspect/skill/skills/shrine-inspect/SKILL.md b/inspect/skill/skills/shrine-inspect/SKILL.md index e44c923e..cb382ac6 100644 --- a/inspect/skill/skills/shrine-inspect/SKILL.md +++ b/inspect/skill/skills/shrine-inspect/SKILL.md @@ -36,7 +36,7 @@ This skill only starts the `shrine-inspect` CLI and watches it. The CLI does eve If the command is not found, or it stops with `E_VERSION`, stop. Tell the user to install the CLI and this skill from the same release (see TRIAL.md in the release). -3. Start a run in the background. Use `--tier T1` only when the user asked for judging ("Pulse"); else use `--tier T0`. Session id: `${CLAUDE_SESSION_ID}`. If that id is empty or still shows as a placeholder, leave out `--session-id` and its value. Never leave `--session-id` with nothing after it. +3. Start a run in the background. Use `--tier T1` only when the user asked for judging ("Pulse"); else use `--tier T0`. Always pass `--tier`: without it, `start` stops with `E_TIER_REQUIRED`. Session id: `${CLAUDE_SESSION_ID}`. If that id is empty or still shows as a placeholder, leave out `--session-id` and its value. Never leave `--session-id` with nothing after it. ```bash shrine-inspect start --tier T0 --session-id ${CLAUDE_SESSION_ID} --skill-version 0.1.3 --json From 5120d2037c894ee82398338c57237af42a22a3f7 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 7 Nov 2026 16:30:53 -0500 Subject: [PATCH 3/4] refactor(inspect): say the tier prompt uses the gates' terminal check --- inspect/src/io/tty.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inspect/src/io/tty.mjs b/inspect/src/io/tty.mjs index 460399c3..6c84bf71 100644 --- a/inspect/src/io/tty.mjs +++ b/inspect/src/io/tty.mjs @@ -3,7 +3,7 @@ import { createInterface } from 'node:readline/promises'; import { InspectError, EXIT } from '../cli/errors.mjs'; -// The one interactive-terminal check: every question this CLI asks uses it. +// The interactive-terminal check, the same `stdin.isTTY` test the gates use. export const isTerminal = (input) => Boolean(input?.isTTY); const notTty = () => From cf9f41d655cda7d1eed1afd2f231a24515532b3e Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 7 Nov 2026 20:54:47 -0500 Subject: [PATCH 4/4] chore(inspect): version 0.1.4 --- docs/src/content/docs/guide/inspect.mdx | 8 ++++---- inspect/TRIAL.md | 16 ++++++++-------- inspect/package-lock.json | 4 ++-- inspect/package.json | 2 +- inspect/skill/.claude-plugin/plugin.json | 2 +- inspect/skill/skills/shrine-inspect/SKILL.md | 8 ++++---- inspect/src/scorer/score.mjs | 2 +- inspect/test/golden/bad-setup/findings.json | 2 +- .../test/golden/zero-history-good/findings.json | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/src/content/docs/guide/inspect.mdx b/docs/src/content/docs/guide/inspect.mdx index e24782ec..d227965b 100644 --- a/docs/src/content/docs/guide/inspect.mdx +++ b/docs/src/content/docs/guide/inspect.mdx @@ -8,7 +8,7 @@ import { Aside, Steps } from '@astrojs/starlight/components'; *SHRINE Inspect is a local, read-only command-line tool. It measures your AI coding setup and session history against SHRINE and reports what to change first. It changes nothing itself.* - The first release reads one harness: Claude Code; other harnesses come later @@ -54,9 +54,9 @@ It checks that it changed nothing it read. If anything changed, the report shows 2. Download the three release files into a new folder ```sh - mkdir -p ~/shrine-inspect-0.1.3 && cd ~/shrine-inspect-0.1.3 - base=https://github.com/stablekernel/SHRINE/releases/download/inspect-v0.1.3 - curl -fLO "$base/shrine-inspect-0.1.3.tgz" + mkdir -p ~/shrine-inspect-0.1.4 && cd ~/shrine-inspect-0.1.4 + base=https://github.com/stablekernel/SHRINE/releases/download/inspect-v0.1.4 + curl -fLO "$base/shrine-inspect-0.1.4.tgz" curl -fLO "$base/install.sh" curl -fLO "$base/SHA256SUMS" ``` diff --git a/inspect/TRIAL.md b/inspect/TRIAL.md index 120928df..9bf3bb59 100644 --- a/inspect/TRIAL.md +++ b/inspect/TRIAL.md @@ -13,12 +13,12 @@ A local health check of your Claude Code setup and session history, measured aga node --version ``` -2. Download the 3 release files into a new folder. With the GitHub CLI, use `gh release download inspect-v0.1.3 -R stablekernel/SHRINE` in place of the `curl` lines. +2. Download the 3 release files into a new folder. With the GitHub CLI, use `gh release download inspect-v0.1.4 -R stablekernel/SHRINE` in place of the `curl` lines. ```sh - mkdir -p ~/shrine-inspect-0.1.3 && cd ~/shrine-inspect-0.1.3 - base=https://github.com/stablekernel/SHRINE/releases/download/inspect-v0.1.3 - curl -fLO "$base/shrine-inspect-0.1.3.tgz" + mkdir -p ~/shrine-inspect-0.1.4 && cd ~/shrine-inspect-0.1.4 + base=https://github.com/stablekernel/SHRINE/releases/download/inspect-v0.1.4 + curl -fLO "$base/shrine-inspect-0.1.4.tgz" curl -fLO "$base/install.sh" curl -fLO "$base/SHA256SUMS" ``` @@ -33,7 +33,7 @@ A local health check of your Claude Code setup and session history, measured aga ``` install.sh: OK - shrine-inspect-0.1.3.tgz: OK + shrine-inspect-0.1.4.tgz: OK ``` 4. Install. Run it from the folder that holds the files. @@ -43,9 +43,9 @@ A local health check of your Claude Code setup and session history, measured aga ``` - Usage: `sh install.sh [--skill] [path/to/tgz]`. - - With no path, it uses `shrine-inspect-0.1.3.tgz` next to `install.sh`, not in your current folder. + - With no path, it uses `shrine-inspect-0.1.4.tgz` next to `install.sh`, not in your current folder. - It checks Node, then checks the tarball against the SHA-256 written in `install.sh`, then extracts it. A mismatch installs nothing. - - The CLI goes to `~/.local/share/shrine-inspect/0.1.3/`, linked as `~/.local/bin/shrine-inspect`. + - The CLI goes to `~/.local/share/shrine-inspect/0.1.4/`, linked as `~/.local/bin/shrine-inspect`. - `--skill` copies the Claude Code skill to `~/.claude/skills/shrine-inspect/`. Leave it out to use the CLI only. - To install somewhere else, set the install folder and the link folder first: @@ -68,7 +68,7 @@ A local health check of your Claude Code setup and session history, measured aga ``` ``` - shrine-inspect 0.1.3 + shrine-inspect 0.1.4 ``` - All commands: `shrine-inspect --help`. diff --git a/inspect/package-lock.json b/inspect/package-lock.json index a7aadb67..14b673c3 100644 --- a/inspect/package-lock.json +++ b/inspect/package-lock.json @@ -1,12 +1,12 @@ { "name": "shrine-inspect", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shrine-inspect", - "version": "0.1.3", + "version": "0.1.4", "license": "Apache-2.0", "bin": { "shrine-inspect": "bin/shrine-inspect.mjs" diff --git a/inspect/package.json b/inspect/package.json index b7650135..ffecb056 100644 --- a/inspect/package.json +++ b/inspect/package.json @@ -1,6 +1,6 @@ { "name": "shrine-inspect", - "version": "0.1.3", + "version": "0.1.4", "description": "Health check of an AI coding setup against the SHRINE docs. Local, read-only, no network.", "license": "Apache-2.0", "author": "Stable Kernel", diff --git a/inspect/skill/.claude-plugin/plugin.json b/inspect/skill/.claude-plugin/plugin.json index c70462c3..d69e9b80 100644 --- a/inspect/skill/.claude-plugin/plugin.json +++ b/inspect/skill/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "shrine-inspect", - "version": "0.1.3", + "version": "0.1.4", "description": "Thin skill that starts and watches the shrine-inspect CLI. Holds no logic; install the CLI from the same release.", "license": "Apache-2.0", "author": { "name": "Stable Kernel" }, diff --git a/inspect/skill/skills/shrine-inspect/SKILL.md b/inspect/skill/skills/shrine-inspect/SKILL.md index cb382ac6..59c213e5 100644 --- a/inspect/skill/skills/shrine-inspect/SKILL.md +++ b/inspect/skill/skills/shrine-inspect/SKILL.md @@ -6,7 +6,7 @@ allowed-tools: Bash(node --version), Bash(shrine-inspect version:*), Bash(shrine # SHRINE Inspect -Skill version: 0.1.3 +Skill version: 0.1.4 This skill only starts the `shrine-inspect` CLI and watches it. The CLI does every read, every gate, and every judge call. @@ -31,7 +31,7 @@ This skill only starts the `shrine-inspect` CLI and watches it. The CLI does eve 2. Check the CLI. Run: ```bash - shrine-inspect version --skill-version 0.1.3 + shrine-inspect version --skill-version 0.1.4 ``` If the command is not found, or it stops with `E_VERSION`, stop. Tell the user to install the CLI and this skill from the same release (see TRIAL.md in the release). @@ -39,7 +39,7 @@ This skill only starts the `shrine-inspect` CLI and watches it. The CLI does eve 3. Start a run in the background. Use `--tier T1` only when the user asked for judging ("Pulse"); else use `--tier T0`. Always pass `--tier`: without it, `start` stops with `E_TIER_REQUIRED`. Session id: `${CLAUDE_SESSION_ID}`. If that id is empty or still shows as a placeholder, leave out `--session-id` and its value. Never leave `--session-id` with nothing after it. ```bash - shrine-inspect start --tier T0 --session-id ${CLAUDE_SESSION_ID} --skill-version 0.1.3 --json + shrine-inspect start --tier T0 --session-id ${CLAUDE_SESSION_ID} --skill-version 0.1.4 --json ``` Keep the `run_id` from the output. @@ -47,7 +47,7 @@ This skill only starts the `shrine-inspect` CLI and watches it. The CLI does eve 4. Watch the run. Run this about every 15 seconds while the state is `running`: ```bash - shrine-inspect status --skill-version 0.1.3 --json + shrine-inspect status --skill-version 0.1.4 --json ``` - `running`: give the user one short line with the phase. Check again. diff --git a/inspect/src/scorer/score.mjs b/inspect/src/scorer/score.mjs index c58d8aad..4827448c 100644 --- a/inspect/src/scorer/score.mjs +++ b/inspect/src/scorer/score.mjs @@ -7,7 +7,7 @@ import { underHome } from '../scrub/mask.mjs'; import { THRESHOLDS } from '../probes/config.mjs'; import { controlTest, usageConfidence, usageSeverity } from './stats.mjs'; -export const TOOL_VERSION = '0.1.3'; +export const TOOL_VERSION = '0.1.4'; const SEV_RANK = { high: 3, medium: 2, low: 1 }; const KIND_ORDER = ['blanket_permissions', 'stale_instructions', 'oversized_instructions', 'done_without_test', 'no_owner_or_review', 'stack_gap', 'short_retention']; diff --git a/inspect/test/golden/bad-setup/findings.json b/inspect/test/golden/bad-setup/findings.json index 0056bc55..28f459b5 100644 --- a/inspect/test/golden/bad-setup/findings.json +++ b/inspect/test/golden/bad-setup/findings.json @@ -3,7 +3,7 @@ "run": { "id": "", "started_at": "2026-11-04T12:00:00.000Z", - "tool_version": "0.1.3", + "tool_version": "0.1.4", "harness": { "name": "Claude Code", "version": null diff --git a/inspect/test/golden/zero-history-good/findings.json b/inspect/test/golden/zero-history-good/findings.json index e80da3ac..0065f87b 100644 --- a/inspect/test/golden/zero-history-good/findings.json +++ b/inspect/test/golden/zero-history-good/findings.json @@ -3,7 +3,7 @@ "run": { "id": "", "started_at": "2026-11-04T12:00:00.000Z", - "tool_version": "0.1.3", + "tool_version": "0.1.4", "harness": { "name": "Claude Code", "version": null