diff --git a/.github/workflows/test-cross-plugin.yml b/.github/workflows/test-cross-plugin.yml new file mode 100644 index 00000000..663316b5 --- /dev/null +++ b/.github/workflows/test-cross-plugin.yml @@ -0,0 +1,35 @@ +name: Cross-Plugin Guards +# Runs repo-root behavior guards that span more than one plugin, so they can't +# live in any single plugin's release-blocking suite. Scoped tightly to the +# files each guard covers — a core-only or single-plugin PR must not trigger it. +# NOTE: the automode-env glob matches the three copies under scripts/ today; +# a future copy placed elsewhere would need its path added here. +on: + push: + branches: [main] + paths: + - 'plugins/*/scripts/automode-env.ts' + - 'tests/cross-plugin/**' + - '.github/workflows/test-cross-plugin.yml' + - 'package.json' + - 'bun.lock' + - 'tsconfig.json' + pull_request: + paths: + - 'plugins/*/scripts/automode-env.ts' + - 'tests/cross-plugin/**' + - '.github/workflows/test-cross-plugin.yml' + - 'package.json' + - 'bun.lock' + - 'tsconfig.json' + +jobs: + cross-plugin: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' + - run: bun install --frozen-lockfile + - run: bun test tests/cross-plugin/ diff --git a/plugins/claude-code-dev-hermit/CHANGELOG.md b/plugins/claude-code-dev-hermit/CHANGELOG.md index 04101308..251630dc 100644 --- a/plugins/claude-code-dev-hermit/CHANGELOG.md +++ b/plugins/claude-code-dev-hermit/CHANGELOG.md @@ -9,6 +9,7 @@ ### Changed - The CLAUDE-APPEND template gained a closing marker (``), placed outside both mode regions so it survives both renderings. Lets core's `hermit-evolve` bound the block exactly instead of a heuristic that used to mistake the template's own `` annotation for the block marker. +- `domain-brainstorm` reads core's proposal-metrics report via `.claude-code-hermit/bin/hermit-run` (a path relative to this plugin can't reach core's install), and a kill-criteria breach now escalates to the operator as a class-level signal instead of instructing the skill to self-retire (the shared segment can't attribute noise to one skill). - §Tests Before PR folded into §Implementation Flow (both stated one ordering, and `dev-pr` Gate 0 enforces the test-freshness half mechanically), the slug algorithm and its worked examples became one naming line, and the harness built-ins left §Dev Quick Reference. Rendered standard block 9,330 B → ~6,158 B, safety 6,338 B → ~4,703 B. No git-safety rule was removed. - The background-session recovery advice ("re-attempt the edit") moved out of §Git Safety into `worktree-boundary-guard`'s block message, where it appears at the moment it is actionable instead of in every session's context. diff --git a/plugins/claude-code-dev-hermit/scripts/dev-pr-transforms.ts b/plugins/claude-code-dev-hermit/scripts/dev-pr-transforms.ts index 159f7a54..45870538 100644 --- a/plugins/claude-code-dev-hermit/scripts/dev-pr-transforms.ts +++ b/plugins/claude-code-dev-hermit/scripts/dev-pr-transforms.ts @@ -16,6 +16,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync } from 'node:child_process'; +import { findHermitDir } from './lib/find-hermit-dir'; type Json = any; @@ -102,19 +103,6 @@ function git(args: string): string { } catch (_) { return ''; } } -function findHermitDir(startDir: string): string | null { - let dir = startDir; - for (let i = 0; i < 8; i++) { - if (fs.existsSync(path.join(dir, '.claude-code-hermit', 'config.json'))) { - return path.join(dir, '.claude-code-hermit'); - } - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - function loadBinding(branch: string): { id?: string; title?: string } | null { const hermitDir = findHermitDir(process.cwd()); if (!hermitDir) return null; diff --git a/plugins/claude-code-dev-hermit/scripts/git-push-guard.ts b/plugins/claude-code-dev-hermit/scripts/git-push-guard.ts index 4611f209..309519c9 100644 --- a/plugins/claude-code-dev-hermit/scripts/git-push-guard.ts +++ b/plugins/claude-code-dev-hermit/scripts/git-push-guard.ts @@ -9,20 +9,10 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; +import { findHermitDir } from './lib/find-hermit-dir'; const MAX_STDIN = 1024 * 1024; -function findHermitDir(startDir: string): string | null { - let dir = startDir; - for (let i = 0; i < 8; i++) { - if (fs.existsSync(path.join(dir, '.claude-code-hermit', 'config.json'))) return path.join(dir, '.claude-code-hermit'); - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - function loadProtectedBranches(): string[] { try { const hermitDir = findHermitDir(process.cwd()); diff --git a/plugins/claude-code-dev-hermit/scripts/lib/find-hermit-dir.ts b/plugins/claude-code-dev-hermit/scripts/lib/find-hermit-dir.ts new file mode 100644 index 00000000..27173bc9 --- /dev/null +++ b/plugins/claude-code-dev-hermit/scripts/lib/find-hermit-dir.ts @@ -0,0 +1,24 @@ +// Walk up from startDir (max 8 levels) to the nearest .claude-code-hermit dir +// that has a config.json; return that dir, or null when none is found. +// +// Returning null (rather than a fail-open default path) is load-bearing: +// git-push-guard falls back to the built-in protected-branch list on null instead +// of blocking, and record-test-result / dev-pr-transforms skip their hermit-state +// writes. Do NOT change this to core's fail-open hermitDir() default. +// +// INVARIANT: mirrors core's cc-compat.ts hermitDir() shape (same 8-level cap, +// same config.json sentinel) — if you change the walk here, check that file too. + +import fs from 'node:fs'; +import path from 'node:path'; + +export function findHermitDir(startDir: string): string | null { + let dir = startDir; + for (let i = 0; i < 8; i++) { + if (fs.existsSync(path.join(dir, '.claude-code-hermit', 'config.json'))) return path.join(dir, '.claude-code-hermit'); + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} diff --git a/plugins/claude-code-dev-hermit/scripts/record-test-result.ts b/plugins/claude-code-dev-hermit/scripts/record-test-result.ts index 57175c45..8a25775f 100644 --- a/plugins/claude-code-dev-hermit/scripts/record-test-result.ts +++ b/plugins/claude-code-dev-hermit/scripts/record-test-result.ts @@ -13,22 +13,12 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync, spawnSync } from 'node:child_process'; +import { findHermitDir } from './lib/find-hermit-dir'; type Json = any; const MAX_STDIN = 1024 * 1024; -function findHermitDir(startDir: string): string | null { - let dir = startDir; - for (let i = 0; i < 8; i++) { - if (fs.existsSync(path.join(dir, '.claude-code-hermit', 'config.json'))) return path.join(dir, '.claude-code-hermit'); - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - function loadTestCommand(hermitDir: string): string | null { try { const cfg = JSON.parse(fs.readFileSync(path.join(hermitDir, 'config.json'), 'utf-8')); diff --git a/plugins/claude-code-dev-hermit/skills/domain-brainstorm/SKILL.md b/plugins/claude-code-dev-hermit/skills/domain-brainstorm/SKILL.md index 0e0f3ca8..0e344a20 100644 --- a/plugins/claude-code-dev-hermit/skills/domain-brainstorm/SKILL.md +++ b/plugins/claude-code-dev-hermit/skills/domain-brainstorm/SKILL.md @@ -7,7 +7,7 @@ description: On-demand dev-voice brainstorm — reads codebase friction signals ## Kill criteria (read before running) -After ≥8 invocations, check the `capability-brainstorm` segment of core's proposal metrics report — run `bun scripts/proposal-metrics-report.ts .claude-code-hermit --source=capability-brainstorm` from the `claude-code-hermit` plugin root (not this plugin's; `${CLAUDE_PLUGIN_ROOT}` cannot reach it), or read the `capability-brainstorm` row of the table `/claude-code-hermit:hermit-evolution` prints. If triage-survival < 25% or PROP-acceptance < 30%, cut this skill rather than tune it — signal-to-noise isn't there. That segment is shared with core's `capability-brainstorm` and the other domain brainstorm skills, so a breach means brainstorm-generated proposals are noisy in general; it does not by itself say which skill to cut. +After ≥8 invocations, check the `capability-brainstorm` segment of core's proposal metrics report — run `.claude-code-hermit/bin/hermit-run proposal-metrics-report .claude-code-hermit --source=capability-brainstorm` (the project-resident `bin/hermit-run` resolves core's plugin root, which a path relative to this plugin can't reach), or read the `capability-brainstorm` row of the table `/claude-code-hermit:hermit-evolution` prints. If triage-survival < 25% or PROP-acceptance < 30%, treat it as a class-level signal that brainstorm output is noisy and raise it with the operator — the segment is shared with core's `capability-brainstorm` and the other domain brainstorm skills, so it cannot by itself say which skill to cut. ### Gate 0 — Gather inputs diff --git a/plugins/claude-code-dev-hermit/skills/hatch/SKILL.md b/plugins/claude-code-dev-hermit/skills/hatch/SKILL.md index 69d5fbf2..4288135d 100644 --- a/plugins/claude-code-dev-hermit/skills/hatch/SKILL.md +++ b/plugins/claude-code-dev-hermit/skills/hatch/SKILL.md @@ -77,7 +77,7 @@ Read the plugin version from `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json`. **Resolve target file:** Read `.claude-code-hermit/state/hatch-options.json`. Use the `"target"` field: - `"local"` → `target_file = CLAUDE.local.md` - `"committed"` or absent → `target_file = CLAUDE.md` -- If the file doesn't exist (no `hatch-options.json` yet — operator's core hermit predates 1.1.1): detect `core_install_scope` from `claude plugin list --json` using the same precedence rules core hatch's Step 1.5 item 2 uses (filter to entries where plugin name is `claude-code-hermit` and `enabled == true`; apply precedence `local` > `project` (both require `projectPath == project root`) > `user` (any `projectPath`) > `null`; map `project` → `committed`, `local`/`user`/`null` → `local` as the scope-derived default). Ask with `AskUserQuestion` (header: "Visibility") — present the scope-derived default at position 0 with `(recommended)` in the label: **`.local` files** (gitignored — operator-personal) / **Committed files** (shared with teammates). Record the choice and write `.claude-code-hermit/state/hatch-options.json` with the full schema: +- If the file doesn't exist (no `hatch-options.json` yet — operator's core hermit predates 1.1.1): detect `core_install_scope` from `claude plugin list --json` using the same precedence core hatch resolves via `resolve-siblings.ts --role core-scope` (filter to entries where plugin name is `claude-code-hermit` and `enabled == true`; apply precedence `local` > `project` (both require `projectPath == project root`) > `user` (any `projectPath`) > `null`; map `project` → `committed`, `local`/`user`/`null` → `local` as the scope-derived default). Ask with `AskUserQuestion` (header: "Visibility") — present the scope-derived default at position 0 with `(recommended)` in the label: **`.local` files** (gitignored — operator-personal) / **Committed files** (shared with teammates). Record the choice and write `.claude-code-hermit/state/hatch-options.json` with the full schema: ```json { diff --git a/plugins/claude-code-fitness-hermit/CHANGELOG.md b/plugins/claude-code-fitness-hermit/CHANGELOG.md index cf0fede7..bacb0313 100644 --- a/plugins/claude-code-fitness-hermit/CHANGELOG.md +++ b/plugins/claude-code-fitness-hermit/CHANGELOG.md @@ -11,6 +11,7 @@ - `hatch` no longer tells the operator to `cp .env.example .env`; a `.env.example` does not ship with the plugin. ### Changed +- `domain-brainstorm` reads core's proposal-metrics report via `.claude-code-hermit/bin/hermit-run` (a path relative to this plugin can't reach core's install), and a kill-criteria breach now escalates to the operator as a class-level signal instead of instructing the skill to self-retire (the shared segment can't attribute noise to one skill). - `strava-sync` and `strava-health-check` routines removed — `fitness-brief` (morning + evening) now owns Strava connectivity, activity sync, RPE binding, and Run deep-dive as the plugin's two daily beats. - Activity notes carry `cardiac_drift_bpm` in frontmatter; `weekly-patterns` reads it there and falls back to the rendered line for older notes. - The CLAUDE-APPEND block dropped the routine and scheduled-check tables (schedules and `enabled` state live in `config.json`) and the five-file state map, which now points at `docs/knowledge-schema.md`. ~3,852 B → ~2,881 B. Connection-first, the secrets rules, the settings-blocked write tools, the `fitness-lab.ts` mediation boundary, the zones rule, and the full-history grounding gotcha are unchanged. diff --git a/plugins/claude-code-fitness-hermit/skills/domain-brainstorm/SKILL.md b/plugins/claude-code-fitness-hermit/skills/domain-brainstorm/SKILL.md index 23bed876..c39c843d 100644 --- a/plugins/claude-code-fitness-hermit/skills/domain-brainstorm/SKILL.md +++ b/plugins/claude-code-fitness-hermit/skills/domain-brainstorm/SKILL.md @@ -7,7 +7,7 @@ description: On-demand fitness-voice brainstorm — reads Strava history and tra ## Kill criteria (read before running) -After ≥8 invocations, check the `capability-brainstorm` segment of core's proposal metrics report — run `bun scripts/proposal-metrics-report.ts .claude-code-hermit --source=capability-brainstorm` from the `claude-code-hermit` plugin root (not this plugin's; `${CLAUDE_PLUGIN_ROOT}` cannot reach it), or read the `capability-brainstorm` row of the table `/claude-code-hermit:hermit-evolution` prints. If triage-survival < 25% or PROP-acceptance < 30%, cut this skill rather than tune it — signal-to-noise isn't there. That segment is shared with core's `capability-brainstorm` and the other domain brainstorm skills, so a breach means brainstorm-generated proposals are noisy in general; it does not by itself say which skill to cut. +After ≥8 invocations, check the `capability-brainstorm` segment of core's proposal metrics report — run `.claude-code-hermit/bin/hermit-run proposal-metrics-report .claude-code-hermit --source=capability-brainstorm` (the project-resident `bin/hermit-run` resolves core's plugin root, which a path relative to this plugin can't reach), or read the `capability-brainstorm` row of the table `/claude-code-hermit:hermit-evolution` prints. If triage-survival < 25% or PROP-acceptance < 30%, treat it as a class-level signal that brainstorm output is noisy and raise it with the operator — the segment is shared with core's `capability-brainstorm` and the other domain brainstorm skills, so it cannot by itself say which skill to cut. ### Gate 0 — Gather inputs diff --git a/plugins/claude-code-fitness-hermit/skills/hatch/SKILL.md b/plugins/claude-code-fitness-hermit/skills/hatch/SKILL.md index a2c0e840..0c6db78f 100644 --- a/plugins/claude-code-fitness-hermit/skills/hatch/SKILL.md +++ b/plugins/claude-code-fitness-hermit/skills/hatch/SKILL.md @@ -152,7 +152,7 @@ Read the source file (using Read tool), then check if the destination exists (`. **Resolve target file:** Read `.claude-code-hermit/state/hatch-options.json`. Use the `"target"` field: - `"local"` → `target_file = CLAUDE.local.md` - `"committed"` or absent → `target_file = CLAUDE.md` -- If the file doesn't exist (no `hatch-options.json` yet — operator's core hermit predates 1.1.1): detect `core_install_scope` from `claude plugin list --json` using the same precedence rules as core hatch Step 1.5 item 2 (filter entries where plugin name is `claude-code-hermit` and `enabled == true`; precedence `local` > `project` (both require `projectPath == project root`) > `user` (any `projectPath`) > `null`; map `project` → `committed`, `local`/`user`/`null` → `local`). Ask with `AskUserQuestion` (header: "Visibility") — scope-derived default at position 0 with `(recommended)`: **`.local` files** (gitignored — operator-personal) / **Committed files** (shared with teammates). Write the canonical 5-field schema to `.claude-code-hermit/state/hatch-options.json`: +- If the file doesn't exist (no `hatch-options.json` yet — operator's core hermit predates 1.1.1): detect `core_install_scope` from `claude plugin list --json` using the same precedence core hatch resolves via `resolve-siblings.ts --role core-scope` (filter entries where plugin name is `claude-code-hermit` and `enabled == true`; precedence `local` > `project` (both require `projectPath == project root`) > `user` (any `projectPath`) > `null`; map `project` → `committed`, `local`/`user`/`null` → `local`). Ask with `AskUserQuestion` (header: "Visibility") — scope-derived default at position 0 with `(recommended)`: **`.local` files** (gitignored — operator-personal) / **Committed files** (shared with teammates). Write the canonical 5-field schema to `.claude-code-hermit/state/hatch-options.json`: ```json { diff --git a/plugins/claude-code-hermit/CHANGELOG.md b/plugins/claude-code-hermit/CHANGELOG.md index 91973a7d..544035a4 100644 --- a/plugins/claude-code-hermit/CHANGELOG.md +++ b/plugins/claude-code-hermit/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - Channel-originated harness commands: an exact `/model `, `/effort `, `/compact`, or `/clear` from a trusted channel sender is recorded by a new `UserPromptSubmit` hook and typed into the session's pane when the turn ends. Args are passed through to Claude Code rather than checked against a fixed list, so new models and effort levels work without a plugin change. - `config.effort` — passed as `--effort` on every `hermit-start`, mirroring `config.model`, so a channel effort change reverts on restart instead of persisting. Ships `null` (no flag, model default); set it to opt into the revert-on-restart guarantee. Not the same lever as `config.env.CLAUDE_CODE_EFFORT_LEVEL`, which pins the session and makes a runtime `/effort` a no-op. +- `micro-proposal.ts brief-cycle` — ages the whole micro-approval queue in one call (re-nudges `follow_up_count` 1 entries, expires 2+ entries, records each expiry) and returns a JSON verdict, replacing the per-entry `nudge`/`resolve` calls both briefs used to issue one at a time. +- Two `Bash(.claude-code-hermit/bin/hermit-run