super-skill-cli 0.9.2__tar.gz → 0.11.0__tar.gz
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.
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.claude-plugin/marketplace.json +2 -2
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.claude-plugin/plugin.json +1 -1
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/CHANGELOG.md +79 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/PKG-INFO +14 -10
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/README.md +13 -9
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/README.zh-CN.md +10 -6
- super_skill_cli-0.11.0/codex/README.md +43 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/codex/install.sh +3 -2
- super_skill_cli-0.11.0/codex/skills/super-skill/agents/openai.yaml +12 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/pyproject.toml +1 -1
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/candidate.py +11 -5
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/capture.py +23 -4
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/cli.py +81 -23
- super_skill_cli-0.11.0/super_skill/config.py +44 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/evallite.py +6 -2
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/gate.py +33 -5
- super_skill_cli-0.11.0/super_skill/minestate.py +69 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/redact.py +29 -6
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/registry.py +35 -12
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/seed.py +6 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_candidate.py +21 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_capture.py +48 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_cli.py +37 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_codex_package.py +2 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_evallite.py +13 -0
- super_skill_cli-0.11.0/tests/test_gate.py +120 -0
- super_skill_cli-0.11.0/tests/test_minestate.py +55 -0
- super_skill_cli-0.11.0/tests/test_redact.py +143 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_registry.py +42 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_seed.py +15 -0
- super_skill_cli-0.11.0/tests/test_target_adapter.py +99 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/uv.lock +1 -1
- super_skill_cli-0.9.2/codex/README.md +0 -43
- super_skill_cli-0.9.2/super_skill/config.py +0 -25
- super_skill_cli-0.9.2/super_skill/minestate.py +0 -57
- super_skill_cli-0.9.2/tests/test_gate.py +0 -54
- super_skill_cli-0.9.2/tests/test_minestate.py +0 -48
- super_skill_cli-0.9.2/tests/test_redact.py +0 -71
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.gitignore +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/LICENSE +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/codex/skills/super-skill/SKILL.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/candidates.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/doctor.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/mine.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/seed.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/status.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/README.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/PROMPT.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/broken.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/fixed.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/verify_test.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/PROMPT.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/broken.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/fixed.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/verify_test.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/PROMPT.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/broken.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/fixed.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/verify_test.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/gate2_report.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/grader.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/handwritten/SKILL.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/results.template.json +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/hooks/hooks.json +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/skills/super-skill/SKILL.md +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/__init__.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/doctor.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/hooks.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/mine.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/schemas.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/skillmd.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_doctor.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_gate2_harness.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_hooks.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_plugin_manifest.py +0 -0
- {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_skillmd.py +0 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "super-skill: personal Agent-Skill package manager (versioned registry, rollback, mining).",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.11.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "super-skill",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Git-backed versioned registry for your Claude Code skills: seed import, explain/provenance, one-command rollback, integrity doctor, and opportunity mining. Wraps the `super-skill` CLI (pip/uv installable as super-skill-cli).",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.11.0",
|
|
17
17
|
"category": "development"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Personal Agent-Skill package manager: a git-backed, versioned registry for your Claude Code skills with seed import, provenance/explain, one-command rollback, integrity doctor, and opportunity mining from captured sessions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss",
|
|
@@ -3,6 +3,85 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project uses semantic versioning.
|
|
5
5
|
|
|
6
|
+
## [0.11.0] - 2026-07-12
|
|
7
|
+
|
|
8
|
+
Security & reliability hardening from a full production-readiness audit. All fixes
|
|
9
|
+
are backward-compatible; defaults unchanged. **Recommended upgrade** if you wire
|
|
10
|
+
live capture via `hooks-config` — the capture/redaction path had secret-leak and
|
|
11
|
+
crash-safety gaps that this release closes.
|
|
12
|
+
|
|
13
|
+
**Migration**: none required. An existing `mine_state.json` from 0.10.0 is auto-
|
|
14
|
+
handled (the watermark format changed to a session-id set; an old file reads as
|
|
15
|
+
"nothing mined" and self-heals on the next `mine`). **Revert path**: pin the prior
|
|
16
|
+
release with `pip install super-skill-cli==0.10.0`.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
- **Redaction no longer leaks underscore-delimited env-var secrets** — `DB_PASSWORD=…`,
|
|
20
|
+
`AWS_SECRET_ACCESS_KEY=…`, `SECRET_KEY=…` etc. were written verbatim to the WAL
|
|
21
|
+
because the keyword rule anchored on `\b` (an underscore is a word char). The
|
|
22
|
+
same `redact_text` feeds the eval-lite secret gate, so this closed a leak in
|
|
23
|
+
both paths at once.
|
|
24
|
+
- **Redaction now matches current key formats** — OpenAI `sk-proj-`/`sk-svcacct-`,
|
|
25
|
+
Stripe `sk_live_`/`sk_test_`, GitHub fine-grained PATs `github_pat_`, and GCP
|
|
26
|
+
`AIza…` keys were previously unmatched.
|
|
27
|
+
- **The instruction-layer gate + eval-lite scan the full frontmatter**, not just
|
|
28
|
+
`description` + body — an injection or secret in any other frontmatter field
|
|
29
|
+
(e.g. `instructions:`, `metadata:`) previously shipped to the host unscanned.
|
|
30
|
+
- **The gate normalizes cheap obfuscation** (NFKC + zero-width strip + common
|
|
31
|
+
Cyrillic/Greek homoglyph fold) so `сurl … | bash` and homoglyph variants no
|
|
32
|
+
longer slip a shell pipe past the ASCII rules.
|
|
33
|
+
- **The capture WAL and mine watermark are gitignored** — `events/` and
|
|
34
|
+
`mine_state.json` are no longer swept into the registry's audit history by
|
|
35
|
+
`git add -A`, keeping (redacted-but-private) session content out of the repo.
|
|
36
|
+
- **Redaction depth is capped** so a pathologically nested payload can't
|
|
37
|
+
`RecursionError`; the over-deep subtree is dropped rather than leaked.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **`capture` never fails the host session (NFR-3)** — non-object JSON on stdin
|
|
41
|
+
and any WAL write error are now swallowed with exit 0; previously a list/scalar
|
|
42
|
+
payload raised and exited 1.
|
|
43
|
+
- **The WAL tolerates a torn/partial line** — a hook killed mid-append no longer
|
|
44
|
+
bricks every reader (`status`/`mine`/`count`); the bad line is skipped.
|
|
45
|
+
- **Concurrent captures no longer corrupt the WAL** — each event is one atomic
|
|
46
|
+
`O_APPEND` write instead of a buffered write that could interleave across
|
|
47
|
+
processes.
|
|
48
|
+
- **Registry/candidate/watermark writes are atomic** (temp + `os.replace`), and a
|
|
49
|
+
corrupt `meta.json`/`candidate.json` surfaces as a typed error instead of a raw
|
|
50
|
+
traceback that crashed `status`/`list`/`doctor`.
|
|
51
|
+
- **`approve` is crash-idempotent** — a crash between the registry commit and
|
|
52
|
+
marking the candidate approved no longer double-promotes on re-run.
|
|
53
|
+
- **`seed` skips a skill whose frontmatter `name` ≠ its directory name**, so two
|
|
54
|
+
host dirs sharing a name can no longer collapse into one version chain.
|
|
55
|
+
- **The `mine` reminder survives WAL TTL pruning** — the watermark tracks mined
|
|
56
|
+
session ids, so new sessions still count as "unmined" after old ones roll off.
|
|
57
|
+
- **`init` rebuilds a missing/stale `.gitignore`** even when `.git` already exists.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- Docs `02`/`03`/`04` bumped to v1.4 with WS-vs-target implementation notes
|
|
61
|
+
(registry-as-source distribution, WS entity/eval-lite subsets) for doc↔code
|
|
62
|
+
consistency. Test suite grew 127 → 159; `gate.py` and `redact.py` at 100%
|
|
63
|
+
line coverage.
|
|
64
|
+
|
|
65
|
+
## [0.10.0] - 2026-07-12
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
- **Codex Target Adapter** (docs/01 FR-PUB-2) — the CLI now distributes to more
|
|
69
|
+
than one host. `seed`, `approve`, `rollback`, and a new `materialize` command
|
|
70
|
+
take `--host claude | codex | all`; `codex` reads/writes `~/.agents/skills`
|
|
71
|
+
(override with `SUPER_SKILL_CODEX_SKILLS`), `all` targets both hosts. Defaults
|
|
72
|
+
stay `claude`, so existing behavior is unchanged.
|
|
73
|
+
- **`materialize [skill_id] --host …`** — explicitly (re)distribute one skill, or
|
|
74
|
+
all active skills, to Claude Code and/or Codex.
|
|
75
|
+
- **`codex/agents/openai.yaml`** — an optional Codex host-extension shipped with
|
|
76
|
+
the meta-skill (`interface` display name + `policy.allow_implicit_invocation`),
|
|
77
|
+
installed alongside `SKILL.md` by `codex/install.sh`. Schema per the Codex
|
|
78
|
+
build-skills docs; kept to the fields super-skill needs.
|
|
79
|
+
|
|
80
|
+
### Notes
|
|
81
|
+
- Self-learning (M1–M5) remains gated off by the H1 falsification GATE — not
|
|
82
|
+
built. The Codex adapter here is packaging/distribution (FR-PUB-2), not the
|
|
83
|
+
self-learning track.
|
|
84
|
+
|
|
6
85
|
## [0.9.2] - 2026-07-12
|
|
7
86
|
|
|
8
87
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: super-skill-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Personal Agent-Skill package manager: versioned registry, seed import, explain, rollback (M0+WS scope).
|
|
5
5
|
Project-URL: Homepage, https://github.com/sdsrss/super-skill
|
|
6
6
|
Project-URL: Repository, https://github.com/sdsrss/super-skill
|
|
@@ -93,8 +93,9 @@ pipx install super-skill-cli
|
|
|
93
93
|
codex/install.sh # drops the meta-skill into ~/.agents/skills
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
The CLI speaks Codex natively via `--host codex` — `super-skill seed --host codex`,
|
|
97
|
+
`super-skill materialize --host codex` (or `--host all` for both hosts). See
|
|
98
|
+
[`codex/README.md`](codex/README.md).
|
|
98
99
|
|
|
99
100
|
## Features
|
|
100
101
|
|
|
@@ -104,7 +105,8 @@ See [`codex/README.md`](codex/README.md).
|
|
|
104
105
|
| `status` / `list` | Registry summary (skills, versions, events, candidates) and the skill list. |
|
|
105
106
|
| `show <id>` | Frontmatter, version history, and content hashes for one skill. |
|
|
106
107
|
| `explain <id>` | Provenance chain + audit trail + the exact rollback command. |
|
|
107
|
-
| `rollback <id> [--to vN]` | Switch the active version and re-materialize it to the host. |
|
|
108
|
+
| `rollback <id> [--to vN]` | Switch the active version and re-materialize it to the host(s). |
|
|
109
|
+
| `materialize [id] --host claude\|codex\|all` | Distribute active skill(s) to Claude Code and/or Codex (the Codex Target Adapter). |
|
|
108
110
|
| `doctor` / `doctor --fix` | Integrity check (hashes, active pointer, host sync); `--fix` restores git-recoverable versions and re-materializes drift, then re-verifies. |
|
|
109
111
|
| `capture` | Append a host event to the redacted WAL — reads hook JSON on stdin, never fails the session. |
|
|
110
112
|
| `mine` | Surface task families recurring across ≥3 distinct sessions; nudges you once enough new sessions accumulate. |
|
|
@@ -161,7 +163,8 @@ super-skill candidate approve <id>
|
|
|
161
163
|
| Env var | Default | Purpose |
|
|
162
164
|
|---|---|---|
|
|
163
165
|
| `SUPER_SKILL_HOME` | `~/.super-skill` | Registry + control state (a git repo). |
|
|
164
|
-
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` |
|
|
166
|
+
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` | Claude Code skills directory (`--host claude`). |
|
|
167
|
+
| `SUPER_SKILL_CODEX_SKILLS` | `~/.agents/skills` | Codex skills directory (`--host codex`). |
|
|
165
168
|
| `SUPER_SKILL_MINE_REMINDER` | `3` | Distinct unmined sessions before `status` nudges you to mine. |
|
|
166
169
|
|
|
167
170
|
## Scope
|
|
@@ -189,13 +192,14 @@ Redaction runs *before* any write: secret values and private paths never reach t
|
|
|
189
192
|
log. Capture is off until you wire it (`super-skill hooks-config`).
|
|
190
193
|
|
|
191
194
|
**Do I need PyPI for the plugin to work?**
|
|
192
|
-
The plugin calls the `super-skill` CLI on your PATH.
|
|
193
|
-
|
|
195
|
+
The plugin calls the `super-skill` CLI on your PATH. Install it from PyPI:
|
|
196
|
+
`uv tool install super-skill-cli` (or `pipx install super-skill-cli`).
|
|
194
197
|
|
|
195
198
|
**Does it support Codex?**
|
|
196
|
-
Yes — the same CLI plus a `codex/` install package for `~/.agents/skills`.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
Yes — the same CLI plus a `codex/` install package for `~/.agents/skills`. The CLI
|
|
200
|
+
has a Codex Target Adapter: `--host codex` (and `--host all`) on `seed`, `approve`,
|
|
201
|
+
`rollback`, and `materialize` reads from and writes to Codex's `~/.agents/skills`.
|
|
202
|
+
The Codex package also ships an optional `agents/openai.yaml` host extension.
|
|
199
203
|
|
|
200
204
|
## Develop
|
|
201
205
|
|
|
@@ -72,8 +72,9 @@ pipx install super-skill-cli
|
|
|
72
72
|
codex/install.sh # drops the meta-skill into ~/.agents/skills
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
The CLI speaks Codex natively via `--host codex` — `super-skill seed --host codex`,
|
|
76
|
+
`super-skill materialize --host codex` (or `--host all` for both hosts). See
|
|
77
|
+
[`codex/README.md`](codex/README.md).
|
|
77
78
|
|
|
78
79
|
## Features
|
|
79
80
|
|
|
@@ -83,7 +84,8 @@ See [`codex/README.md`](codex/README.md).
|
|
|
83
84
|
| `status` / `list` | Registry summary (skills, versions, events, candidates) and the skill list. |
|
|
84
85
|
| `show <id>` | Frontmatter, version history, and content hashes for one skill. |
|
|
85
86
|
| `explain <id>` | Provenance chain + audit trail + the exact rollback command. |
|
|
86
|
-
| `rollback <id> [--to vN]` | Switch the active version and re-materialize it to the host. |
|
|
87
|
+
| `rollback <id> [--to vN]` | Switch the active version and re-materialize it to the host(s). |
|
|
88
|
+
| `materialize [id] --host claude\|codex\|all` | Distribute active skill(s) to Claude Code and/or Codex (the Codex Target Adapter). |
|
|
87
89
|
| `doctor` / `doctor --fix` | Integrity check (hashes, active pointer, host sync); `--fix` restores git-recoverable versions and re-materializes drift, then re-verifies. |
|
|
88
90
|
| `capture` | Append a host event to the redacted WAL — reads hook JSON on stdin, never fails the session. |
|
|
89
91
|
| `mine` | Surface task families recurring across ≥3 distinct sessions; nudges you once enough new sessions accumulate. |
|
|
@@ -140,7 +142,8 @@ super-skill candidate approve <id>
|
|
|
140
142
|
| Env var | Default | Purpose |
|
|
141
143
|
|---|---|---|
|
|
142
144
|
| `SUPER_SKILL_HOME` | `~/.super-skill` | Registry + control state (a git repo). |
|
|
143
|
-
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` |
|
|
145
|
+
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` | Claude Code skills directory (`--host claude`). |
|
|
146
|
+
| `SUPER_SKILL_CODEX_SKILLS` | `~/.agents/skills` | Codex skills directory (`--host codex`). |
|
|
144
147
|
| `SUPER_SKILL_MINE_REMINDER` | `3` | Distinct unmined sessions before `status` nudges you to mine. |
|
|
145
148
|
|
|
146
149
|
## Scope
|
|
@@ -168,13 +171,14 @@ Redaction runs *before* any write: secret values and private paths never reach t
|
|
|
168
171
|
log. Capture is off until you wire it (`super-skill hooks-config`).
|
|
169
172
|
|
|
170
173
|
**Do I need PyPI for the plugin to work?**
|
|
171
|
-
The plugin calls the `super-skill` CLI on your PATH.
|
|
172
|
-
|
|
174
|
+
The plugin calls the `super-skill` CLI on your PATH. Install it from PyPI:
|
|
175
|
+
`uv tool install super-skill-cli` (or `pipx install super-skill-cli`).
|
|
173
176
|
|
|
174
177
|
**Does it support Codex?**
|
|
175
|
-
Yes — the same CLI plus a `codex/` install package for `~/.agents/skills`.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
Yes — the same CLI plus a `codex/` install package for `~/.agents/skills`. The CLI
|
|
179
|
+
has a Codex Target Adapter: `--host codex` (and `--host all`) on `seed`, `approve`,
|
|
180
|
+
`rollback`, and `materialize` reads from and writes to Codex's `~/.agents/skills`.
|
|
181
|
+
The Codex package also ships an optional `agents/openai.yaml` host extension.
|
|
178
182
|
|
|
179
183
|
## Develop
|
|
180
184
|
|
|
@@ -59,7 +59,8 @@ pipx install super-skill-cli
|
|
|
59
59
|
codex/install.sh # 把元技能装进 ~/.agents/skills
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
CLI 原生支持 Codex——`--host codex`:`super-skill seed --host codex`、
|
|
63
|
+
`super-skill materialize --host codex`(或 `--host all` 同时分发两个宿主)。详见
|
|
63
64
|
[`codex/README.md`](codex/README.md)。
|
|
64
65
|
|
|
65
66
|
## 功能
|
|
@@ -71,6 +72,7 @@ codex/install.sh # 把元技能装进 ~/.agents/skills
|
|
|
71
72
|
| `show <id>` | 某技能的 frontmatter、版本历史与内容哈希。 |
|
|
72
73
|
| `explain <id>` | 来源链 + 审计记录 + 精确回滚命令。 |
|
|
73
74
|
| `rollback <id> [--to vN]` | 切换活动版本并重新落地到宿主。 |
|
|
75
|
+
| `materialize [id] --host claude\|codex\|all` | 把活动技能分发到 Claude Code 与/或 Codex(Codex Target Adapter)。 |
|
|
74
76
|
| `doctor` / `doctor --fix` | 完整性校验(哈希、活动指针、宿主同步);`--fix` 从 git 恢复版本、重落地漂移,再复验。 |
|
|
75
77
|
| `capture` | 把一条宿主事件追加进脱敏 WAL——从 stdin 读 hook JSON,绝不让会话失败。 |
|
|
76
78
|
| `mine` | 挖掘跨 ≥3 个去重会话复现的任务家族;攒够新会话时提醒你挖掘。 |
|
|
@@ -124,7 +126,8 @@ super-skill candidate approve <id>
|
|
|
124
126
|
| 环境变量 | 默认 | 用途 |
|
|
125
127
|
|---|---|---|
|
|
126
128
|
| `SUPER_SKILL_HOME` | `~/.super-skill` | 注册表 + 控制状态(git 仓库)。 |
|
|
127
|
-
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` |
|
|
129
|
+
| `SUPER_SKILL_HOST_SKILLS` | `~/.claude/skills` | Claude Code 技能目录(`--host claude`)。 |
|
|
130
|
+
| `SUPER_SKILL_CODEX_SKILLS` | `~/.agents/skills` | Codex 技能目录(`--host codex`)。 |
|
|
128
131
|
| `SUPER_SKILL_MINE_REMINDER` | `3` | `status` 提醒挖掘前的去重未挖掘会话数阈值。 |
|
|
129
132
|
|
|
130
133
|
## 范围
|
|
@@ -146,12 +149,13 @@ super-skill 刻意停在**包管理器**形态(里程碑 M0 + WS)。自学习闭
|
|
|
146
149
|
脱敏在*任何写入之前*运行:密钥值与私有路径永不进日志。捕获默认关闭,需你接线(`super-skill hooks-config`)。
|
|
147
150
|
|
|
148
151
|
**插件必须依赖 PyPI 才能用吗?**
|
|
149
|
-
插件调用 PATH 上的 `super-skill` CLI
|
|
150
|
-
`uv tool install
|
|
152
|
+
插件调用 PATH 上的 `super-skill` CLI。从 PyPI 安装:
|
|
153
|
+
`uv tool install super-skill-cli`(或 `pipx install super-skill-cli`)。
|
|
151
154
|
|
|
152
155
|
**支持 Codex 吗?**
|
|
153
|
-
支持——同一个 CLI 加一个面向 `~/.agents/skills` 的 `codex/` 安装包。CLI
|
|
154
|
-
Adapter
|
|
156
|
+
支持——同一个 CLI 加一个面向 `~/.agents/skills` 的 `codex/` 安装包。CLI 已内置 Codex Target
|
|
157
|
+
Adapter:`seed`、`approve`、`rollback`、`materialize` 的 `--host codex`(及 `--host all`)会从
|
|
158
|
+
Codex 的 `~/.agents/skills` 读取并写入。Codex 包还附带一个可选的 `agents/openai.yaml` 宿主扩展。
|
|
155
159
|
|
|
156
160
|
## 开发
|
|
157
161
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# super-skill for Codex
|
|
2
|
+
|
|
3
|
+
Codex adopted the [Agent Skills](https://agentskills.io) open standard: it reads
|
|
4
|
+
`SKILL.md` from `~/.agents/skills` (user-level) and `.agents/skills` (repo-level).
|
|
5
|
+
There is no marketplace/plugin install like Claude Code — a skill is just its
|
|
6
|
+
`SKILL.md` placed in the skills directory. This package is that skill plus a
|
|
7
|
+
one-line installer.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. Install the host-agnostic CLI (the command stays `super-skill`)
|
|
13
|
+
pipx install super-skill-cli # or: uv tool install super-skill-cli
|
|
14
|
+
|
|
15
|
+
# 2. Install the meta-skill into ~/.agents/skills (idempotent)
|
|
16
|
+
codex/install.sh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`install.sh` copies `skills/super-skill/SKILL.md` into
|
|
20
|
+
`~/.agents/skills/super-skill/`. Re-running it is safe.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
super-skill manages the skills in your skills directory (version, explain,
|
|
25
|
+
rollback, integrity `doctor`, opportunity `mine`). The CLI has a **Codex Target
|
|
26
|
+
Adapter** — use `--host codex` (or `--host all`) to read from and write to Codex's
|
|
27
|
+
`~/.agents/skills`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
super-skill seed --host codex # import from ~/.agents/skills (read-only)
|
|
31
|
+
super-skill materialize --host codex # distribute active skills to Codex
|
|
32
|
+
super-skill materialize <id> --host all # push one skill to both hosts
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`~/.agents/skills` is super-skill's canonical Codex source — Codex reads promoted
|
|
36
|
+
skills there directly (zero-copy). Override the path with `SUPER_SKILL_CODEX_SKILLS`.
|
|
37
|
+
|
|
38
|
+
## `agents/openai.yaml`
|
|
39
|
+
|
|
40
|
+
This package ships an optional `agents/openai.yaml` host extension (installed
|
|
41
|
+
alongside `SKILL.md`) with Codex UI + invocation-policy hints (`interface`,
|
|
42
|
+
`policy`). Only the open-standard `SKILL.md` is required; edit or remove the YAML
|
|
43
|
+
per your current Codex version's docs.
|
|
@@ -14,8 +14,9 @@ if [ ! -f "${SRC}/SKILL.md" ]; then
|
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
16
|
mkdir -p "${DEST}"
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Copy the whole skill tree: SKILL.md + optional agents/openai.yaml (host extension).
|
|
18
|
+
cp -R "${SRC}/." "${DEST}/"
|
|
19
|
+
echo "installed super-skill meta-skill -> ${DEST}/ (SKILL.md + agents/openai.yaml)"
|
|
19
20
|
|
|
20
21
|
if ! command -v super-skill >/dev/null 2>&1; then
|
|
21
22
|
echo "note: the 'super-skill' CLI is not on PATH — install it with:" >&2
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Codex host-extension metadata (OPTIONAL). Only SKILL.md is required; this file
|
|
2
|
+
# adds Codex-specific UI + invocation hints. Schema per the Codex build-skills
|
|
3
|
+
# docs (learn.chatgpt.com/docs/build-skills): interface / policy / dependencies.
|
|
4
|
+
# Kept to the fields super-skill actually needs — a CLI-driven manager, not an
|
|
5
|
+
# MCP tool, so no dependencies block.
|
|
6
|
+
interface:
|
|
7
|
+
display_name: super-skill
|
|
8
|
+
short_description: Version, roll back, and audit your Agent Skills.
|
|
9
|
+
policy:
|
|
10
|
+
# true = Codex may auto-select this skill from the user's prompt;
|
|
11
|
+
# false would require an explicit $super-skill invocation.
|
|
12
|
+
allow_implicit_invocation: true
|
|
@@ -10,11 +10,12 @@ candidate -> gate (human approve) -> promote, never bypassed.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import os
|
|
13
14
|
import re
|
|
14
15
|
from datetime import datetime
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
18
19
|
|
|
19
20
|
from . import config
|
|
20
21
|
from .evallite import EvalError, eval_lite
|
|
@@ -95,7 +96,10 @@ class CandidateStore:
|
|
|
95
96
|
meta = self._cdir(cand_id) / "candidate.json"
|
|
96
97
|
if not meta.exists():
|
|
97
98
|
return None
|
|
98
|
-
|
|
99
|
+
try:
|
|
100
|
+
return Candidate.model_validate_json(meta.read_text(encoding="utf-8"))
|
|
101
|
+
except ValidationError as e:
|
|
102
|
+
raise CandidateError(f"{cand_id}: corrupt candidate.json ({e})") from e
|
|
99
103
|
|
|
100
104
|
def list(self) -> list[Candidate]:
|
|
101
105
|
if not self.dir.exists():
|
|
@@ -116,9 +120,11 @@ class CandidateStore:
|
|
|
116
120
|
def save(self, cand: Candidate) -> None:
|
|
117
121
|
cdir = self._cdir(cand.candidate_id)
|
|
118
122
|
cdir.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
123
|
+
# Atomic write so a crash can't leave a truncated candidate.json (M12).
|
|
124
|
+
p = cdir / "candidate.json"
|
|
125
|
+
tmp = p.with_suffix(".json.tmp")
|
|
126
|
+
tmp.write_text(cand.model_dump_json(indent=2), encoding="utf-8")
|
|
127
|
+
os.replace(tmp, p)
|
|
122
128
|
|
|
123
129
|
|
|
124
130
|
def draft_from_families(
|
|
@@ -7,10 +7,13 @@ events are TTL-bounded (FR-CAP-6); structured products live elsewhere.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import os
|
|
10
11
|
import uuid
|
|
11
12
|
from collections.abc import Iterator
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
|
|
14
17
|
from . import config
|
|
15
18
|
from .redact import redact_payload, redact_text
|
|
16
19
|
from .schemas import CaptureEvent, EventType
|
|
@@ -53,8 +56,15 @@ class EventLog:
|
|
|
53
56
|
day = event.timestamp.strftime("%Y-%m-%d")
|
|
54
57
|
out = self.events_dir / day / "events.jsonl"
|
|
55
58
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
# One atomic append per event: a single O_APPEND os.write, not a buffered
|
|
60
|
+
# writer that can split a large line into multiple write() calls and let
|
|
61
|
+
# two concurrent captures interleave into a corrupt line (H4).
|
|
62
|
+
data = (event.model_dump_json() + "\n").encode("utf-8")
|
|
63
|
+
fd = os.open(out, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
|
64
|
+
try:
|
|
65
|
+
os.write(fd, data)
|
|
66
|
+
finally:
|
|
67
|
+
os.close(fd)
|
|
58
68
|
return event
|
|
59
69
|
|
|
60
70
|
def iter_events(self) -> Iterator[CaptureEvent]:
|
|
@@ -66,11 +76,20 @@ class EventLog:
|
|
|
66
76
|
continue
|
|
67
77
|
for line in wal.read_text(encoding="utf-8").splitlines():
|
|
68
78
|
line = line.strip()
|
|
69
|
-
if line:
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
70
82
|
yield CaptureEvent.model_validate_json(line)
|
|
83
|
+
except ValidationError:
|
|
84
|
+
# A killed hook can leave a torn final line; one bad record
|
|
85
|
+
# must not brick every reader (status/mine/count). Skip it.
|
|
86
|
+
continue
|
|
71
87
|
|
|
72
88
|
def count(self) -> int:
|
|
73
89
|
return sum(1 for _ in self.iter_events())
|
|
74
90
|
|
|
91
|
+
def session_ids(self) -> set[str]:
|
|
92
|
+
return {ev.session_id for ev in self.iter_events()}
|
|
93
|
+
|
|
75
94
|
def distinct_sessions(self) -> int:
|
|
76
|
-
return len(
|
|
95
|
+
return len(self.session_ids())
|
|
@@ -37,19 +37,54 @@ def _short(text: str, n: int = 60) -> str:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@app.command()
|
|
40
|
-
def seed() -> None:
|
|
40
|
+
def seed(host: str = typer.Option("claude", "--host", help="source host: claude | codex")) -> None:
|
|
41
41
|
"""Import existing host skills into the registry (idempotent, read-only on host)."""
|
|
42
|
+
if host not in config.HOSTS:
|
|
43
|
+
typer.echo(f"unknown host {host!r} (expected: {', '.join(config.HOSTS)})", err=True)
|
|
44
|
+
raise typer.Exit(1)
|
|
42
45
|
reg = _registry()
|
|
43
|
-
|
|
46
|
+
src = config.host_skills_dir(host)
|
|
47
|
+
report = seed_from_host(reg, src)
|
|
44
48
|
typer.echo(
|
|
45
49
|
f"seed: {len(report.imported)} imported, {len(report.updated)} updated, "
|
|
46
50
|
f"{len(report.unchanged)} unchanged, {len(report.skipped)} skipped "
|
|
47
|
-
f"(host={
|
|
51
|
+
f"(host={src})"
|
|
48
52
|
)
|
|
49
53
|
for name, reason in report.skipped:
|
|
50
54
|
typer.echo(f" skipped {name}: {reason}")
|
|
51
55
|
|
|
52
56
|
|
|
57
|
+
@app.command()
|
|
58
|
+
def materialize(
|
|
59
|
+
skill_id: str = typer.Argument("", help="skill to distribute; empty = all active skills"),
|
|
60
|
+
host: str = typer.Option("claude", "--host", help="target host: claude | codex | all"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Distribute active skill(s) to a host skills dir — Claude Code and/or Codex (FR-PUB-2)."""
|
|
63
|
+
reg = _registry()
|
|
64
|
+
try:
|
|
65
|
+
hosts = config.resolve_hosts(host)
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
typer.echo(str(e), err=True)
|
|
68
|
+
raise typer.Exit(1) from e
|
|
69
|
+
ids = [skill_id] if skill_id else [
|
|
70
|
+
r.skill.skill_id for r in reg.list_skills() if r.skill.active_version
|
|
71
|
+
]
|
|
72
|
+
if not ids:
|
|
73
|
+
typer.echo("no active skills to materialize")
|
|
74
|
+
return
|
|
75
|
+
failed = False
|
|
76
|
+
for sid in ids:
|
|
77
|
+
for h in hosts:
|
|
78
|
+
try:
|
|
79
|
+
dest = reg.materialize(sid, config.host_skills_dir(h))
|
|
80
|
+
typer.echo(f"{sid} -> {h}: {dest}")
|
|
81
|
+
except RegistryError as e:
|
|
82
|
+
typer.echo(f"{sid} -> {h}: {e}", err=True)
|
|
83
|
+
failed = True
|
|
84
|
+
if failed:
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
53
88
|
@app.command()
|
|
54
89
|
def capture(
|
|
55
90
|
event_type: str = typer.Option("", "--event-type", help="override hook_event_name"),
|
|
@@ -64,35 +99,43 @@ def capture(
|
|
|
64
99
|
data = json.loads(raw) if raw.strip() else {}
|
|
65
100
|
except (json.JSONDecodeError, OSError):
|
|
66
101
|
raise typer.Exit(0) from None
|
|
67
|
-
|
|
102
|
+
# NFR-3: the hook must NEVER fail the host session. Everything past here —
|
|
103
|
+
# non-dict JSON (data.get would raise), an unknown event type, or any WAL
|
|
104
|
+
# write error — is swallowed and exits 0. Capture-never-fails beats
|
|
105
|
+
# capture-complete.
|
|
68
106
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
raise typer.Exit(0)
|
|
109
|
+
name = event_type or str(data.get("hook_event_name", ""))
|
|
110
|
+
etype = EventType(name) # ValueError on unknown type
|
|
111
|
+
session_id = str(data.get("session_id", "unknown"))
|
|
112
|
+
project_id = data.get("cwd")
|
|
113
|
+
EventLog().append(
|
|
114
|
+
etype, session_id, data, project_id=str(project_id) if project_id else None
|
|
115
|
+
)
|
|
116
|
+
except typer.Exit:
|
|
117
|
+
raise
|
|
118
|
+
except Exception:
|
|
71
119
|
raise typer.Exit(0) from None
|
|
72
|
-
session_id = str(data.get("session_id", "unknown"))
|
|
73
|
-
project_id = data.get("cwd")
|
|
74
|
-
EventLog().append(
|
|
75
|
-
etype, session_id, data, project_id=str(project_id) if project_id else None
|
|
76
|
-
)
|
|
77
120
|
|
|
78
121
|
|
|
79
122
|
@app.command()
|
|
80
123
|
def mine(min_sessions: int = typer.Option(3, "--min-sessions")) -> None:
|
|
81
124
|
"""Surface recurring task families from captured events (FR-GEN-1 signal)."""
|
|
82
125
|
log = EventLog()
|
|
126
|
+
session_ids = log.session_ids()
|
|
83
127
|
families = mine_families(log.iter_events(), min_sessions=min_sessions)
|
|
84
|
-
distinct = log.distinct_sessions()
|
|
85
128
|
if not families:
|
|
86
129
|
typer.echo(
|
|
87
130
|
f"no families recurring across >={min_sessions} sessions yet "
|
|
88
|
-
f"({
|
|
131
|
+
f"({len(session_ids)} distinct sessions captured)"
|
|
89
132
|
)
|
|
90
133
|
else:
|
|
91
134
|
typer.echo(f"{'sessions':>8} {'events':>6} family")
|
|
92
135
|
for fam in families:
|
|
93
136
|
typer.echo(f"{fam.session_count:>8} {fam.event_count:>6} {fam.label}")
|
|
94
137
|
# Mining acknowledges the accumulated sessions: reset the reminder watermark.
|
|
95
|
-
minestate.record_mined(log.root,
|
|
138
|
+
minestate.record_mined(log.root, session_ids)
|
|
96
139
|
|
|
97
140
|
|
|
98
141
|
@app.command()
|
|
@@ -114,7 +157,7 @@ def status() -> None:
|
|
|
114
157
|
log = EventLog(reg.root)
|
|
115
158
|
typer.echo(f"events : {log.count()}")
|
|
116
159
|
typer.echo(f"candidates : {len(cands)} ({breakdown})")
|
|
117
|
-
unmined = minestate.unmined(reg.root, log.
|
|
160
|
+
unmined = minestate.unmined(reg.root, log.session_ids())
|
|
118
161
|
if unmined >= minestate.reminder_threshold():
|
|
119
162
|
typer.echo(f"reminder : {unmined} distinct sessions unmined "
|
|
120
163
|
f"— run `super-skill mine`")
|
|
@@ -189,9 +232,15 @@ def rollback(
|
|
|
189
232
|
skill_id: str,
|
|
190
233
|
to: str = typer.Option("", "--to", help="target version; default = previous"),
|
|
191
234
|
reason: str = typer.Option("", "--reason"),
|
|
235
|
+
host: str = typer.Option("claude", "--host", help="re-materialize to: claude | codex | all"),
|
|
192
236
|
) -> None:
|
|
193
|
-
"""Switch the active pointer to an older version and re-materialize to the host."""
|
|
237
|
+
"""Switch the active pointer to an older version and re-materialize to the host(s)."""
|
|
194
238
|
reg = _registry()
|
|
239
|
+
try:
|
|
240
|
+
hosts = config.resolve_hosts(host)
|
|
241
|
+
except ValueError as e:
|
|
242
|
+
typer.echo(str(e), err=True)
|
|
243
|
+
raise typer.Exit(1) from e
|
|
195
244
|
rec = reg.get(skill_id)
|
|
196
245
|
if rec is None:
|
|
197
246
|
typer.echo(f"unknown skill: {skill_id}", err=True)
|
|
@@ -205,11 +254,11 @@ def rollback(
|
|
|
205
254
|
to = versions[idx - 1]
|
|
206
255
|
try:
|
|
207
256
|
reg.set_active(skill_id, to, op=OperationType.ROLLBACK, reason=reason or None)
|
|
208
|
-
|
|
257
|
+
dests = [reg.materialize(skill_id, config.host_skills_dir(h)) for h in hosts]
|
|
209
258
|
except RegistryError as e:
|
|
210
259
|
typer.echo(str(e), err=True)
|
|
211
260
|
raise typer.Exit(1) from e
|
|
212
|
-
typer.echo(f"rolled back {skill_id} -> {to}; materialized {
|
|
261
|
+
typer.echo(f"rolled back {skill_id} -> {to}; materialized {', '.join(str(d) for d in dests)}")
|
|
213
262
|
|
|
214
263
|
|
|
215
264
|
@app.command()
|
|
@@ -281,7 +330,7 @@ def candidate_draft(min_sessions: int = typer.Option(3, "--min-sessions")) -> No
|
|
|
281
330
|
store = CandidateStore(config.state_root())
|
|
282
331
|
created = draft_from_families(store, families)
|
|
283
332
|
# Drafting acts on the mining, so it too clears the status reminder.
|
|
284
|
-
minestate.record_mined(log.root, log.
|
|
333
|
+
minestate.record_mined(log.root, log.session_ids())
|
|
285
334
|
if not created:
|
|
286
335
|
typer.echo("no new candidates (nothing mined, or all already drafted)")
|
|
287
336
|
return
|
|
@@ -338,12 +387,21 @@ def candidate_show(candidate_id: str) -> None:
|
|
|
338
387
|
def candidate_approve(
|
|
339
388
|
candidate_id: str,
|
|
340
389
|
reason: str = typer.Option("", "--reason"),
|
|
390
|
+
host: str = typer.Option("claude", "--host", help="materialize to: claude | codex | all"),
|
|
341
391
|
) -> None:
|
|
342
|
-
"""Approve a candidate: promote to the registry and materialize to the host."""
|
|
392
|
+
"""Approve a candidate: promote to the registry and materialize to the host(s)."""
|
|
343
393
|
store = CandidateStore(config.state_root())
|
|
344
394
|
reg = _registry()
|
|
345
395
|
try:
|
|
346
|
-
|
|
396
|
+
hosts = config.resolve_hosts(host)
|
|
397
|
+
except ValueError as e:
|
|
398
|
+
typer.echo(str(e), err=True)
|
|
399
|
+
raise typer.Exit(1) from e
|
|
400
|
+
try:
|
|
401
|
+
sv = approve(store, reg, candidate_id, config.host_skills_dir(hosts[0]),
|
|
402
|
+
reason=reason or None)
|
|
403
|
+
for extra in hosts[1:]:
|
|
404
|
+
reg.materialize(sv.skill_id, config.host_skills_dir(extra))
|
|
347
405
|
except InstructionGateError as e:
|
|
348
406
|
typer.echo(str(e), err=True)
|
|
349
407
|
for f in e.findings:
|
|
@@ -359,9 +417,9 @@ def candidate_approve(
|
|
|
359
417
|
except (CandidateError, RegistryError, SkillMdError) as e:
|
|
360
418
|
typer.echo(str(e), err=True)
|
|
361
419
|
raise typer.Exit(1) from e
|
|
420
|
+
dests = ", ".join(str(config.host_skills_dir(h) / sv.skill_id) for h in hosts)
|
|
362
421
|
typer.echo(
|
|
363
|
-
f"approved {candidate_id} -> {sv.skill_id}@{sv.version}; "
|
|
364
|
-
f"materialized to {config.host_skills_dir() / sv.skill_id}"
|
|
422
|
+
f"approved {candidate_id} -> {sv.skill_id}@{sv.version}; materialized to {dests}"
|
|
365
423
|
)
|
|
366
424
|
|
|
367
425
|
|