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
35 changes: 35 additions & 0 deletions .github/workflows/test-cross-plugin.yml
Original file line number Diff line number Diff line change
@@ -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/
1 change: 1 addition & 0 deletions plugins/claude-code-dev-hermit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Changed
- The CLAUDE-APPEND template gained a closing marker (`<!-- /claude-code-dev-hermit: Development Workflow -->`), 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 `<!-- mode:standard-only -->` 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.

Expand Down
14 changes: 1 addition & 13 deletions plugins/claude-code-dev-hermit/scripts/dev-pr-transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
12 changes: 1 addition & 11 deletions plugins/claude-code-dev-hermit/scripts/git-push-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
24 changes: 24 additions & 0 deletions plugins/claude-code-dev-hermit/scripts/lib/find-hermit-dir.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 1 addition & 11 deletions plugins/claude-code-dev-hermit/scripts/record-test-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code-dev-hermit/skills/hatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
1 change: 1 addition & 0 deletions plugins/claude-code-fitness-hermit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code-fitness-hermit/skills/hatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
6 changes: 6 additions & 0 deletions plugins/claude-code-hermit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Added
- Channel-originated harness commands: an exact `/model <arg>`, `/effort <arg>`, `/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 <script> *)` allow entries so domain plugins can reach core's shared scripts through the project-resident `bin/hermit-run` (their own `${CLAUDE_PLUGIN_ROOT}` can't resolve core's versioned cache dir). The word-boundary space and `hermit-exec.sh`'s new `/`/`..` rejection keep the route from reaching a script outside core's `scripts/`.

### Fixed
- A `/clear` reaching the pane outside the watchdog no longer skips the hermit's own reset bookkeeping. The runtime stamp, `SHELL.md` breadcrumb, and status-cache clear moved to `lib/context-reset.ts`; skipping the cache clear previously let the watchdog fire a spurious `/compact` against a freshly-cleared context.
Expand All @@ -21,10 +23,14 @@
- A CLAUDE-APPEND marker that appears more than once in the target file is now refused (reported `block-ambiguous`, no Edit applied) instead of risking a replace that hits the wrong instance.
- Micro-approval resolve/expire/nudge now go through the new `scripts/micro-proposal.ts` instead of hand-edited JSON, which could leave `state/micro-proposals.json` unparseable.
- `queue-micro-proposal.ts` refuses to write over an unparseable `micro-proposals.json` instead of resetting it and silently dropping the pending backlog.
- `hermit-evolve`'s sibling upgrade now applies `local > project` scope precedence when the same sibling is installed at both scopes in one project, matching `resolve-siblings.ts` and Claude Code's own settings layering — it previously read the project-scope install's version and changelog while the session was running the local-scope one.
- `hatch` Step 8's rationale no longer claims HA's `ha-morning-brief` reaches `micro-proposal.ts` via a `../claude-code-hermit/scripts/` sibling path (that path never resolved from an installed domain plugin); it reaches it via `bin/hermit-run`.

### Changed
- Proactive operator notifications unified onto `channel-send.ts --notice`; `weekly-review`, `cost-reflect`, and (via § Operator Notification) `heartbeat`/`brief`/`capability-brainstorm` are tier-aware, so spend and technical detail no longer reach the client chat. On a `non-technical` install with no `maintainer_channel_id`, routine spend reports now land in `SHELL.md` Findings instead of the primary chat.
- The core CLAUDE-APPEND template gained a closing marker (`<!-- /claude-code-hermit: Session Discipline -->`), matching `claude-code-homeassistant-hermit`/`claude-code-fitness-hermit`/`feed-hermit`/`laravel-forge-hermit`. Bounds detection still falls back correctly for already-installed blocks that predate the marker.
- `hermit-evolve` Step 8 now delegates the permission merge to `bun scripts/apply-settings.ts <settings-file> allow` instead of maintaining a hand-enumerated script list in prose. The prose list had drifted to 15 of the canonical 28 entries and told the model to add a `Write` rule the writer deliberately strips; the delegated writer holds the single canonical list and writes via `fs`, so it also works under the strict hook profile where an `Edit`/`Write` to `.claude/settings*.json` is blocked.
- `bin/hermit-run` validates `HERMIT_PLUGIN_ROOT` by manifest name and fails loud when two marketplaces provide `claude-code-hermit` (instead of silently taking glob order); `hermit-exec.sh` rejects a script name containing `/` or `..`.

### Upgrade Instructions
1. The CLAUDE-APPEND change reaches installed hermits through the standard `<!-- claude-code-hermit: Session Discipline -->` marker-block resync `hermit-evolve` already performs. Skills and scripts ship in the plugin, so no per-operator migration is needed for those.
Expand Down
Loading
Loading