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.
Files changed (76) hide show
  1. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.claude-plugin/marketplace.json +2 -2
  2. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.claude-plugin/plugin.json +1 -1
  3. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/CHANGELOG.md +79 -0
  4. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/PKG-INFO +14 -10
  5. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/README.md +13 -9
  6. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/README.zh-CN.md +10 -6
  7. super_skill_cli-0.11.0/codex/README.md +43 -0
  8. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/codex/install.sh +3 -2
  9. super_skill_cli-0.11.0/codex/skills/super-skill/agents/openai.yaml +12 -0
  10. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/pyproject.toml +1 -1
  11. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/candidate.py +11 -5
  12. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/capture.py +23 -4
  13. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/cli.py +81 -23
  14. super_skill_cli-0.11.0/super_skill/config.py +44 -0
  15. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/evallite.py +6 -2
  16. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/gate.py +33 -5
  17. super_skill_cli-0.11.0/super_skill/minestate.py +69 -0
  18. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/redact.py +29 -6
  19. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/registry.py +35 -12
  20. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/seed.py +6 -0
  21. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_candidate.py +21 -0
  22. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_capture.py +48 -0
  23. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_cli.py +37 -0
  24. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_codex_package.py +2 -0
  25. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_evallite.py +13 -0
  26. super_skill_cli-0.11.0/tests/test_gate.py +120 -0
  27. super_skill_cli-0.11.0/tests/test_minestate.py +55 -0
  28. super_skill_cli-0.11.0/tests/test_redact.py +143 -0
  29. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_registry.py +42 -0
  30. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_seed.py +15 -0
  31. super_skill_cli-0.11.0/tests/test_target_adapter.py +99 -0
  32. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/uv.lock +1 -1
  33. super_skill_cli-0.9.2/codex/README.md +0 -43
  34. super_skill_cli-0.9.2/super_skill/config.py +0 -25
  35. super_skill_cli-0.9.2/super_skill/minestate.py +0 -57
  36. super_skill_cli-0.9.2/tests/test_gate.py +0 -54
  37. super_skill_cli-0.9.2/tests/test_minestate.py +0 -48
  38. super_skill_cli-0.9.2/tests/test_redact.py +0 -71
  39. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/.gitignore +0 -0
  40. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/LICENSE +0 -0
  41. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/codex/skills/super-skill/SKILL.md +0 -0
  42. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/candidates.md +0 -0
  43. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/doctor.md +0 -0
  44. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/mine.md +0 -0
  45. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/seed.md +0 -0
  46. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/commands/status.md +0 -0
  47. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/README.md +0 -0
  48. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/PROMPT.md +0 -0
  49. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/broken.py +0 -0
  50. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/fixed.py +0 -0
  51. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case1/verify_test.py +0 -0
  52. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/PROMPT.md +0 -0
  53. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/broken.py +0 -0
  54. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/fixed.py +0 -0
  55. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case2/verify_test.py +0 -0
  56. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/PROMPT.md +0 -0
  57. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/broken.py +0 -0
  58. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/fixed.py +0 -0
  59. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/cases/case3/verify_test.py +0 -0
  60. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/gate2_report.py +0 -0
  61. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/grader.py +0 -0
  62. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/handwritten/SKILL.md +0 -0
  63. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/evals/test-fix-family/results.template.json +0 -0
  64. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/hooks/hooks.json +0 -0
  65. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/skills/super-skill/SKILL.md +0 -0
  66. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/__init__.py +0 -0
  67. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/doctor.py +0 -0
  68. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/hooks.py +0 -0
  69. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/mine.py +0 -0
  70. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/schemas.py +0 -0
  71. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/super_skill/skillmd.py +0 -0
  72. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_doctor.py +0 -0
  73. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_gate2_harness.py +0 -0
  74. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_hooks.py +0 -0
  75. {super_skill_cli-0.9.2 → super_skill_cli-0.11.0}/tests/test_plugin_manifest.py +0 -0
  76. {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.2"
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.9.2",
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.9.2",
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.9.2
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
- Point the CLI at the Codex directory with `SUPER_SKILL_HOST_SKILLS=~/.agents/skills`.
97
- See [`codex/README.md`](codex/README.md).
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` | Skills directory to seed/materialize (set to `~/.agents/skills` for Codex). |
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. Until the PyPI release,
193
- install the CLI from source: `uv tool install git+https://github.com/sdsrss/super-skill`.
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`. A Codex
197
- *Target Adapter inside the CLI* is a later item; distributing skills to Codex needs
198
- no extra step since they already live in `~/.agents/skills`.
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
- Point the CLI at the Codex directory with `SUPER_SKILL_HOST_SKILLS=~/.agents/skills`.
76
- See [`codex/README.md`](codex/README.md).
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` | Skills directory to seed/materialize (set to `~/.agents/skills` for Codex). |
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. Until the PyPI release,
172
- install the CLI from source: `uv tool install git+https://github.com/sdsrss/super-skill`.
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`. A Codex
176
- *Target Adapter inside the CLI* is a later item; distributing skills to Codex needs
177
- no extra step since they already live in `~/.agents/skills`.
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
- `SUPER_SKILL_HOST_SKILLS=~/.agents/skills` CLI 指向 Codex 目录。详见
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` | seed/落地的技能目录(Codex 设为 `~/.agents/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` CLIPyPI 发布前,从源码装 CLI:
150
- `uv tool install git+https://github.com/sdsrss/super-skill`。
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 *内部的* Codex Target
154
- Adapter 是后续项;把技能分发给 Codex 无需额外步骤,因为它们本就在 `~/.agents/skills`。
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
- cp "${SRC}/SKILL.md" "${DEST}/SKILL.md"
18
- echo "installed super-skill meta-skill -> ${DEST}/SKILL.md"
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "super-skill-cli"
3
- version = "0.9.2"
3
+ version = "0.11.0"
4
4
  description = "Personal Agent-Skill package manager: versioned registry, seed import, explain, rollback (M0+WS scope)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
- return Candidate.model_validate_json(meta.read_text(encoding="utf-8"))
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
- (cdir / "candidate.json").write_text(
120
- cand.model_dump_json(indent=2), encoding="utf-8"
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
- with out.open("a", encoding="utf-8") as f:
57
- f.write(event.model_dump_json() + "\n")
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({ev.session_id for ev in self.iter_events()})
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
- report = seed_from_host(reg, config.host_skills_dir())
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={config.host_skills_dir()})"
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
- name = event_type or str(data.get("hook_event_name", ""))
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
- etype = EventType(name)
70
- except ValueError:
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"({distinct} distinct sessions captured)"
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, distinct)
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.distinct_sessions())
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
- dest = reg.materialize(skill_id, config.host_skills_dir())
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 {dest}")
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.distinct_sessions())
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
- sv = approve(store, reg, candidate_id, config.host_skills_dir(), reason=reason or None)
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