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 +156 -0
- package/README.md +112 -33
- package/bin/sshlg-skills.js +117 -5
- package/claude-plugin-pins.json +126 -101
- package/hooks/post-tool-use.js +39 -8
- package/lib/apply.js +25 -12
- package/lib/blocks.js +75 -9
- package/lib/blockwriter.js +504 -0
- package/lib/claude-pin-transaction.js +19 -4
- package/lib/context.js +67 -0
- package/lib/foreign-lines.js +69 -0
- package/lib/guard.js +1 -1
- package/lib/hooks.js +6 -2
- package/lib/hostvis.js +3 -1
- package/lib/labelledsecrets.js +803 -0
- package/lib/mcpsecrets.js +12 -7
- package/lib/routers.js +81 -7
- package/lib/triggers.js +3 -4
- package/lib/wait-rule-text.js +8 -4
- package/lib/wait-rules.js +61 -18
- package/package.json +1 -1
- package/skills.json +12 -13
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,
|
|
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
|
-
|
|
26
|
-
|
|
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.
|
|
47
|
-
| **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | 1.
|
|
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.
|
|
51
|
-
| **[seo-aeo-audit](https://github.com/ssheleg/seo-aeo-audit)** | 0.
|
|
52
|
-
| **[sheleg-dev](https://github.com/ssheleg/sheleg-dev)** | 0.
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
647
|
-
`CODEX_HOME
|
|
648
|
-
|
|
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` |
|
|
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.
|
package/bin/sshlg-skills.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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);
|