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
2 changes: 1 addition & 1 deletion plugins/claude-ops/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "claude-ops",
"version": "0.20.0",
"version": "0.21.0",
"description": "Claude Code operations toolkit. Seven skills: observability (read locally captured telemetry — OTEL store, collector, hook-event JSONL, ccusage — with trend reports and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand — marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view — queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort and a repo-pull + marketplace-refresh launch step), and a re-runnable setup action that settles where the known-issues registry lives. Plus a family of seven advisory *-audit telemetry-emitter hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.",
"author": {
"name": "Melodic Software",
Expand Down
68 changes: 68 additions & 0 deletions plugins/claude-ops/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,74 @@
All notable changes to the `claude-ops` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.21.0]

### Added

- **`lanes` skill: `lane-launcher.sh` now captures and persists the launch commit
(`#792`).** `context/refresh.md`'s git staleness probe referenced a
`<lane-launch-commit>` placeholder with no producer — the repo HEAD when
`lanes start`/`restart` last ran was advisory-only, with no automated way to
retrieve it. `lane-launcher.sh` now captures `git rev-parse HEAD` right after
the pre-launch pull (a pure read, so it also previews correctly under
`--dry-run`) and writes it, for every lane actually (re)started that run, to
`<data-dir>/lanes/<lane>-launch-commit` — a lane `start` skips as
already-running keeps its existing marker untouched. New `--data-dir DIR`
option (default: the `$CLAUDE_PLUGIN_DATA` env var if set, else
`~/.claude/plugins/data/claude-ops`, matching `check-all.sh`'s convention).
`SKILL.md`'s invocation now passes `--data-dir "${CLAUDE_PLUGIN_DATA}"`
explicitly — per current
[plugins-reference](https://code.claude.com/docs/en/plugins-reference#environment-variables),
`CLAUDE_PLUGIN_DATA` is exported as a real env var only to hook/MCP/LSP
subprocesses, not to a script a skill shells out to via the Bash tool, so a
script-internal fallback alone would silently miss the marketplace-qualified
data directory in a real session. The write is best-effort: a failure (or an
unresolvable HEAD) warns on stderr but never fails an already-launched lane.
`context/refresh.md` and `SKILL.md` now point the probe at the real marker
file instead of the unfillable placeholder, with an explicit hex-only-input
note for anyone who later sources the value from something other than `git
rev-parse`, and a `tr -d '\r'` strip on the marker read (the repo's standing
CRLF-hazard convention for any captured Windows value). New regression cases
in `lane-launcher.test.sh` cover the capture/write, the
skip-if-already-running case, `--dry-run` (preview only, no write),
unresolvable-HEAD (best-effort, no failure), and the `$CLAUDE_PLUGIN_DATA`
fallback.
- The probe's `data_dir` is sourced from `SKILL.md`, which is the only surface
where it resolves. Per
[plugins-reference](https://code.claude.com/docs/en/plugins-reference#environment-variables),
`${CLAUDE_PLUGIN_DATA}` substitutes inline in *skill and agent content* but is
exported as a real environment variable only to hook and MCP/LSP subprocesses
— and `context/refresh.md` is read raw rather than rendered as skill content.
An env-var-with-fallback expression there would have silently resolved to the
unqualified `~/.claude/plugins/data/claude-ops` guess, read no marker, and
skipped the staleness check without saying so. `SKILL.md` now carries the
substituted `data_dir=` assignment and `context/refresh.md` points at it.
- A lane name is now validated as a single path component at config preflight
(exit `3` on `/`, `\`, `.`, or `..`). The name is the marker's filename, so
without that check two distinct configured lanes — `work` and
`group/../work` — would share one marker file and a targeted restart of
either would make the other's probe read a launch commit it never launched
at. Rejecting rather than encoding keeps the documented
`<data-dir>/lanes/<lane>-launch-commit` path literally true.
- The marker path is namespaced by repo
(`<data-dir>/lanes/<repo-key>/<lane>-launch-commit`). The data directory is
plugin-wide but a lane name is only unique within one repo, so a
conventional `work` lane in two checkouts would otherwise share a marker and
each repo's probe would diff against the other's unrelated history — usually
an invalid-revision error, at best a silently wrong answer. `<repo-key>` is
`git hash-object` over `git rev-parse --show-toplevel`: a digest rather than
a character fold, because folding collapses two real checkout paths like
`/repos/foo-bar` and `/repos/foo/bar` onto one key, and git's canonical
(symlink-resolved) toplevel rather than the `--repo` argument, because the
documented probe asks git directly and both sides must land on the same key.
Print the key for a checkout with
`printf '%s' "$(git rev-parse --show-toplevel)" | git hash-object --stdin`.
- A (re)start that cannot record its own commit (unresolvable HEAD, or a failed
write) now removes any marker the previous launch left. Leaving it made the
probe treat that older commit as the new session's launch point and report
already-consumed merges indefinitely; removing it degrades the probe to its
honest "no marker → skip" branch. `--dry-run` still touches nothing.

## [0.20.0]

### Added
Expand Down
15 changes: 11 additions & 4 deletions plugins/claude-ops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,17 @@ your own repository's context:
hooks emit it; every source degrades gracefully when absent.
- **Persistent state** defaults to the plugin's own per-machine data directory
(`${CLAUDE_PLUGIN_DATA}`): the known-issues registry
(`registry.json`), `check-all` output, and `--write` observability reports.
By default nothing is written into your repository. Opt in for the registry
via the `registry_dir` option (see Configuration) to keep it git-tracked and
team-shared inside your repo instead.
(`registry.json`), `check-all` output, `--write` observability reports, and
the `lanes` skill's per-lane launch-commit markers
(`${CLAUDE_PLUGIN_DATA}/lanes/<repo-key>/<lane>-launch-commit`, overridable
via `lane-launcher.sh --data-dir`). `<repo-key>` namespaces markers by
repository — the data directory is plugin-wide, while a lane name like `work`
is only unique within one checkout. It is a digest of the repository's
canonical path; print the one for a given checkout with
`printf '%s' "$(git rev-parse --show-toplevel)" | git hash-object --stdin`. By default nothing is written into your
repository. Opt in for the registry via the `registry_dir` option (see
Configuration) to keep it git-tracked and team-shared inside your repo
instead.
- **Work-item and docs integration.** Where the skills propose follow-up work
items or cross-reference quirks/workaround docs, they use whatever tracker
and docs your project has (e.g. `gh issue create`, your `CLAUDE.md` /
Expand Down
61 changes: 55 additions & 6 deletions plugins/claude-ops/skills/lanes/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,27 @@ begins with fresh context.
## Run it

```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/lanes/scripts/lane-launcher.sh" $ARGUMENTS
bash "${CLAUDE_PLUGIN_ROOT}/skills/lanes/scripts/lane-launcher.sh" --data-dir "${CLAUDE_PLUGIN_DATA}" $ARGUMENTS
```

Print the script's output verbatim — it is the deliverable. Preview any mutating
run first with `--dry-run` (prints the exact `claude`/`git` commands, seeds
nothing, kills nothing).

**`--data-dir` is passed explicitly, not left to the script's own
`$CLAUDE_PLUGIN_DATA` env-var fallback.** Per
[plugins-reference](https://code.claude.com/docs/en/plugins-reference#environment-variables),
`${CLAUDE_PLUGIN_DATA}` is exported as a real environment variable only to hook
processes and MCP/LSP subprocesses — for skill content it instead resolves by
**inline text substitution anywhere the placeholder appears** in the rendered
skill body, exactly like `${CLAUDE_PLUGIN_ROOT}` above. A script this skill
shells out to via the Bash tool does **not** inherit `CLAUDE_PLUGIN_DATA` as an
env var, so leaving `--data-dir` off here would silently fall through to
`lane-launcher.sh`'s own `~/.claude/plugins/data/claude-ops` guess instead of
the marketplace-qualified directory Claude Code actually resolves. `$ARGUMENTS`
comes after `--data-dir`, so an explicit `--data-dir` the caller passes in
`$ARGUMENTS` still wins (last flag wins in `lane-launcher.sh`'s parser).

## Action Router

Parse `$ARGUMENTS` for the action (first token); remaining tokens are lane names
Expand All @@ -85,8 +99,9 @@ Parse `$ARGUMENTS` for the action (first token); remaining tokens are lane names

Options: `--config FILE`, `--repo DIR`, `--no-pull`, `--no-update`, `--dry-run`,
`--agents-json FILE` (read the session list from a file instead of the live CLI —
offline/scripted reuse). Exit codes: `0` ok · `3` bad argument/config · `4`
prerequisite missing or repo/config unresolved.
offline/scripted reuse), `--data-dir DIR` (base dir for the per-lane
launch-commit marker; default `$CLAUDE_PLUGIN_DATA`). Exit codes: `0` ok · `3`
bad argument/config · `4` prerequisite missing or repo/config unresolved.

## Lane config

Expand All @@ -113,9 +128,32 @@ its launch-time plugin versions, `/loop` never re-reads a skill's body on later
cycles, and a loop can't self-trigger `/reload-plugins`). Restart is the honest
refresh mechanism — the same `restart` that clears context bloat (#496). Detect an
unconsumed self-fix with a read-only git probe against the repo's default branch,
then restart that lane at its next cycle boundary. Full reasoning, the probe, and the cadence
live in [context/refresh.md](context/refresh.md) — read it before answering "why is
my merged fix not live in the lane?" or setting a restart frequency.
then restart that lane at its next cycle boundary. The probe reads the launch
commit `lane-launcher.sh` records per lane at `start`/`restart`
(`${CLAUDE_PLUGIN_DATA}/lanes/<repo-key>/<lane>-launch-commit`, #792 — the data
directory is plugin-wide, so `<repo-key>`, a digest of the repo's canonical
path, keeps a conventional `work` lane in two different checkouts from sharing
one marker) — no manual fill-in needed.
Full reasoning, the probe, and the cadence live in
[context/refresh.md](context/refresh.md) — read it before answering "why is my
merged fix not live in the lane?" or setting a restart frequency.

**Carry this line into that probe** — it is the `data_dir` assignment
`context/refresh.md` deliberately leaves unresolved, because only skill content
(this file) substitutes the placeholder:

```bash
data_dir="${CLAUDE_PLUGIN_DATA}"
```

Copy it as it renders **here**, already substituted to an absolute path. Writing
the placeholder — or a `${CLAUDE_PLUGIN_DATA:-…}` env fallback — inside
`context/refresh.md` would not work: that file is read raw, and per
[plugins-reference](https://code.claude.com/docs/en/plugins-reference#environment-variables)
`CLAUDE_PLUGIN_DATA` reaches only hook and MCP/LSP subprocesses as a real
environment variable, never a script the Bash tool runs. The probe would then
read the unqualified `~/.claude/plugins/data/claude-ops` guess, find no marker,
and skip the staleness check silently.

## Verified CLI surface

Expand Down Expand Up @@ -146,6 +184,17 @@ only for a configured lane name.
in-flight conversation). Use `start` for "bring up whatever is down".
- **A missing/empty prompt file skips that lane** (with an error) rather than
launching an empty session. `status` flags `[prompt MISSING]`.
- **The launch-commit marker is per-machine and best-effort.** It lives under
`${CLAUDE_PLUGIN_DATA}` (a per-machine dir, not synced), so a lane restarted
on a different machine has no marker there yet. A write failure only warns —
it never fails an already-launched (or already-stopped-and-relaunched) lane —
so a missing marker means "never started here via `lane-launcher.sh`", not
"launcher broken". A (re)start that *cannot* record its commit also deletes
any marker the previous launch left, so "missing" always beats a stale commit
the probe would otherwise trust.
- **A lane name must be a single path component.** It is the marker's filename,
so config preflight exits `3` on a name containing `/` or `\`, or equal to `.`
or `..` — otherwise two distinct lanes could share one marker.

## Per-cycle deterministic scripts (#538)

Expand Down
9 changes: 9 additions & 0 deletions plugins/claude-ops/skills/lanes/context/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ Lane names are free-form (`work`, `work-2`, `babysit`, `decide`, …); nothing i
hardcoded. The set above mirrors the lanes this repo's telemetry conventions use,
but any names work — `status`/`stop` only ever act on names present in this config.

One constraint on the name, enforced at preflight: it is also the filename of the
lane's launch-commit marker (#792,
`<data-dir>/lanes/<repo-key>/<name>-launch-commit`), so it must be a single path
component. A name containing `/` or `\`, or equal to `.` or `..`, exits `3` —
without that check, `work` and `group/../work` would share one marker file and a
targeted restart of either would corrupt the other's staleness probe. The
`<repo-key>` component keeps same-named lanes in different repos apart, since the
data directory is plugin-wide rather than per-repo.

## Prompt-storage seam (#480)

`prompt_dir` defaulting to `.work` reflects today's reality: canonical prompts live
Expand Down
67 changes: 62 additions & 5 deletions plugins/claude-ops/skills/lanes/context/refresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,71 @@ Read-only, pure git — resolve the default branch rather than assuming `main`:
git fetch origin -q
# default branch of this repo — never hardcode main/master
default="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || echo origin/main)"
# data_dir: paste the `data_dir=…` line SKILL.md's "Mid-session staleness &
# restart cadence" section carries — it is the ONLY place the real directory is
# resolvable (see the paragraph below). Do not substitute an env var here.
# Markers are namespaced by repo: the data dir is plugin-wide, but `work` is a
# conventional lane name in every repo this launcher manages. Digest, not a
# character fold, so two paths differing only in a folded character keep
# distinct keys; keyed on git's canonical (symlink-resolved) toplevel, which is
# exactly what lane-launcher.sh hashes.
repo_key="$(printf '%s' "$(git rev-parse --show-toplevel)" | git hash-object --stdin)"
# the commit lane-launcher.sh recorded when this lane last (re)started (#792)
# tr -d '\r': strip a Windows CRLF read hazard on any captured value (the
# repo's standing convention — see the CHANGELOG's #1176/F2 note) before it
# reaches the git log range below.
lane_launch_commit="$(cat "$data_dir/lanes/$repo_key/<lane>-launch-commit" 2>/dev/null | tr -d '\r')"
# merged changes to the claude-ops plugin the running lane has NOT consumed
git log --oneline "<lane-launch-commit>..${default}" -- plugins/claude-ops/
[[ -n "$lane_launch_commit" ]] && git log --oneline "${lane_launch_commit}..${default}" -- plugins/claude-ops/
```

`<lane-launch-commit>` is the repo HEAD when `lanes start`/`restart` last ran (the
launch pulls first, so a running lane's skills correspond to that commit). Any
output = an unconsumed merge. Swap the pathspec for whichever installed plugin a
lane runs.
**`data_dir` comes from SKILL.md, not from the environment.** Per current
[plugins-reference](https://code.claude.com/docs/en/plugins-reference#environment-variables),
`${CLAUDE_PLUGIN_DATA}` is exported as a real environment variable only to hook
processes and MCP/LSP server subprocesses; for a plugin's **skill and agent
content** it instead resolves by inline substitution "anywhere the placeholder
appears". Both facts cut against resolving it here: this reference file is read
raw rather than rendered as skill content (so a placeholder written here would
not substitute), and the probe runs through the Bash tool (so the env var is
unset there). An env-var-with-fallback expression would therefore have silently
resolved to the unqualified `~/.claude/plugins/data/claude-ops` guess, missed
the marketplace-qualified directory Claude Code actually uses, read no marker,
and skipped the staleness check without saying so. SKILL.md — which *is* skill
content — carries the substituted `data_dir=` assignment; take it from there.

`<lane-launch-commit>` (substitute the lane's own name for `<lane>` above) is the
repo HEAD `lane-launcher.sh` captured when `lanes start`/`restart` last (re)started
that lane — written to `<data-dir>/lanes/<repo-key>/<lane>-launch-commit` right
after the launch's pre-launch pull, for every lane actually (re)started that run
(`start` leaves the marker untouched for a lane it skipped as already-running; a
(re)start that cannot record its own commit deletes the previous launch's marker
rather than leaving it to be misread as this session's launch point). The lane
name is the marker's filename, so config preflight rejects a lane name that is
not a single path component — the path above is literally true for every
accepted name. `<repo-key>` namespaces the marker by repo, because the data
directory is plugin-wide while a lane name is only unique within one repo: a
conventional `work` lane in two checkouts would otherwise share one marker and
each probe would diff against the other repo's unrelated history. It is a digest
of git's canonical toplevel rather than a readable slug, deliberately — a
character fold would collapse `/repos/foo-bar` and `/repos/foo/bar` onto one
key, and the canonical toplevel keeps a symlinked `--repo` argument pointing at
the same key both sides use. Recompute it for a by-hand inspection with the
`repo_key=` line above. An empty
`lane_launch_commit` means no marker exists for that
lane (never started/restarted through `lane-launcher.sh` on this machine, or the
last (re)start could not record one) — the probe has nothing to diff against and
is skipped rather than run against a resolved-empty range. Any probe output = an
unconsumed merge. Swap the pathspec for whichever installed plugin a lane runs.

**Not an injection vector today, but treat it as untrusted if that ever changes.**
`lane-launcher.sh` writes `lane_launch_commit` from `git rev-parse HEAD` only — a
bare hex SHA, so reading it back and interpolating it unquoted into `git log
"${lane_launch_commit}..${default}"` above carries no shell-injection risk. If a
future change ever sources this value from something other than `git rev-parse`
(external input, a hand-edited marker file, anything not mechanically
hex-constrained), that value must never be interpolated unquoted into the probe
command — validate it (e.g. `[[ "$lane_launch_commit" =~ ^[0-9a-f]{7,64}$ ]]`)
before it reaches `git log`.

**Not an `!` injection candidate.** This probe is deliberately a body instruction,
not `!` dynamic-context injection — it fails every condition of the precompute
Expand Down
Loading
Loading