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
91 changes: 91 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,97 @@ All notable changes to `role-x` are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.1] — 2026-05-14

Closes the meta-progression gap. The per-prompt routing was wired in v0.2.0;
v0.4.0 shipped the observability substrate; v0.4.1 wires the agent-facing
nudges so the *expression* of the system progresses naturally — without
requiring the user to remember to run `role-x suggest` or notice when the
registry undercovers their work.

### Added

- **In-prompt authoring nudge** — when intake routes to `_meta` only **AND**
the prompt is "domain-rich" (≥8 words, ≥4 distinct meaningful tokens), the
agent's working-context output appends one line:

```
Note: no domain lens scored ≥2 for this prompt. If this kind of work
recurs, consider expanding the registry: `role-x init <slug>` (status:
candidate).
```

The slug is auto-derived from the first 2 distinctive tokens. Tuning
knobs: `DOMAIN_RICH_MIN_WORDS = 8`, `DOMAIN_RICH_MIN_TOKENS = 4`.

- **`role-x coverage`** — brief registry-health summary suitable for a
SessionStart hook. Silent (exit 0, no output) when fire-rate ≥30% AND
sanitized capture is enabled. Surfaces a 3-5 line nudge otherwise.

```
role-x coverage [--since 7d --min-events N --force --events-path PATH]
```

- **`scripts/role-x-coverage-hook.sh`** — Claude Code `SessionStart` hook
wrapper. 24h cooldown via `~/.config/broomva/role/coverage-stamp` (override
via `ROLE_X_COVERAGE_COOLDOWN_HOURS`). Graceful-fail on missing Python /
missing PyYAML / missing CLI. Always exits 0. Wired into workspace via
`.claude/settings.json` `SessionStart` entry (separate PR on
broomva/workspace).

- **8 new tests** (30 → **38 total**):
- `intake_nudges_for_meta_only_domain_rich_prompt`
- `intake_no_nudge_when_lens_fires`
- `intake_no_nudge_for_short_prompt`
- `coverage_silent_when_healthy`
- `coverage_reports_when_no_sanitized_capture`
- `coverage_reports_low_fire_rate`
- `coverage_silent_below_min_events`
- `coverage_force_prints_when_below_min`

### Why this exists (the gap closed)

v0.4.0 made `role-x suggest` available, but nothing reminded agents to *run*
it. The system captured 31 unrouted prompts over 7 days but no one noticed
the pattern. v0.4.1 fixes that with two complementary nudges:

| Cadence | Mechanism | Trigger |
|---|---|---|
| Per-prompt | Intake context appendix | `_meta`-only AND domain-rich prompt |
| Per-session (≤1/24h) | SessionStart hook → `coverage` | Fire-rate < 30% OR sanitized capture off |

Both are non-blocking and exit silently when the registry is healthy. Like
P8 skill-freshness, they nudge but never gate.

### Backward compatibility

- All v0.1.0-v0.4.0 lenses work unchanged.
- Event schema unchanged — nudge is computed at runtime, not stored.
- CLI signatures preserved.
- The intake context output has a new optional appendix; agents that don't
consume it experience no change.

### Workspace wiring (separate PR on broomva/workspace)

To enable the SessionStart hook, add to `.claude/settings.json`:

```json
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$HOME/.agents/skills/role-x/scripts/role-x-coverage-hook.sh",
"timeout": 5
}
]
}
]
```

If the role-x install doesn't have v0.4.1+ yet, the hook silently exits 0.
No new failure mode introduced.

## [0.4.0] — 2026-05-14

Observability for organic lens growth. Closes the half-loop from v0.3.0 — the
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Selection and mode-decision are **reasoning-enforced** (bstack-idiom, same as P1
| `role-x intake [--prompt … --workspace … --session …]` | **v0.2.0+** — `UserPromptSubmit` hook entry point. Scores lenses against current signals (git + prompt content), walks `extends:` chain, decides mode, emits event to `~/.config/broomva/role/events.jsonl`, prints agent-context to stdout. Reads JSON from stdin if `--prompt` omitted (the Claude Code hook protocol). |
| `role-x suggest [--since 7d --threshold N --limit M --events-path PATH]` | **v0.4.0+** — analyze `events.jsonl` over a window. Reports fire-rate (lens-fired vs `_meta`-only), per-lens drift (fires + sessions + avg prompt length), and (when sanitized capture is on) emergent keyword clusters in unrouted events with suggested lens names. Read-only. |
| `role-x init <name> [--keywords K1,K2 --paths P1,P2 --threshold N --extends NAME ...]` | **v0.4.0+** — scaffold a new `status: candidate` lens under `roles/<name>.md` from CLI flags. Scaffolded file passes `validate` immediately. Author edits, then promotes to `status: active` after ≥3 positive-outcome uses (P16). |
| `role-x coverage [--since 7d --min-events 10 --force]` | **v0.4.1+** — brief registry-health summary. Silent (exit 0, no output) when fire-rate ≥30% AND sanitized capture is on. Surfaces a 3-5 line nudge otherwise. Designed as the entry point for the `SessionStart` hook with a 24h cooldown — `scripts/role-x-coverage-hook.sh` wires this. |

## Hook integration (v0.2.0+)

Expand Down Expand Up @@ -264,7 +265,8 @@ git add roles/ && git commit -m "feat(roles): add deploy-vercel-env candidate le
- **v0.1.0** — Markdown lens registry + CLI (`validate`, `list`, `index`) + reference docs
- **v0.2.0** — `intake` subcommand + `UserPromptSubmit` hook + `~/.config/broomva/role/events.jsonl` capture
- **v0.3.0** — Per-lens `threshold:` override + per-signal-type `signals.weights:`
- **v0.4.0** (this release) — Observability for organic growth: `role-x suggest` + `role-x init` + opt-in sanitized prompt capture
- **v0.4.0** — Observability for organic growth: `role-x suggest` + `role-x init` + opt-in sanitized prompt capture
- **v0.4.1** (this release) — Meta-progression nudges: `role-x coverage` SessionStart hook + per-prompt authoring suggestion when intake routes to `_meta` only on a domain-rich prompt
- **v0.5.0** — `role-x tune <lens>` + `role-x propose-lens <cluster>` (PR-driven lens updates)
- **v0.6.0** — P13 dream cycle: `role-x-replay.py` with replay-against-frozen-substrate; `status.json` per-lens stats cache; auto-promote candidates on rule-of-three positive outcomes
- **v0.7.0** — `PostToolUse` + `Stop` outcome hooks → quality signals per lens-use
Expand Down
31 changes: 29 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,45 @@ description: |
- `role-x list` — list all available lenses with status + extends + default_mode
- `role-x validate <path>` — validate lens YAML frontmatter against schema
- `role-x index` — regenerate `roles/_index.md` discovery file
5. **Reference docs** (`references/`):
- `role-x intake` (v0.2.0) — `UserPromptSubmit` hook entry point
- `role-x suggest` (v0.4.0) — analyze events.jsonl; surface fire-rate + drift + emergent clusters
- `role-x init <name>` (v0.4.0) — scaffold a `status: candidate` lens from CLI flags
- `role-x coverage` (v0.4.1) — brief registry-health summary; silent when healthy (SessionStart hook entry point)
5. **Hooks** (`scripts/*-hook.sh`):
- `role-x-intake-hook.sh` (v0.2.0) — `UserPromptSubmit` wrapper
- `role-x-coverage-hook.sh` (v0.4.1) — `SessionStart` wrapper with 24h cooldown
6. **Reference docs** (`references/`):
- `lens-schema.md` — YAML frontmatter field reference
- `selection-algorithm.md` — scoring algorithm in detail
- `mode-selection.md` — augment/rewrite/decompose decision tree
- `feedback-loop.md` — Nous-pattern telemetry + P13 consolidation (M2+)

## When to invoke

Always — at the start of every session, before responding to substantive user input. P17 is a reflexive primitive. The skill exists to make the lens registry and CLI helpers discoverable; the *behavior* is enforced by reasoning.
### Intake reflex (every prompt)

Always — at the start of every session, before responding to substantive user input. P17 is a reflexive primitive. The skill exists to make the lens registry and CLI helpers discoverable; the *behavior* is enforced by reasoning + the UserPromptSubmit hook.

Carve-outs (no role-x intake needed): single-line typo fixes, pure read questions ("what does this function do?"), conversation continuation without new substantive request.

### Meta-progression discipline (v0.4.1+)

The intake reflex routes prompts in real-time. The meta-progression discipline ensures the *registry itself* grows from real telemetry:

| When | Action | Cadence |
|---|---|---|
| **SessionStart** in a workspace with the role-x coverage hook wired | `role-x coverage --since 7d` fires automatically; surfaces fire-rate + config hints when registry health drops | ≤1 nudge per 24h |
| **Per substantive prompt** | If intake routes to `_meta` only **AND** prompt is domain-rich (≥8 words, ≥4 distinct meaningful tokens) | Agent sees a 1-line `role-x init <slug>` suggestion appended to the intake context |
| **When the agent observes a recurring `_meta`-only pattern** within a session (e.g. 3+ unrouted prompts about the same domain) | Propose `role-x init <name>` to the user as the rule-of-three trigger | At the agent's discretion, surfaced as a one-line note |
| **Weekly (or after collecting ~50+ events)** | Run `role-x suggest --since 7d` for the full report — fire-rate, per-lens drift, emergent keyword clusters (requires sanitized capture on) | Manual, with telemetry signal from the SessionStart nudge |
| **After ≥3 positive-outcome uses of a `status: candidate` lens** | Author promotes the lens to `status: active` (P16 rule-of-three) | Manual, candidate ledger tracks instances |

### When NOT to invoke meta-actions

- Single-prompt sessions where the intake nudge is purely informational — don't pause work to author lenses mid-flow
- Edits to existing active lenses unless `role-x tune <lens>` (v0.5.0+) surfaces concrete drift signals
- New lenses without ≥3 distinct-session evidence in events.jsonl (avoids cargo-cult lens proliferation)

## When NOT to invoke

- One-shot read questions answered from context alone
Expand Down
54 changes: 54 additions & 0 deletions scripts/role-x-coverage-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# role-x-coverage-hook.sh — Claude Code SessionStart hook for v0.4.1 onwards.
#
# Once-per-session-ish nudge surfacing registry health when it looks under-
# covered. Always exits 0; never blocks. Cooldown via stamp file.
#
# Installed by: `npx skills add broomva/role-x`
# Canonical location: ~/.agents/skills/role-x/scripts/role-x-coverage-hook.sh
# Wired from: $WORKSPACE/.claude/settings.json under "SessionStart"

set -eu

PYTHON_BIN="${ROLE_X_PYTHON:-python3}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROLE_X_PY="$SCRIPT_DIR/role-x.py"

# Cooldown — at most one report per N hours (default 24h)
COOLDOWN_HOURS="${ROLE_X_COVERAGE_COOLDOWN_HOURS:-24}"
STAMP_FILE="${HOME}/.config/broomva/role/coverage-stamp"

# Graceful-fail if Python or the CLI are absent
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
exit 0
fi
if [ ! -f "$ROLE_X_PY" ]; then
exit 0
fi
if ! "$PYTHON_BIN" -c "import yaml" >/dev/null 2>&1; then
exit 0
fi

# Cooldown check (Darwin and Linux paths)
if [ -f "$STAMP_FILE" ]; then
if [ "$(uname)" = "Darwin" ]; then
last_run=$(stat -f %m "$STAMP_FILE" 2>/dev/null || echo 0)
else
last_run=$(stat -c %Y "$STAMP_FILE" 2>/dev/null || echo 0)
fi
now=$(date +%s)
elapsed=$((now - last_run))
cooldown=$((COOLDOWN_HOURS * 3600))
if [ "$elapsed" -lt "$cooldown" ]; then
exit 0
fi
fi

# Run the coverage summary. The subcommand stays silent when healthy.
"$PYTHON_BIN" "$ROLE_X_PY" coverage --since 7d 2>/dev/null || true

# Refresh the stamp regardless of whether we printed anything
mkdir -p "$(dirname "$STAMP_FILE")"
touch "$STAMP_FILE"

exit 0
133 changes: 133 additions & 0 deletions scripts/role-x.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ def cmd_index(args: argparse.Namespace) -> int:


CARVE_OUT_MIN_WORDS = 3 # prompts shorter than this skip the intake reflex

# v0.4.1 — "domain-rich" heuristic. When intake routes to _meta-only AND the
# prompt is non-trivial AND has enough distinct meaningful tokens, surface a
# one-line nudge: "consider role-x init <slug>". Closes the gap where agents
# saw _meta-only repeatedly but never aggregated the signal.
DOMAIN_RICH_MIN_WORDS = 8
DOMAIN_RICH_MIN_TOKENS = 4

# v0.4.1 — coverage thresholds for the SessionStart hook's silent-when-healthy
# logic. If recent fire-rate is at-or-above this floor AND there's no
# clusters-above-threshold visible, the coverage hook stays silent.
COVERAGE_HEALTHY_FIRE_RATE = 30 # percent
EVENTS_PATH = Path.home() / ".config" / "broomva" / "role" / "events.jsonl"

# v0.4.0 — observability config. Privacy-by-default: no sanitized prompt
Expand Down Expand Up @@ -651,9 +663,56 @@ def _format_intake_context(selection: dict) -> str:
"Agents: apply the quality_bar entries as the P14 enumeration template for this response. "
"If mode != augment, surface the rewrite/decompose proposal to the user before proceeding."
)
# v0.4.1: when no domain lens fired AND the prompt is domain-rich enough
# to plausibly merit one, surface a one-line "consider authoring a lens"
# nudge. Pure suggestion — agent decides whether to act on it.
nudge = selection.get("authoring_nudge")
if nudge:
lines.append("")
lines.append(nudge)
return "\n".join(lines)


def _domain_rich(prompt: str, word_count: int) -> bool:
"""Heuristic: is the prompt substantive enough to plausibly need a lens?"""
if word_count < DOMAIN_RICH_MIN_WORDS:
return False
tokens = {
tok.lower()
for tok in re.findall(r"[A-Za-z][A-Za-z0-9_-]+", prompt)
if len(tok) > 3 # filter trivial connectors
}
return len(tokens) >= DOMAIN_RICH_MIN_TOKENS


def _build_authoring_nudge(prompt: str, selection: dict) -> str | None:
"""v0.4.1 — return a 1-line role-x init suggestion when warranted."""
if selection.get("lenses_selected"):
return None # a domain lens already fired — no nudge
word_count = len(prompt.split())
if not _domain_rich(prompt, word_count):
return None
# Pick a short candidate slug from the prompt's most distinctive tokens
tokens = [
tok.lower()
for tok in re.findall(r"[A-Za-z][A-Za-z0-9_-]+", prompt)
if len(tok) > 3
]
# Dedupe in order
seen: set[str] = set()
ordered: list[str] = []
for tok in tokens:
if tok not in seen:
seen.add(tok)
ordered.append(tok)
slug = "-".join(ordered[:2]) if ordered else "candidate"
return (
f"Note: no domain lens scored ≥2 for this prompt. If this kind of "
f"work recurs, consider expanding the registry: "
f"`role-x init {slug}` (status: candidate)."
)


def cmd_intake(args: argparse.Namespace) -> int:
"""Intake subcommand — UserPromptSubmit hook entry point.

Expand Down Expand Up @@ -688,6 +747,8 @@ def cmd_intake(args: argparse.Namespace) -> int:

signals = _git_signals(workspace)
selection = _select_lenses(roles_dir, signals, prompt)
# v0.4.1: attach authoring nudge for _meta-only domain-rich prompts
selection["authoring_nudge"] = _build_authoring_nudge(prompt, selection)
_emit_event(session_id, prompt, selection)
print(_format_intake_context(selection))
return 0
Expand Down Expand Up @@ -879,6 +940,59 @@ def cmd_suggest(args: argparse.Namespace) -> int:
return 0


### coverage subcommand (v0.4.1 — SessionStart-friendly silent-when-healthy report) ###


def cmd_coverage(args: argparse.Namespace) -> int:
"""Brief registry-health summary suitable for a SessionStart hook.

Silent (exit 0, no output) when registry coverage looks healthy. Prints a
3-5 line nudge when fire-rate is below the floor OR when sanitized capture
is off so cluster discovery can't run.
"""
events_path = Path(args.events_path) if args.events_path else EVENTS_PATH
since_seconds = _parse_duration(args.since)
events = _read_events_since(events_path, since_seconds)

total = len(events)
if total < args.min_events and not args.force:
return 0 # not enough events to report meaningfully

fired = sum(1 for ev in events if ev.get("lenses_selected"))
unrouted = total - fired
pct_fired = (100.0 * fired / total) if total else 0.0

has_sanitized = any(
(ev.get("prompt_sanitized") or {}).get("strategy") == "keywords"
for ev in events
)

is_healthy = pct_fired >= COVERAGE_HEALTHY_FIRE_RATE and has_sanitized
if is_healthy and not args.force:
return 0 # silent — registry coverage looks fine

# Build a tight nudge (≤5 lines including the action hints)
print(
f"[role-x coverage] {total} events over {args.since}. "
f"Fire-rate: {pct_fired:.0f}% "
f"({'low' if pct_fired < COVERAGE_HEALTHY_FIRE_RATE else 'ok'})."
)
if not has_sanitized:
print(
" Sanitized prompt capture is OFF — cluster discovery disabled. "
f"Enable: {CONFIG_PATH}"
)
print(
' Body: {"capture_sanitized_prompt": true, '
'"sanitization_strategy": "keywords"}'
)
elif pct_fired < COVERAGE_HEALTHY_FIRE_RATE:
# We have sanitized data but coverage is still low — gesture toward suggest
print(" Run `role-x suggest` for emergent cluster + drift report.")
print(" Author next: `role-x init <name>` (status: candidate, promote on rule-of-three).")
return 0


### init subcommand (v0.4.0 — scaffold a new lens from CLI args) ###


Expand Down Expand Up @@ -1054,6 +1168,25 @@ def build_parser() -> argparse.ArgumentParser:
)
p_intake.set_defaults(func=cmd_intake)

p_coverage = sub.add_parser(
"coverage",
help="(v0.4.1) brief registry-health summary; silent when coverage is healthy",
)
p_coverage.add_argument(
"--since", default="7d", help="window (default 7d)",
)
p_coverage.add_argument(
"--events-path", default=None, help=f"path to events.jsonl (default: {EVENTS_PATH})",
)
p_coverage.add_argument(
"--min-events", type=int, default=10,
help="minimum events in window before reporting (default 10)",
)
p_coverage.add_argument(
"--force", action="store_true", help="always print, even when healthy",
)
p_coverage.set_defaults(func=cmd_coverage)

p_suggest = sub.add_parser(
"suggest",
help="(v0.4.0) analyze events.jsonl; suggest new lenses + per-lens drift signals",
Expand Down
Loading
Loading