sshlg-skills 1.63.3 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,159 @@
1
+ ## v1.65.0 — foreign text no longer stops an update; WebKit-first members; OpenClaw opt-in
2
+
3
+ From the 2026-10-16 host validation ([report](docs/reports/2026-10-16-coding-agent-compatibility/README.md),
4
+ brief `docs/evidence/briefs/2026-10-16-host-validation-release.md`).
5
+
6
+ - **Members — WebKit (Safari) first in every browser check.** task-pipeline 1.97.1 (`references/browser.md`
7
+ § Engine order, `companion-skills.md`, DEC-0007), sheleg-design 1.65.0 (visual review, capture record `engine`),
8
+ seo-aeo-audit 0.27.0 (Lighthouse/PSI/CrUX stated as Chrome-only measurement; its validator reads number words
9
+ past forty), sheleg-dev 0.14.0 (frontend-performance), super-ux 0.60.0 (ux-audit `references/browser-engine.md`).
10
+ Chromium stays a stated exception; an own Chrome runs on a temporary profile and is closed when done.
11
+ - **Operator text inside a managed block no longer stops the whole run (B-201).** Text placed inside
12
+ the `wait-is-not-a-stop` block made `update`/`install` exit 2 on every host and name no file.
13
+ Now that one host is refused, its file is left untouched, the message names `file:line` and the
14
+ remedy (move the text outside the markers), every other step runs, and the run exits 1 (partial).
15
+ `blocks` reports `FOREIGN_TEXT_INSIDE <marker> lines a-b` without printing the text.
16
+ - **A router section you edited is kept, never silently deleted (B-202).** `routers` records what it
17
+ wrote (`~/.sshlg-skills/routers-rendered.json`); a section that matches neither that record nor the
18
+ new text is kept and recorded as `authored` (`--diff`, `--adopt` work on it); an edited
19
+ MAP/PROTOCOL/TABLE region is refused with `file:line` until the lines move out.
20
+ - **The wait rule names a host it has not reached (B-208).** With `rules.wait=on`, `update` prints the
21
+ instruction file and the exact `rules --apply --agent <host>` command for a supported host without a
22
+ receipt, instead of staying silent; consent is not widened automatically.
23
+ - **Claude Code 2.2 and later no longer stop plugin updates (B-203)**: the pin transaction accepts any
24
+ version from 2.1.296 on by numeric comparison. **No stray `~/.claude/.claude.json` (B-220)**:
25
+ `CLAUDE_CONFIG_DIR` is passed to `claude plugin` only for a non-default profile.
26
+ - **Gemini relocation follows `GEMINI_CLI_HOME` (B-204)**, a HOME replacement (`<home>/.gemini`,
27
+ `<home>/.agents/skills`); the non-existent `GEMINI_CONFIG_DIR` is no longer read.
28
+ - **OpenClaw is opt-in (B-219):** out of `defaultAgents` (the skills CLI detects it by folder only, and
29
+ its prompt caps — 150 skills, 18,000 characters — would hide most of the family); `--agent openclaw`
30
+ still works.
31
+ - **Continue copies only when Continue is there (B-213):** `update` adds the `--agent continue --copy`
32
+ pass only when Continue is installed and in the requested scope.
33
+ - **README corrections (B-215):** which hosts read which instruction file, that Cursor and the
34
+ Continue CLI can import the Claude hooks, Cursor rules load by ancestor walk, universal adapters
35
+ install into `~/.agents/skills` only, `skillOverrides` does not hide plugin skills, Codex invokes
36
+ skills as `$name`.
37
+
38
+ - **A credential printed under its label is named in the same turn (I-6 remainder, OPS-002).**
39
+ `secrets.scan` knew vendor prefixes only, so a key with none — a 40-character hex token, a
40
+ webhook secret, a JWT — printed as `x-api-key: …`, `FOO_API_KEY=…`, `"apiKey": "…"` or
41
+ `Authorization: Bearer …` went unreported. `lib/labelledsecrets.js` reads the label and the
42
+ value's shape: the label's noun must name a credential (token, secret, password, or `key`
43
+ with a credential qualifier such as api, client or an `x-` header), and the value must look
44
+ generated (16–4,096 characters, a letter and a digit, at least 3 bits of entropy per
45
+ character, not a placeholder, path, URL, number, timestamp, snake_case name, sequential
46
+ example or code). The `PostToolUse` note names the label, the tool and the file or program —
47
+ never the value or any slice of it — and the remedy: rotate through the owner's door
48
+ (Observatory), and the step for a machine without Observatory.
49
+ - `PostToolUse` now runs on `Read` and `Grep` as well as `Bash`, so a config file read whole
50
+ is scanned like `cat`. MCP results are not matched (every MCP call would pay a spawn);
51
+ `update` refreshes the matcher on a machine that already has the hook.
52
+ - Also read: escaped-quote JSON in a string (an `aws secretsmanager` SecretString),
53
+ `.netrc`, URL userinfo (`scheme://user:PASSWORD@host`, `DATABASE_URL=…`), labels starting
54
+ with `_` (`.npmrc` `:_authToken=`), base64 values starting with `/`, and Laravel's
55
+ `APP_KEY=base64:…`. Left out on purpose: a short password with symbols under a label (the
56
+ token alphabet stops at a symbol; widening it reads prose and code as values).
57
+ - **A local proxy's token is not a vendor key.** Switchboard's managed sessions (SB-118) put
58
+ `ANTHROPIC_AUTH_TOKEN` beside `ANTHROPIC_BASE_URL=http://127.0.0.1:…` in every project's
59
+ `.claude/settings.local.json` (93 of the 150 such files under the operator's projects). A
60
+ `<P>_AUTH_TOKEN` or `<P>_API_KEY` on an active line is not reported only when, in the same
61
+ file (a Grep `path:line:` prefix, a `grep --heading` path line, a `==> path <==` header or a
62
+ heredoc body starts another), an active line within 20 lines sets `<P>_BASE_URL` to 127.x,
63
+ `localhost` or `[::1]` with an optional port and a path free of `@`, `\`, quotes, `,` and
64
+ `}` (a quoted URL ends at its quote, and what follows the quote must close the value), no
65
+ other `<P>_BASE_URL` of that prefix appears at all, and there are no more such key lines,
66
+ commented ones included, than loopback base URLs. A commented key always reports. A
67
+ commented loopback, an override, concatenated `.env` files, Grep lines from two files, a
68
+ key 30 lines away, a heredoc then an export, and `@`-, quote- and brace-tailed hosts all
69
+ report (rounds 2 and 3: they hid legacy `sk-`, DeepSeek, Groq and Stripe keys that
70
+ `secrets.scan` has no literal for). Disclosed limit: output that concatenates files with no
71
+ marker between them is one file to this rule; the count and distance rules keep a second
72
+ file's key reported there.
73
+ - Round 2 also: the one-identifier skip needs a real case transition (`nM` in
74
+ `AuthenticationMD5Password`), so base32, lower-case alphanumerics and word-digit-word
75
+ passwords are never skipped, and a quoted flag value is never skipped. Helm/Kubernetes
76
+ object names (`s3-backup-credentials-prod`) are names. A URL password is reported under
77
+ the assignment's name only when it names a credential or a connection (`*_URL`, `*_URI`,
78
+ `*_DSN`), else as `<scheme>-url-password`. A long key with no digit (about one Stripe key in
79
+ 70) is read when it breaks into short case runs. Labels are captured atomically: `-a-a…`
80
+ filler 174 → 60 ms a MiB. A push registration token (`deviceToken`) is not reported; a
81
+ session's `resumeToken` is; a URL password under 12 characters (`p4ssw0rd` in docs) is not.
82
+ The PARTIAL note names its cause: a field over 1 MiB, or the 4 MiB budget across fields.
83
+ - Measured before shipping on 29,827 files, 440 MB (this repository with its members,
84
+ Observatory's docs and tools, and eight other repositories on the operator's machine),
85
+ scanned in 17 s. The false-positive classes found are fixtures now: a registry's
86
+ `credential:<name>` ids, a vault slot name `project/env/NAME`, a publishable `pk_` key in
87
+ an image URL, a function name or a one-identifier value (`AuthenticationMD5Password`) as a
88
+ value, a URL whose user and host say it is fake, a k8s object name. 13 files still report. One is a disclosed
89
+ residual class: a git SHA under a name ending in `_token` cannot be told from a hex token by
90
+ its shape. The other 12 hold a generated-looking value under a credential label: configs,
91
+ documentation examples and test sentinels. The detector reads shape, not provenance, and
92
+ its note says so.
93
+ - Round 3 also: values up to 4,096 characters (RS256 bearers and `id_token`s of 600–2,500,
94
+ an 800-character `aws_session_token`); an `ENV_VAR_NAME_2` is a name; a label segment of
95
+ eight or more characters is printed whole only when every word in it is known label
96
+ vocabulary, otherwise as its first three characters and `*` (a label can be glued to its
97
+ value), and a URL password's scheme only from a list of known schemes.
98
+ - The note leads with the step that needs no Observatory, as `test/remedies_test.js` holds
99
+ every refusal to; that suite now reads this note through the real hook process too (B-195).
100
+ - **Bounded, and the vendor scan unchanged.** `secrets.scan` reads every byte of every string
101
+ of the response, as before; only the labelled detector windows each string (1 MiB whole,
102
+ else its first 768 KiB and last 256 KiB; 4 MiB in all), and its note says when a scan was
103
+ partial, also when it found nothing, or when its 5 s time budget in the hook ran out — the
104
+ vendor note is never lost to it. Values and labels are captured without backtracking: a MiB
105
+ of `token … =a=a…` takes 13 ms (2.4 s in the first cut). The file-scope reader is linear:
106
+ a `_BASE_URL` beside a MiB of colon-free, slash-dense output took 101 s in round 3 (past the
107
+ host's 60 s hook timeout, losing the vendor note) and now takes milliseconds. The note names the program by argv[0]
108
+ only, so a second word of the command (which can be a short secret) is never repeated. A
109
+ detector that throws costs neither the turn nor the vendor scan.
110
+
111
+ ## v1.64.0 — fewer tokens before the first word; work stops at its finish line
112
+
113
+ - `npx sshlg-skills context` measures what every host loads before the first user word:
114
+ bytes, approximate tokens and lines per instruction file, the hub's blocks against other
115
+ text, and documented caps. It reports a Codex instruction chain over `project_doc_max_bytes`
116
+ (Codex stops adding instruction files there; the default is 32 KiB) as an error and a
117
+ `CLAUDE.md` over Claude Code's 200-line target as a note. Read-only; prints sizes, never text.
118
+ - The routing text may fall and never grow by accident: `routing-registry-total` in DOCMAP is
119
+ checked on every run. The per-prompt routing note is one header line shorter.
120
+ - The wait rule gains the finish line (revision wait-v2): continuing is not polishing — a unit
121
+ ends when its exit checks are green and two rounds found nothing blocking; a finding that
122
+ blocks nothing becomes a board row. It also says a board row addressed to you is a next
123
+ task, never to end a turn on "shall I continue?", to release a lease as soon as the wait
124
+ clears, and lists spend above the limit and the operator's recorded decisions as human gates
125
+ (B-196, B-197). Profiles that applied wait-v1 are upgraded in place, and
126
+ a crash left by a wait-v1 launcher still recovers. A record from a newer launcher names
127
+ `npx --yes sshlg-skills@latest` as the remedy. Launchers up to 1.63.3 cannot read a wait-v2
128
+ receipt; update with `@latest`.
129
+ - task-pipeline is pinned at 1.96.0 (`2674147`): an iteration fixes its finish line at intake,
130
+ triages every finding as blocking, deferred or noise, and stops after two rounds with nothing
131
+ blocking or when its round and release budget is spent; `waves.py finish` computes the verdict.
132
+ - New working rule: [context economy](docs/working-rules/context-economy.md) — what belongs in
133
+ always-loaded instructions, one home per rule, host caps, and stopping by contract.
134
+ - **`blocks install` / `uninstall` / `resolve` — a writer other tools can call for their
135
+ own block (managed-blocks/1).** Preview by default; with `--apply` the host file is
136
+ copied to `~/.sshlg-skills/backups/`, only the named block changes, and every step is
137
+ journaled in `~/.sshlg-skills/managed-blocks/journal.jsonl` (hash-chained, no block
138
+ text). It refuses the hub's own blocks, `SSHLG:REPOSITORY-HANDOFF` (owner undecided,
139
+ B-174), other `SSHLG:` names and an `--owner` that differs from the marker's prefix.
140
+ A `journal.head` beside the journal catches cut-off trailing records; every failure
141
+ answers in one JSON/text shape that names `blocks resolve` when a write may have landed.
142
+ Install adds exactly the block and its line ending (CRLF in CRLF files) and uninstall
143
+ takes exactly those back; a result that would not parse, a read-only file and a
144
+ dropped in-block edit are refused or warned about before the write.
145
+ The convention is `docs/working-rules/managed-blocks.md`. `blocks --json` now names each
146
+ block's registered owner and writer; `OBSERVATORY:AGENT-UPDATES` is registered as
147
+ Observatory's. `blocks --help` and `blocks <verb> --help` answer.
148
+ - **`blocks` ignores markers inside closed Markdown code fences:** an example of a block
149
+ in prose is no longer listed or treated as a stray marker.
150
+ - **The DigitalOcean credential refusal leads with the step that needs no Observatory**
151
+ (a file with mode 600), with the vault named as the alternative. A new suite,
152
+ `test/remedies_test.js`, holds every secret-guard refusal to an Observatory-free step
153
+ before any Observatory step (USR-011); it reads only the remedy part of each refusal, every
154
+ `rule` key, bare or quoted, refusing a spelling it cannot read (a variable, the
155
+ `{ rule }` shorthand), and every deny site of the PreToolUse hook.
156
+
1
157
  ## v1.63.3 — pinned sources for every Claude member; blocked members no longer stop the run
2
158
 
3
159
  - A Claude member whose installed bytes already equal the pin is now rebound to the hub's
package/README.md CHANGED
@@ -20,10 +20,13 @@ every routing rule verbatim, and [`llms.txt`](https://skills.sshlg.me/llms.txt)
20
20
  for the machines. · **[Follow @sshlg93 on X](https://x.com/intent/follow?screen_name=sshlg93)**
21
21
 
22
22
  Works with **Claude Code** (as plugins) and **DeepSeek Harness** (`dsh`), plus
23
- Cursor, OpenCode, Codex, Kilo, Kimi, Hermes, OpenClaw, Gemini CLI, Windsurf, Zed
23
+ Cursor, OpenCode, Codex, Kilo, Kimi, Hermes, Gemini CLI, Windsurf, Zed
24
24
  and other targets among the 79+ agents the vercel `skills` CLI supports.
25
- The pinned snapshot has 77 global targets; native extension support is checked
26
- separately in the [compatibility report](docs/reports/2026-10-09-coding-agent-compatibility/README.md).
25
+ Kimi gets the skills only: the hub has no instruction target for it, so the
26
+ routing block reaches it only through a file you keep yourself
27
+ ([which](#routing--making-the-family-engage-by-default)). OpenClaw is opt-in (`--agent openclaw`,
28
+ [why](#install)). The pinned snapshot has 77 global targets; native extension
29
+ support is checked separately in the [compatibility report](docs/reports/2026-10-16-coding-agent-compatibility/README.md).
27
30
 
28
31
  ---
29
32
 
@@ -43,13 +46,13 @@ scripts. No services, no telemetry, no API keys.
43
46
 
44
47
  | Skill | Version | What it does |
45
48
  |---|---|---|
46
- | **[super-ux](https://github.com/ssheleg/super-ux)** | 0.59.3 | Scenario-driven UI development. A versioned design chain in `docs/ux/` — the product vision → personas and jobs → user flows → a screens-and-states map with Figma frames → traced scenarios → evidence-backed audits → fix plans, plus `docs/brand/` for how the product speaks. One `/ux` entry point that reaches every skill, two doc-drift linters and a contract doctor. |
47
- | **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | 1.95.0 | Full-cycle delivery orchestrator. An intake grill interrogates the request into a complete brief, then **ten gated stages** carry it — docs, brainstorm and decompose, spec, plan, build, tests, deploy, post-deploy, wiki, acceptance — refusing to advance until each gate passes. Documentation is a deliverable with its own portable gate, and the retrospective it leaves behind is traceable to the commit that earned each lesson. Ships `project-audit`: a cold-start audit of a whole project — discovery, probes whose unmet needs read `blind` rather than clean, production evidence including one artefact served under two channels, and an HTML report beside a JSON sidecar that says what moved. |
49
+ | **[super-ux](https://github.com/ssheleg/super-ux)** | 0.60.0 | Scenario-driven UI development. A versioned design chain in `docs/ux/` — the product vision → personas and jobs → user flows → a screens-and-states map with Figma frames → traced scenarios → evidence-backed audits → fix plans, plus `docs/brand/` for how the product speaks. One `/ux` entry point that reaches every skill, two doc-drift linters and a contract doctor. |
50
+ | **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | 1.97.1 | Full-cycle delivery orchestrator. An intake grill interrogates the request into a complete brief, then **ten gated stages** carry it — docs, brainstorm and decompose, spec, plan, build, tests, deploy, post-deploy, wiki, acceptance — refusing to advance until each gate passes. Documentation is a deliverable with its own portable gate, and the retrospective it leaves behind is traceable to the commit that earned each lesson. Ships `project-audit`: a cold-start audit of a whole project — discovery, probes whose unmet needs read `blind` rather than clean, production evidence including one artefact served under two channels, and an HTML report beside a JSON sidecar that says what moved. |
48
51
  | **[agent-sync](https://github.com/ssheleg/agent-sync)** | 1.22.2 | Several agents, one repository, no collisions. Leases with a TTL so two agents cannot claim the same work, race-free id reservation, a run journal and a generated board — over a pluggable knowledge cloud. The answer to "two sessions just committed over each other". |
49
52
  | **[make-skill](https://github.com/ssheleg/make-skill)** | 0.31.2 | A skill that builds skills. Create, retrofit, audit and publish agent skills and Claude Code plugins: conformance to the [Agent Skills](https://agentskills.io/specification) open standard, [Anthropic's platform rules](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) (per-surface runtime limits, the Skills API, evals) and the [Claude Code plugin reference](https://code.claude.com/docs/en/plugins-reference), marketplace layout, version sync, validator + CI, every distribution channel, the review checklist for third-party skills, and what a *skill author* must know about MCP and A2A — the protocols themselves live in `agent-stack`. |
50
- | **[sheleg-design](https://github.com/ssheleg/sheleg-design-skill)** | 1.64.2 | The taste layer. Cinematic scroll-driven landing pages — one scroll clock, motion that degrades to calm, WebGL particle formations — plus product-UI style packs each shipping a ready token layer, and the Figma border: tokens as variables, design to code without hand-copied values. |
51
- | **[seo-aeo-audit](https://github.com/ssheleg/seo-aeo-audit)** | 0.26.6 | Evidence-first website audit for search **and** answer engines. Ten tracks from crawl access to AI citation mechanics; every finding carries an observation, every recommendation an evidence tier, and the output is a prioritized change plan plus a link-building brief — not a score. |
52
- | **[sheleg-dev](https://github.com/ssheleg/sheleg-dev)** | 0.13.4 | The integration layer a product reaches once it has users: **money in, tracking, sign-in, speed**. Stripe subscription billing reconciled into your own database — webhook idempotency, renewals, seats and proration, refunds, price drift, and the coupon offered at the cancel step, where a `duration=once` discount leaves `subscription.discounts` the moment its invoice finalizes and the offer can be taken every cycle; crypto payments that survive under-payment, duplicate webhooks and rate drift; GA4, Google Ads, Meta and LinkedIn under Consent Mode v2; Google sign-in with the account pre-hijacking guard, and the server-side auth surface behind it; Core Web Vitals work that moves the score rather than the report; and Sentry error tracking wired so it does not forward your own credentials to a third party and so a stack trace names the commit that caused it. |
53
+ | **[sheleg-design](https://github.com/ssheleg/sheleg-design-skill)** | 1.65.0 | The taste layer. Cinematic scroll-driven landing pages — one scroll clock, motion that degrades to calm, WebGL particle formations — plus product-UI style packs each shipping a ready token layer, and the Figma border: tokens as variables, design to code without hand-copied values. |
54
+ | **[seo-aeo-audit](https://github.com/ssheleg/seo-aeo-audit)** | 0.27.0 | Evidence-first website audit for search **and** answer engines. Ten tracks from crawl access to AI citation mechanics; every finding carries an observation, every recommendation an evidence tier, and the output is a prioritized change plan plus a link-building brief — not a score. |
55
+ | **[sheleg-dev](https://github.com/ssheleg/sheleg-dev)** | 0.14.0 | The integration layer a product reaches once it has users: **money in, tracking, sign-in, speed**. Stripe subscription billing reconciled into your own database — webhook idempotency, renewals, seats and proration, refunds, price drift, and the coupon offered at the cancel step, where a `duration=once` discount leaves `subscription.discounts` the moment its invoice finalizes and the offer can be taken every cycle; crypto payments that survive under-payment, duplicate webhooks and rate drift; GA4, Google Ads, Meta and LinkedIn under Consent Mode v2; Google sign-in with the account pre-hijacking guard, and the server-side auth surface behind it; Core Web Vitals work that moves the score rather than the report; and Sentry error tracking wired so it does not forward your own credentials to a third party and so a stack trace names the commit that caused it. |
53
56
  | **[agent-stack](https://github.com/ssheleg/agent-stack)** | 0.28.2 | Production patterns for agent systems, in four skills. **The orchestrator** — tool-calling loops that survive their own context pressure, pipelines with human checkpoints and resume, provider routing with fallback and health checks, memory architecture — form, function and dynamics, the four-stage retrieval pipeline, and what a layer table alone cannot say, and **the shape of the work decided before the work**: an edge that carries no data is no edge, a plan that declares its dependencies is executed in layers rather than in list order, and a parallel layer gets a checker before the node that consumes it. **The harness** — what the agent is *told*: system prompts at the right altitude, tool descriptions the model can act on, technique choice with a verdict each, workflow versus agent and static versus dynamic, plus a seven-track audit of an agent somebody else built and a scanner for the defects that are mechanically visible. **The evals** — suites that measure whether it actually works: judging trajectories, regression fixtures grown from production, judges calibrated before they are trusted. **The interop layer** — MCP at revision 2026-07-28 and the four features it deprecated, running many servers at once, the registry, A2A 1.0, and the gateway, every reference carrying the spec revision it was read against. Plus the wallet side of reselling LLM access: tiered balances, one markup boundary, two-phase commit against a provider API. |
54
57
  | **[telegram-dev](https://github.com/ssheleg/telegram-dev)** | 0.2.4 | Telegram, split by the API each surface actually speaks. **`telegram-bots`** — the official HTTP Bot API: `update_id` as the only idempotency key an update carries, the `allowed_updates` default that subscribes you to everything *except* `chat_member` and the two reaction types while answering `ok: true`, the webhook secret header that is the real check, rate limits as a design constraint (1/sec per chat, 20/min per group, ~30/sec bulk), and Telegram Stars with a **ten-second** pre-checkout window and a grant that belongs to `successful_payment`. **`telegram-userbots`** — MTProto and Telethon: the session file as a credential equal to the password, `FloodWaitError` as the API telling you exactly how long to wait, pinning across minor releases that move session and entity-cache behaviour, and the account-ban risk a bot token does not carry — the skill opens with whether a user account is needed at all. **`telegram-miniapps`** — the web layer, whose entire security model is one signed query string: verifying `initData` server-side, the `auth_date` window, the Ed25519 path for a third party, and the SDK package whose name moved while the organisation's own page still fronts the old one. Both the verifier and the delivery invariants ship as runnable fixtures with their mutants. |
55
58
  | **[xr-dev](https://github.com/ssheleg/xr-dev)** | 0.3.4 | **`quest-lifecycle`** maps the product from platform choice through design, build, testing, Store, monetization, launch and operation. The existing owners cover native OpenXR, Kotlin Spatial/hybrid apps, measured rendering optimization, current-source/tool discovery, commercial and asset readiness, and WebXR/PWA. Every stage names evidence and a next action; Meta engine companions and official Godot XR documentation supply implementation detail. |
@@ -132,14 +135,24 @@ Flags: `--agent a,b` picks valid global targets, `--all` selects the pinned CLI
132
135
  77 globally supported targets (76 while Claude uses plugins),
133
136
  `--no-claude` skips the plugin step, `--claude-only` does nothing else.
134
137
 
138
+ **OpenClaw is opt-in:** `--agent openclaw` (or `--all`) installs for it; a plain
139
+ `install` or `update` does not. The skills CLI counts OpenClaw as present whenever
140
+ a `~/.openclaw` folder exists, so a leftover folder kept a dead host in every run.
141
+ And OpenClaw's caps would show only part of the family even where it runs: at most
142
+ 150 skills and 18,000 characters in the prompt, and 300 candidates and 200 loaded
143
+ skills per root. Raise its `skills.limits.*` before you opt in.
144
+
135
145
  ### What `install` actually does
136
146
 
137
147
  - **Claude Code** → each skill as a **plugin** (`claude plugin marketplace add` +
138
148
  `claude plugin install`), never as a plain `~/.claude/skills/` copy.
139
149
  - **Other installer targets** → the vercel
140
- [`skills`](https://github.com/vercel-labs/skills) CLI, using shared storage and
141
- each adapter’s global destination, with repeated `--agent` flags. Hermes and
142
- Kiro use native roots; shared `~/.agents/skills` loading is host-specific.
150
+ [`skills`](https://github.com/vercel-labs/skills) CLI, using shared storage, with
151
+ repeated `--agent` flags. A *universal* adapter (Cursor, Gemini CLI, OpenCode,
152
+ Kilo, GitHub Copilot, Droid and the like) installs into `~/.agents/skills` only,
153
+ never into its own declared global folder (skills@1.7.2 `dist/cli.mjs:2337-2341`),
154
+ so such a host sees the family only if it reads `~/.agents/skills`. Other adapters
155
+ get their own global destination; Hermes and Kiro use native roots.
143
156
  - **Unsupported global targets** → Eve and PromptScript are excluded from
144
157
  `--all`; explicit unknown or project-only names fail before installation.
145
158
  The [dated compatibility report](docs/reports/2026-10-09-coding-agent-compatibility/README.md)
@@ -439,10 +452,15 @@ Five channels get it, and each only where that agent is actually installed:
439
452
  | Agent | Where |
440
453
  |---|---|
441
454
  | Claude Code | `~/.claude/CLAUDE.md` |
442
- | Codex | `~/.codex/AGENTS.md` |
443
- | Gemini CLI | `~/.gemini/GEMINI.md` |
455
+ | Codex | `~/.codex/AGENTS.md` (Codex invokes a skill as `$name`; the block's `/name` is Claude's syntax) |
456
+ | Gemini CLI | `~/.gemini/GEMINI.md` (or `$GEMINI_CLI_HOME/.gemini/GEMINI.md`) |
444
457
  | Pi | `~/.pi/agent/AGENTS.md` (or `$PI_CODING_AGENT_DIR/AGENTS.md` — the writer, `uninstall` and the write guard all follow it) |
445
- | Cursor | `~/.cursor/rules/sshlg-routing.mdc` — one file per rule, `alwaysApply: true` |
458
+ | Cursor | `~/.cursor/rules/sshlg-routing.mdc` — one file per rule, `alwaysApply: true`. Cursor finds it by walking up from the workspace, so it loads only for workspaces under `$HOME` |
459
+
460
+ Other hosts read these files without the hub writing for them. OpenCode and VS Code
461
+ Copilot read `~/.claude/CLAUDE.md` as a fallback; OpenCode stops as soon as a
462
+ `~/.config/opencode/AGENTS.md` exists. `~/.agents/AGENTS.md` is read by Kimi, Goose,
463
+ Warp and Droid; the hub does not write it, so today it holds what you put there.
446
464
 
447
465
  **Pi** (`@earendil-works/pi-coding-agent`, command `pi`) reads skills from `~/.pi/agent/skills`,
448
466
  `~/.agents/skills` and, per project, `.pi/skills` and `.agents/skills` up to the repository root;
@@ -514,6 +532,19 @@ back the wrong text. Your word still wins; silence stops being the decision.
514
532
  out and which survives a reinstall and a restored dotfile. Everything outside
515
533
  the sentinels is preserved byte for byte.
516
534
 
535
+ **Text you put inside the block is kept, not refreshed away.** Each write records
536
+ a hash of every section and of the map, protocol and table regions it wrote, in
537
+ `~/.sshlg-skills/routers-rendered.json`. A later refresh that finds a section or
538
+ region matching neither that record nor the new text, and that would drop a
539
+ non-blank line, leaves it as it stands and names `file:line` in the preview and
540
+ the run. A kept **section** becomes your authored text for that router, so
541
+ `routers --diff <name>` and `--adopt <name>` apply to it. A kept **region** is a
542
+ refused refresh: the run exits 1 until you move those lines outside the block.
543
+ Without a record (a block an older release wrote), any section or region that
544
+ would lose a line is kept the same way, so an older packaged wording can stay
545
+ until you adopt the current one. `npx sshlg-skills blocks` reports such text as
546
+ `FOREIGN_TEXT_INSIDE` with its line range, without printing it.
547
+
517
548
  ### Turning routers off
518
549
 
519
550
  ```bash
@@ -542,8 +573,17 @@ npx sshlg-skills rules --apply --agent codex
542
573
  Supported destinations are existing Claude Code, Codex, Gemini, Pi and Cursor
543
574
  instruction roots. Use `--claude-only` for Claude Code; `--agent` selects other
544
575
  hosts. A later `update` refreshes only profiles with a prior ownership receipt.
576
+ It does not extend the rule to a host added later: with `rules.wait` on, `update`
577
+ names each such host's instruction file and the command that applies it there
578
+ (`npx sshlg-skills rules --apply --agent pi`; `--claude-only` for Claude Code).
579
+ That stays a named gap rather than an automatic write because consent to the rule
580
+ is recorded per host, and an update never creates it for a new one.
545
581
  Set `rules.wait off` and apply the same scope to remove its owned text. Surrounding
546
582
  content is preserved; malformed, edited or retargeted state stops for recovery.
583
+ Text placed inside an applied block refuses that block on that host only:
584
+ `install`, `update` and `uninstall` name `file:line` and the remedy (move the text
585
+ outside the markers), run every other step and exit 1. An explicit `rules --apply`
586
+ still refuses as a whole and writes nothing.
547
587
  The rule describes continuation and durable handoff. It creates no watcher or job,
548
588
  and sign-ins, consent, payment and action-specific authority keep their own gates.
549
589
 
@@ -614,7 +654,9 @@ hides every other installed pack; an existing file is replaced only with `--repl
614
654
 
615
655
  **Which switch each host gets, and what decides it:**
616
656
 
617
- - **Claude Code and Codex** hide every skill outside the usage core, as since v1.55.0.
657
+ - **Claude Code and Codex** hide every skill outside the usage core, as since v1.55.0,
658
+ with one exception on Claude Code: `skillOverrides` does not apply to plugin skills,
659
+ so a plugin's skills stay listed (switch a foreign plugin off with `enabledPlugins`).
618
660
  The pack marks and the overrides below do not change them; `--keep <skill>` keeps a
619
661
  skill listed there.
620
662
  - **Gemini, Pi, OpenCode and Hermes** hide the skills of marked packs, except the usage
@@ -634,7 +676,7 @@ npx sshlg-skills visibility --revert # remove exactly what --apply wrote; with
634
676
 
635
677
  | Host | The switch `visibility` writes | Source | A hidden skill can still |
636
678
  |---|---|---|---|
637
- | Claude Code | `skillOverrides: "user-invocable-only"` in `~/.claude/settings.json` | since v1.55.0 | be run from the `/` menu by a person |
679
+ | Claude Code | `skillOverrides: "user-invocable-only"` in `~/.claude/settings.json`; plugin skills are out of its reach | since v1.55.0; Claude Code settings reference: overrides do not apply to plugin skills | be run from the `/` menu by a person |
638
680
  | Codex | `enabled = false` entries in `~/.codex/config.toml` `[skills] config` | since v1.55.0 | be read from its SKILL.md path when asked |
639
681
  | Gemini CLI 0.63.0 | `skills.disabled` in `~/.gemini/settings.json`, the list `gemini skills disable <name> --scope user` writes; read back with `gemini skills list` (`[Disabled]`) | `packages/cli/src/utils/skillSettings.ts`; `SkillManager.getSkills()` drops disabled skills | come back with `gemini skills enable <name>` |
640
682
  | Pi 1.1.0 | `-<path>` exact exclusions in the `skills` array of `~/.pi/agent/settings.json`, one per folder holding the skill (`~/.agents/skills`, `~/.pi/agent/skills`); the file is created if absent and removed again by `--revert` | Pi's own docs, settings → Resources; `isEnabledByOverrides` in its `dist/core/package-manager.js` | be loaded for one session with `pi --skill <path>` |
@@ -643,9 +685,10 @@ npx sshlg-skills visibility --revert # remove exactly what --apply wrote; with
643
685
  | Cursor agent CLI 2026.09.02 | **cannot hide on cursor**: no setting switches a skill off. The only model-side filter is the skill's own `disable-model-invocation` front matter, and editing a third-party SKILL.md is ruled out | the bundled `index.js` (`filterModelDisabledSkills`, `disable-model-invocation`) | (not hidden) |
644
686
  | Kimi 2.1.1 | **cannot hide on kimi**: `[[permission.rules]] decision = "deny"`, `pattern = "Skill(<name>)"` in `~/.kimi-code/config.toml` refuses the call, but the listing keeps the skill, so it is not applied as hiding | the `kimi` 2.1.1 bundle (`features/skill/catalog` filters built-in skills only) | (not hidden) |
645
687
 
646
- Visibility follows the active host directories, including `CLAUDE_CONFIG_DIR` and
647
- `CODEX_HOME`. New ownership records bind to the canonical settings files; changing a
648
- profile or retargeting its symlink refuses apply/revert until the original target is
688
+ Visibility follows the active host directories, including `CLAUDE_CONFIG_DIR`,
689
+ `CODEX_HOME` and `GEMINI_CLI_HOME` (Gemini's HOME replacement: its files sit under
690
+ `$GEMINI_CLI_HOME/.gemini`). New ownership records bind to the canonical settings
691
+ files; changing a profile or retargeting its symlink refuses apply/revert until the original target is
649
692
  restored. Malformed or unreadable policy and incomplete skill inventories refuse apply
650
693
  before writing an ownership journal. Revert can still restore owned settings without a
651
694
  working discovery inventory. Legacy records without file bindings are accepted only
@@ -679,7 +722,12 @@ never extended: `update` prints one line asking for one `visibility --apply [--k
679
722
 
680
723
  The routing block loads in every session and is still routed around, because
681
724
  prose in a long file loses to whatever spoke last. Hooks are what speak first —
682
- and, since v0.42.0, what **holds**:
725
+ and, since v0.42.0, what **holds**.
726
+
727
+ The hub wires hooks into one target: Claude Code's `~/.claude/settings.json`. Two
728
+ other hosts can import them from that file: Cursor (its third-party hooks import,
729
+ on by default) and the Continue CLI. Neither is tested here, so the family claims
730
+ nothing about how its hooks behave there:
683
731
 
684
732
  ```bash
685
733
  npx sshlg-skills hooks # what would be wired, and what holds it now
@@ -715,7 +763,7 @@ npx sshlg-skills humanizers # anti-AI-writing skills here, with th
715
763
  | `SessionStart` | one note saying the block is not advisory; the session's title and the ledger to watch; **and, since v0.48.0, one line naming any other enabled plugin that also injects here** — silent when there is none, and since v1.56.0 printed only when that set differs from the one recorded in `state.json` — a machine-wide record, so a second session with the same set is not told again | ~90 tokens per session |
716
764
  | `UserPromptSubmit` | names the route this prompt asks for — **once per route per session** since v1.56.0 (a route already named is not repeated; `clear` and `compact` reset the record, `resume` keeps it, a fork is a new session id and starts without one; a payload with no session id keeps no turn record at all — its note always prints and it is never escalated). One exception: a facet route printed for the first time on a prompt that writes brings the `task-pipeline` line with it, even when delivery was named earlier | nothing on the turns that don't need it |
717
765
  | `PreToolUse` | **copies before any write** to `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.pi/agent/AGENTS.md`, `~/.cursor/rules/sshlg-routing.mdc` and `~/.claude/settings.json` — and **refuses the write** if the copy cannot be taken. Also refuses a bare `npx skills update <member>` | nothing until one of those files is touched |
718
- | `PostToolUse` | after any skills-CLI run, names the plain copies shadowing a plugin; after `obsidian-wiki setup`, puts back the config keys it truncates | nothing on any other command |
766
+ | `PostToolUse` | on `Bash`, `Read` and `Grep`: names a credential the tool printed — a vendor-shaped key, or a generated-looking value under a label that names one (`x-api-key: …`, `FOO_API_KEY=…`, `Authorization: Bearer …`, `.netrc`, a URL's `user:password@`) — by its label, the tool and the file or program, **never the value**, with the rotation remedy; hashes, ids and fingerprints under neutral names, placeholders, paths and code stay silent. After any skills-CLI run, names the plain copies shadowing a plugin; after `obsidian-wiki setup`, puts back the config keys it truncates | one scan of the output (each field whole up to 1 MiB, else its first 768 KiB and last 256 KiB; it says when a scan was partial), silent when it finds nothing; a local proxy's token (an active loopback `*_BASE_URL` of the same prefix, in the same file, with no other) is not reported; MCP results are not read |
719
767
  | `Notification` | a desktop ping when a long run goes idle or a background agent finishes | nothing — it emits a terminal sequence |
720
768
  | `ConfigChange` | notices when something else rewrites these entries (a field this pack writes changed; a field you added is not a change, and an entry that is gone looks like one you removed, so `hooks` lists it on demand instead) | nothing — it records; `SessionStart` reports |
721
769
  | `FileChanged` | says when the run ledger advances a stage | nothing — one line, when it moves |
@@ -861,6 +909,17 @@ Credentials go through Project Observatory (`observatory_credentials`, `use_secr
861
909
  --vault-only`); the family's secret guard names an Observatory-free route in every refusal
862
910
  as well. Observatory: <https://github.com/passioncode-ai/project-observatory-dashboard>.
863
911
 
912
+ ### What every agent loads — `context`
913
+
914
+ ```bash
915
+ npx sshlg-skills context # per host: bytes, ~tokens, lines, hub-managed vs other text, caps
916
+ npx sshlg-skills context --json
917
+ ```
918
+
919
+ Read-only. Exits 1 when a host file is over a documented cap (Codex stops adding instruction
920
+ files at `project_doc_max_bytes`, 32 KiB by default) and notes a `CLAUDE.md` above Claude Code's
921
+ 200-line target. The rules behind it: [context economy](docs/working-rules/context-economy.md).
922
+
864
923
  ## Other commands
865
924
 
866
925
  ```bash
@@ -1001,18 +1060,6 @@ GitHub release from the matching changelog section.
1001
1060
  Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). To report a
1002
1061
  vulnerability, see [SECURITY.md](SECURITY.md).
1003
1062
 
1004
- ## Author
1005
-
1006
- Built by ssheleg — [sshlg.me](https://sshlg.me)
1007
-
1008
- - X / Twitter — [@sshlg93](https://x.com/sshlg93) ·
1009
- [follow in one click](https://x.com/intent/follow?screen_name=sshlg93)
1010
- - Telegram — [@sshlg](https://t.me/sshlg)
1011
-
1012
- ## License
1013
-
1014
- MIT © 2026 ssheleg.
1015
-
1016
1063
  ### Inspect instruction blocks
1017
1064
 
1018
1065
  `npx sshlg-skills blocks --agent codex --json` reads marker metadata from the
@@ -1028,3 +1075,35 @@ The source must contain one complete matching outer block. Output contains only
1028
1075
  the selected block's before/after text, so treat that output as local material.
1029
1076
  The command does not apply the diff. Unknown/foreign blocks remain inspectable;
1030
1077
  REPOSITORY-HANDOFF ownership is still a separate operator decision.
1078
+
1079
+ ### Write another tool's block (managed-blocks/1)
1080
+
1081
+ A tool that keeps its own block in a host instruction file — Observatory's
1082
+ `OBSERVATORY:AGENT-UPDATES`, for one — can let the hub write it:
1083
+
1084
+ ```bash
1085
+ npx sshlg-skills blocks install OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --from ./block.md
1086
+ npx sshlg-skills blocks install OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --from ./block.md --apply
1087
+ npx sshlg-skills blocks uninstall OBSERVATORY:AGENT-UPDATES --owner OBSERVATORY --agent claude --apply
1088
+ ```
1089
+
1090
+ Without `--apply` it previews and writes nothing. With it, the file is copied to
1091
+ `~/.sshlg-skills/backups/` first, only the named block changes, and each step is
1092
+ recorded in `~/.sshlg-skills/managed-blocks/journal.jsonl`. It writes `claude`,
1093
+ `codex`, `gemini` and `pi` files, never installs a host, and refuses the hub's own
1094
+ blocks (they have their own writers), `SSHLG:REPOSITORY-HANDOFF` (no owner decided)
1095
+ and any block whose owner prefix differs from `--owner`. An interrupted write is
1096
+ classified with `npx sshlg-skills blocks resolve <operation-id>`. The convention:
1097
+ [docs/working-rules/managed-blocks.md](docs/working-rules/managed-blocks.md).
1098
+
1099
+ ## Author
1100
+
1101
+ Built by ssheleg — [sshlg.me](https://sshlg.me)
1102
+
1103
+ - X / Twitter — [@sshlg93](https://x.com/sshlg93) ·
1104
+ [follow in one click](https://x.com/intent/follow?screen_name=sshlg93)
1105
+ - Telegram — [@sshlg](https://t.me/sshlg)
1106
+
1107
+ ## License
1108
+
1109
+ MIT © 2026 ssheleg.
@@ -293,6 +293,7 @@ Usage:
293
293
  # Pi, OpenCode, Hermes; not Claude/Codex)
294
294
  npx sshlg-skills hooks [status] # what is wired, and what holds it
295
295
  npx sshlg-skills hooks install [--force] [--dry-run]
296
+ npx sshlg-skills context [--json] # what each host loads before the first word; caps
296
297
  npx sshlg-skills hooks recovery inspect [--json]
297
298
  npx sshlg-skills hooks recovery recover --expect <digest|absent> [--json]
298
299
  npx sshlg-skills hooks remove
@@ -312,7 +313,7 @@ Usage:
312
313
  npx sshlg-skills visibility marks [--init --show <owner/repo,…> [--replace] [--apply]]
313
314
  # this machine's pack marks
314
315
  # (~/.sshlg-skills/pack-marks.json)
315
- npx sshlg-skills blocks [--agent claude|codex|gemini|pi|cursor] [--json]\n npx sshlg-skills blocks diff OWNER:NAME --from <local-file> --agent <host>\n # read-only markers and selected local diff\n npx sshlg-skills rules [--apply] [--agent codex|gemini|pi|cursor] [--claude-only]\n # opt-in wait rule; preview unless --apply\n npx sshlg-skills rules recovery inspect [--json]\n npx sshlg-skills rules recovery finalize --operation <id> [--apply] [--json]\n # metadata only; never restores host instructions\n npx sshlg-skills domains [--apply | --revert] [--root <dir>] [--no-usage]
316
+ npx sshlg-skills blocks [--agent claude|codex|gemini|pi|cursor] [--json]\n npx sshlg-skills blocks diff OWNER:NAME --from <local-file> --agent <host>\n # read-only markers and selected local diff\n npx sshlg-skills blocks install|uninstall OWNER:NAME --owner OWNER --agent <host> [--from <file>] [--apply]\n npx sshlg-skills blocks resolve <operation-id>\n # managed-blocks/1 writer for a tool's own block; preview unless --apply\n npx sshlg-skills rules [--apply] [--agent codex|gemini|pi|cursor] [--claude-only]\n # opt-in wait rule; preview unless --apply\n npx sshlg-skills rules recovery inspect [--json]\n npx sshlg-skills rules recovery finalize --operation <id> [--apply] [--json]\n # metadata only; never restores host instructions\n npx sshlg-skills domains [--apply | --revert] [--root <dir>] [--no-usage]
316
317
  npx sshlg-skills domains --enable|--disable <member> [--project <dir>]
317
318
  # xr-dev, web3d-dev, telegram-dev; hook-bearing members stay global
318
319
  # load only in projects that call for them
@@ -374,7 +375,7 @@ function plainSkillsPlan(f, update = false) {
374
375
  if (f.claudeOnly) return [];
375
376
  if (cliSpec() !== 'skills@1.7.2') throw new Error('immutable source contract requires verified skills@1.7.2');
376
377
  return require('../lib/installer-pins').build(SKILLS, skillsCliAgents(f),
377
- { continueCopies: update && continueHasFamilyCopies() });
378
+ { continueCopies: update && continueRestoreWanted(f) });
378
379
  }
379
380
 
380
381
  function planInstall(f) {
@@ -1482,7 +1483,17 @@ function cmdRouters(f) {
1482
1483
  }
1483
1484
  }
1484
1485
 
1486
+ // B-202: what this hub last wrote into each file, so a refresh can tell its own
1487
+ // bytes from a hand edit inside a section or region. Hub metadata, not an
1488
+ // operator file: no backup; a damaged record reads as none, which keeps MORE.
1489
+ const store = require('../lib/store.js');
1490
+ const renderedFile = path.join(home, '.sshlg-skills', 'routers-rendered.json');
1491
+ const renderedRecord = store.readJson(renderedFile);
1492
+ const renderedFiles = renderedRecord.schema === 1 && renderedRecord.files && typeof renderedRecord.files === 'object'
1493
+ ? renderedRecord.files : {};
1494
+
1485
1495
  const res = apply.apply({
1496
+ rendered: renderedFiles,
1486
1497
  home, mode, consent: decision, routers: packaged,
1487
1498
  // Only the selected host files are written; the rest stay byte-identical.
1488
1499
  hostTargets: scoped.hostTargets, includeCursor: scoped.includeCursor,
@@ -1518,6 +1529,38 @@ function cmdRouters(f) {
1518
1529
  for (const name of restored) configLib.stashClear(home, name);
1519
1530
  }
1520
1531
 
1532
+ // Hand edits the refresh kept (B-202). Named by file:line in the preview and the run,
1533
+ // every time, until resolved: a section becomes the operator's authored text (so
1534
+ // `--diff` / `--adopt` apply); a region is a refused refresh and the run is partial.
1535
+ const keptRegions = [];
1536
+ const lines = (k) => (k.startLine === k.endLine ? `${k.startLine}` : `${k.startLine}-${k.endLine}`);
1537
+ for (const r of res.targets) {
1538
+ for (const k of r.kept || []) {
1539
+ if (k.kind === 'section') {
1540
+ log(`routers: ${r.file}:${lines(k)} — section "${k.name}" holds text the pack did not write; kept as it is and ${f.dryRun ? 'would be ' : ''}recorded as your authored text for ${k.name}.`);
1541
+ log(` Compare: npx sshlg-skills routers --diff ${k.name} Take the packaged text: npx sshlg-skills routers --update --adopt ${k.name}`);
1542
+ if (!f.dryRun && decision === 'yes' && configLib.authoredGet(configLib.readConfig(home), k.name) === undefined) {
1543
+ configLib.authoredSet(home, k.name, k.body);
1544
+ }
1545
+ } else {
1546
+ keptRegions.push(k);
1547
+ log(`routers: ${r.file}:${lines(k)} — the ${k.name} region holds text the pack did not write; the region was left as it is, not refreshed.`);
1548
+ log(' Move those lines outside the block (above its BEGIN or below its END marker), then run: npx sshlg-skills routers --update');
1549
+ }
1550
+ }
1551
+ }
1552
+ if (!f.dryRun && decision === 'yes') {
1553
+ const next = Object.assign({}, renderedFiles);
1554
+ for (const r of res.targets) {
1555
+ if (r.rendered && ['unchanged', 'updated', 'created'].includes(r.action)) next[r.file] = r.rendered;
1556
+ }
1557
+ // Written only when it changes, so a run that changed nothing writes nothing.
1558
+ if (JSON.stringify(next) !== JSON.stringify(renderedFiles) || renderedRecord.schema !== 1) {
1559
+ try { store.writeJson(renderedFile, () => ({ schema: 1, files: next })); }
1560
+ catch (e) { log(`routers: the record of what was written (${renderedFile}) was not saved: ${e.message}; the next refresh keeps more, never less`); }
1561
+ }
1562
+ }
1563
+
1521
1564
  // A copy of every file this run modified, taken before it was modified. Said
1522
1565
  // once rather than per target: four identical lines about the same directory
1523
1566
  // is how a useful line becomes noise the operator learns to skip.
@@ -1571,7 +1614,9 @@ function cmdRouters(f) {
1571
1614
  log(' Посмотреть разницу: npx sshlg-skills routers --diff <имя>');
1572
1615
  log(' Принять пакетный: npx sshlg-skills routers --update --adopt <имя>');
1573
1616
  }
1574
- return true;
1617
+ // A refused region refresh makes a real run partial (exit 1), like a refused wait
1618
+ // block; a preview is a render and exits 0, as `update --dry-run` does.
1619
+ return f.dryRun || !keptRegions.length;
1575
1620
  }
1576
1621
 
1577
1622
  /**
@@ -1591,17 +1636,29 @@ function attachWaitPlan(steps, f, options = {}) {
1591
1636
  if (f.member) return opres.buildPlan(steps);
1592
1637
  const pending = waitPlan(f, { automatic: true, ...options });
1593
1638
  steps.push(opres.step('working-rule', 'home', 'refresh only previously applied wait-rule profiles', {
1594
- changes: pending.rows.map(r => `${r.agent}: ${r.action} (${r.file})`),
1639
+ changes: pending.rows.map(waitRowLine),
1595
1640
  }));
1596
1641
  const planned = opres.buildPlan(steps);
1597
1642
  WAIT_PLANS.set(planned, pending);
1598
1643
  return planned;
1599
1644
  }
1645
+ // One wording for the plan and the run. A refused block names `file:line` and its remedy
1646
+ // (B-201); a host the enabled rule has not reached names the command that applies it (B-208).
1647
+ function waitRowLine(r) {
1648
+ if (r.action === 'refused') return `${r.agent}: refused — ${r.reason}; ${r.remedy}`;
1649
+ if (r.command) return `${r.agent}: not applied — ${r.file}: rules.wait is on, this host has no ownership receipt; apply it here: ${r.command}`;
1650
+ return `${r.agent}: ${r.action} (${r.file})`;
1651
+ }
1600
1652
  function executeWaitPlan(planned) {
1601
1653
  const pending = WAIT_PLANS.get(planned);
1602
1654
  if (!pending) return true;
1603
1655
  const result = require('../lib/wait-rules.js').execute(pending);
1604
- for (const r of result.results) log(`rules.wait: ${r.agent} ${r.action}${r.error ? ': ' + r.error : ''}`);
1656
+ for (const r of result.results) {
1657
+ if (r.action === 'refused') log(`rules.wait: ${r.agent} refused — ${r.error}; ${r.remedy}. Every other step of this run continues; the run reports partial (exit 1).`);
1658
+ else if (r.command) log(`rules.wait: ${r.agent} not applied — ${r.file}: rules.wait is on and this host has no ownership receipt (an update never extends it to a new host); apply it here: ${r.command}`);
1659
+ else if (r.action === 'agent-absent') continue;
1660
+ else log(`rules.wait: ${r.agent} ${r.action}${r.error ? ': ' + r.error : ''}`);
1661
+ }
1605
1662
  return result.ok;
1606
1663
  }
1607
1664
  function parseRuleFlags(args) {
@@ -1982,6 +2039,36 @@ function cmdToolkit(argv) {
1982
2039
  log(toolkit.report(skills, family, { for: forQuery, expand, limit, seeds, protect }));
1983
2040
  }
1984
2041
 
2042
+ /**
2043
+ * Should `update` add the Continue `--copy` restore pass (B-213)? Only when the run's
2044
+ * scope includes Continue (no --agent filter, or --agent names it), Continue itself is
2045
+ * installed, and its directory holds family copies to keep current. Copies alone are
2046
+ * not a host: a leftover ~/.continue/skills on a machine without Continue gets nothing.
2047
+ */
2048
+ function continueRestoreWanted(f) {
2049
+ if (f.agents && f.agents.length && !f.agents.includes('continue')) return false;
2050
+ return continueInstalled() && continueHasFamilyCopies();
2051
+ }
2052
+
2053
+ /** Continue is installed: its `cn` CLI is on PATH, or ~/.continue holds a config.* file. */
2054
+ function continueInstalled() {
2055
+ const home = process.env.HOME || os.homedir();
2056
+ try {
2057
+ if (fs.readdirSync(path.join(home, '.continue')).some((n) => /^config\.[A-Za-z0-9]+$/.test(n))) return true;
2058
+ } catch (e) { /* no ~/.continue: decided by PATH alone */ }
2059
+ const names = process.platform === 'win32' ? ['cn.cmd', 'cn.exe', 'cn'] : ['cn'];
2060
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
2061
+ if (!dir) continue;
2062
+ for (const n of names) {
2063
+ try {
2064
+ const file = path.join(dir, n);
2065
+ if (fs.statSync(file).isFile()) { fs.accessSync(file, fs.constants.X_OK); return true; }
2066
+ } catch (e) { /* not here */ }
2067
+ }
2068
+ }
2069
+ return false;
2070
+ }
2071
+
1985
2072
  /**
1986
2073
  * Does this machine hold family skills in Continue's directory? Continue reads only
1987
2074
  * real directories, so `update` restores them as copies (lib/plan.js updatePlan).
@@ -3217,6 +3304,30 @@ function cmdMaterialised(argv) {
3217
3304
  return rows.some((r) => !r.satisfied) ? 2 : 0;
3218
3305
  }
3219
3306
 
3307
+ /** Read-only: sizes of the instruction files each host loads, and documented caps. */
3308
+ function cmdContext(argv) {
3309
+ const json = argv.includes('--json');
3310
+ if (argv.some(a => a !== '--json')) { log('usage: npx sshlg-skills context [--json]'); return 2; }
3311
+ const pathMod = require('path'), C = require('../lib/context.js'), A = require('../lib/apply.js'), home = os.homedir();
3312
+ const hosts = A.TARGETS.map(t => C.host(t.agent, pathMod.join(A.hostRoot(t, home), t.file)));
3313
+ const codex = A.TARGETS.find(t => t.agent === 'codex'), codexRoot = A.hostRoot(codex, home);
3314
+ // Codex reads AGENTS.override.md in place of AGENTS.md when it exists.
3315
+ const override = C.host('codex', pathMod.join(codexRoot, 'AGENTS.override.md'));
3316
+ if (override.text != null || override.error) { const main = hosts.find(h => h.agent === 'codex'); if (main && override.text != null) main.shadowed = true; hosts.push(override); }
3317
+ for (const name of ['sshlg-routing.mdc', 'sshlg-wait.mdc']) hosts.push(C.host('cursor', pathMod.join(home, '.cursor', 'rules', name)));
3318
+ const config = C.read(pathMod.join(codexRoot, 'config.toml'));
3319
+ const r = C.report(hosts, config);
3320
+ if (json) { console.log(JSON.stringify(r)); return r.findings.some(f => f.severity === 'error') ? 1 : 0; }
3321
+ log('Always-loaded instruction files (' + r.estimate + '):');
3322
+ for (const row of r.rows) {
3323
+ if (!row.present) { log(` ${row.agent.padEnd(7)} ${row.error ? 'unreadable (' + row.error + ')' : 'absent'} ${row.file}`); continue; }
3324
+ log(` ${row.agent.padEnd(7)} ${String(row.bytes).padStart(7)} B ~${String(row.approxTokens).padStart(6)} tok ${String(row.lines).padStart(5)} lines hub ${row.managedBytes} B · other ${row.operatorBytes} B${row.shadowed ? ' (shadowed by AGENTS.override.md)' : row.cap ? ` cap ${row.cap} (${row.capSource})` : ''}`);
3325
+ }
3326
+ for (const f of r.findings) log(` ${f.severity}: ${f.message}`);
3327
+ if (!r.findings.length) log(' no host is over a documented cap');
3328
+ return r.findings.some(f => f.severity === 'error') ? 1 : 0;
3329
+ }
3330
+
3220
3331
  function cmdHooks(argv) {
3221
3332
  const fs = require('fs');
3222
3333
  const pathMod = require('path');
@@ -3770,6 +3881,7 @@ function main(argv) {
3770
3881
  // Also before parseFlags: `hooks` takes a positional subcommand.
3771
3882
  if (cmd === 'hooks') { const r = cmdHooks(rest); return typeof r === 'number' ? r : (r ? 0 : 1); }
3772
3883
  if (cmd === 'injectors') { cmdInjectors(); return 0; }
3884
+ if (cmd === 'context') return cmdContext(rest);
3773
3885
  if (cmd === 'conflicts') { cmdConflicts(); return 0; }
3774
3886
  if (cmd === 'toolkit') { return cmdToolkit(argv) || 0; }
3775
3887
  if (cmd === 'visibility') return cmdVisibility(argv);