skill-automation-package 0.2.0 → 0.3.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/README.md CHANGED
@@ -2,7 +2,22 @@
2
2
 
3
3
  Portable repo-local skill automation for Codex and Claude Code.
4
4
 
5
- This repository ships a Python-installed automation bundle. It is not an npm runtime package; `package.json` is used here as a package manifest for managed assets, versioning, and install metadata.
5
+ ## Quick Start
6
+
7
+ Install into the repository you are already in, then use the installed runtime immediately. The package is `npx`-first, but the installed runtime still uses Python 3.10+.
8
+
9
+ ```bash
10
+ cd your-repo
11
+ npx skill-automation-package install --target .
12
+ python3 .claude/tools/skill_agent.py auto "add tests" --json
13
+ ```
14
+
15
+ - no setup required beyond `npx` and Python 3.10+
16
+ - works on existing repositories
17
+ - safe: uses bounded managed blocks for generated guidance and ignore rules
18
+
19
+ The published npm package is `skill-automation-package`. It is a thin installer frontend over the same Python-based automation bundle; the runtime and install core still live in Python.
20
+ This remains a Python-installed automation bundle, not an npm runtime package.
6
21
 
7
22
  ## Why This Exists
8
23
 
@@ -17,39 +32,26 @@ After installation, an agent can:
17
32
  - archive low-value skills that stay unused long enough to become cleanup candidates
18
33
  - route future Codex and Claude Code sessions through the same workflow
19
34
 
20
- ## What It Installs
21
-
22
- - `.claude/tools/skill_agent.py`
23
- - `.claude/skills/project-skill-router/`
24
- - optional `.claude/tests/test_skill_agent.py`
25
- - managed automation blocks for `AGENTS.md` and `CLAUDE.md`
26
-
27
- ## Quick Start
35
+ ## Basic Flow
28
36
 
29
- Install into another repository:
30
-
31
- ```bash
32
- python3 scripts/install.py --target /path/to/target-repo
33
- ```
34
-
35
- Or use the npm wrapper entrypoint:
37
+ Use the published package to install into another repository:
36
38
 
37
39
  ```bash
38
40
  npx skill-automation-package install --target /path/to/target-repo
39
41
  ```
40
42
 
41
- To update an existing target without forcing an unnecessary reinstall when it is already current:
43
+ Update an existing target with version-aware reinstall behavior:
42
44
 
43
45
  ```bash
44
46
  npx skill-automation-package update --target /path/to/target-repo
45
47
  ```
46
48
 
47
- The npm entrypoint is a thin wrapper around the Python installer. It does not replace the Python core, and Python 3.10 or newer is still required.
48
- If you already have this repository checked out locally, the direct `python3 scripts/install.py ...` path remains fully supported.
49
+ Python 3.10 or newer is still required. The npm entrypoint is a thin wrapper around the Python installer and does not replace the Python core.
50
+
49
51
  `install` always allows reinstall. `update` is version-aware and only reinstalls when the target reports an older installed version.
50
52
  Before reinstalling, the wrapper checks `.claude/skill-automation-package.json` in the target repo and reports whether the target is not installed, already at the current version, or behind the current package version.
51
53
 
52
- The installer copies the packaged assets, updates managed blocks in `AGENTS.md` and `CLAUDE.md` unless skipped, writes `.claude/skill-automation-package.json`, and refreshes `.claude/skills/registry.json`.
54
+ The installer copies the packaged assets, updates managed blocks in `AGENTS.md` and `CLAUDE.md` unless skipped, updates a generated-state `.gitignore` block unless disabled, writes `.claude/skill-automation-package.json`, and refreshes `.claude/skills/registry.json`.
53
55
 
54
56
  Then, inside the target repository, start non-trivial work with:
55
57
 
@@ -63,6 +65,13 @@ If you want a preview before writing files:
63
65
  python3 .claude/tools/skill_agent.py auto "<task>" --dry-run --json
64
66
  ```
65
67
 
68
+ ## What It Installs
69
+
70
+ - `.claude/tools/skill_agent.py`
71
+ - five packaged core default skills under `.claude/skills/`: `project-skill-router/` plus read-only helpers for project summary, repo structure analysis, docs entrypoint guidance, and change summary
72
+ - optional `.claude/tests/test_skill_agent.py`
73
+ - managed automation blocks for `AGENTS.md` and `CLAUDE.md`
74
+
66
75
  ## Example Outcomes
67
76
 
68
77
  Representative `auto` outcomes look like this.
@@ -96,35 +105,59 @@ Create a new skill when no strong match exists:
96
105
 
97
106
  The exact skill name is inferred from the task, but the flow is stable: reuse when there is a strong match, otherwise scaffold a new reusable local skill and refresh the registry immediately.
98
107
 
99
- ## Installation Guide
108
+ ## Advanced Installation
100
109
 
101
- Use this package when you have the package repository checked out locally and want to install the automation bundle into another repository.
110
+ Use this package when you want to install the automation bundle into another repository. The published npm package is the default entrypoint, and the direct Python path remains available when you are working from a local checkout of this repository.
102
111
 
103
112
  ### Prerequisites
104
113
 
105
114
  - Python 3.10 or newer
106
- - for the npm entrypoint, Node.js 18 or newer
115
+ - Node.js 18 or newer
116
+ - npm or `npx` for the published package entrypoint
107
117
  - a target repository where you want repo-local skill automation under `.claude/`
108
118
  - write access to the target repository
109
119
 
110
120
  ### Standard Install
111
121
 
112
122
  1. Choose the target repository.
113
- 2. Run the installer from this package repository:
123
+ 2. Use the published package:
124
+
125
+ ```bash
126
+ npx skill-automation-package install --target /path/to/target-repo
127
+ ```
128
+
129
+ ### Manual Installation
130
+
131
+ If you already have this repository checked out locally, the direct Python install path remains fully supported:
114
132
 
115
133
  ```bash
116
134
  python3 scripts/install.py --target /path/to/target-repo
117
135
  ```
118
136
 
119
- 3. The installer will:
137
+ ### What The Installer Writes
120
138
 
121
139
  - copy `.claude/tools/skill_agent.py`
122
- - copy `.claude/skills/project-skill-router/`
140
+ - copy the packaged core default skills under `.claude/skills/`
123
141
  - optionally copy `.claude/tests/test_skill_agent.py`
124
142
  - insert managed automation blocks into `AGENTS.md` and `CLAUDE.md`
143
+ - insert a managed generated-state block into `.gitignore` unless disabled
125
144
  - write `.claude/skill-automation-package.json`
126
145
  - refresh `.claude/skills/registry.json`
127
146
 
147
+ ### Install Vs Update
148
+
149
+ Install always runs and allows a deliberate reinstall:
150
+
151
+ ```bash
152
+ npx skill-automation-package install --target /path/to/target-repo
153
+ ```
154
+
155
+ Update is version-aware and skips work when the target is already current:
156
+
157
+ ```bash
158
+ npx skill-automation-package update --target /path/to/target-repo
159
+ ```
160
+
128
161
  ### Why `AGENTS.md` And `CLAUDE.md` Are Updated
129
162
 
130
163
  The installer adds a bounded managed block so future Codex and Claude Code sessions start from the same routing command instead of bypassing the local skill system.
@@ -145,7 +178,7 @@ python3 .claude/tools/skill_agent.py list
145
178
  python3 .claude/tools/skill_agent.py auto "find or create the right reusable workflow" --json
146
179
  ```
147
180
 
148
- You should see the packaged router skill in the list, and `auto` should return either a reusable local skill match or a generated preview/result.
181
+ You should see the packaged core skills in the list, including the router, and `auto` should return either a reusable local skill match or a generated preview/result.
149
182
 
150
183
  ### Common Install Variants
151
184
 
@@ -153,17 +186,24 @@ You should see the packaged router skill in the list, and `auto` should return e
153
186
  - Skip the packaged test file: `python3 scripts/install.py --target /path/to/target-repo --no-tests`
154
187
  - Skip managed `AGENTS.md`: `python3 scripts/install.py --target /path/to/target-repo --skip-agents`
155
188
  - Skip managed `CLAUDE.md`: `python3 scripts/install.py --target /path/to/target-repo --skip-claude`
189
+ - Disable `.gitignore` updates: `python3 scripts/install.py --target /path/to/target-repo --gitignore-mode none`
190
+ - Ignore the whole local-only install: `python3 scripts/install.py --target /path/to/target-repo --gitignore-mode local-only`
156
191
 
157
192
  ## Managed File Behavior
158
193
 
159
194
  - If `AGENTS.md` or `CLAUDE.md` does not exist, install creates the file and inserts the managed block.
160
195
  - If both package markers already exist, install replaces only the content inside that managed block.
161
196
  - If the markers are missing, install appends the managed block to the end of the existing file.
197
+ - By default, install also creates or updates a managed `.gitignore` block for generated state only.
198
+ - Use `--gitignore-mode none` to leave `.gitignore` untouched, or `--gitignore-mode local-only` when the whole install should stay local to one checkout.
162
199
  - `--dry-run` previews the install result without creating the target directory or writing package files, and its status lines use `Would ...` wording for changes that are only being previewed.
200
+ - Dry-run output includes package file groups for files that would be created, updated, left unchanged, or reported as previously installed but no longer shipped.
201
+ - Dry-run output also reports whether each managed guidance file would be created, replaced, appended, left unchanged, or skipped.
163
202
 
164
203
  ## Target Repo Git Hygiene
165
204
 
166
205
  Decide up front whether the installed automation should be shared through version control or kept local to one checkout.
206
+ The default installer policy is shared-friendly: it adds only generated-state patterns to `.gitignore`.
167
207
 
168
208
  ### Shared Automation In Version Control
169
209
 
@@ -172,11 +212,11 @@ Use this when the repository wants shared repo-local skills and shared entrypoin
172
212
  Usually commit:
173
213
 
174
214
  - `.claude/tools/skill_agent.py`
175
- - `.claude/skills/project-skill-router/`
215
+ - the packaged core default skills under `.claude/skills/`
176
216
  - `.claude/tests/test_skill_agent.py` when installed
177
217
  - `AGENTS.md` and `CLAUDE.md` when you want the managed guidance blocks shared with the team
178
218
 
179
- Usually ignore:
219
+ The installer adds this generated-state block by default:
180
220
 
181
221
  ```gitignore
182
222
  .claude/skills/registry.json
@@ -197,10 +237,11 @@ Use this when the install is only for one developer checkout and should not affe
197
237
 
198
238
  Typical approach:
199
239
 
200
- - install with `--skip-agents --skip-claude` if you do not want top-level guidance files touched
240
+ - install with `--gitignore-mode local-only`
241
+ - add `--skip-agents --skip-claude` if you do not want top-level guidance files touched
201
242
  - ignore the installed automation tree and any optional managed docs
202
243
 
203
- Example:
244
+ The local-only mode manages this block:
204
245
 
205
246
  ```gitignore
206
247
  .claude/
@@ -235,7 +276,17 @@ npx skill-automation-package update --target /path/to/target-repo
235
276
 
236
277
  - `update` is a version-aware reinstall, not a partial update.
237
278
  - `update` blocks implicit downgrade attempts; use `install` only when you intentionally want to replace the target with the current package version.
238
- - If install metadata is malformed, `update` stops and asks you to use `install` for a deliberate reinstall.
279
+ - If install metadata is malformed, incomplete, for a different package, or missing a usable asset list, `update` stops and asks you to use `install` for a deliberate reinstall.
280
+
281
+ Metadata recovery behavior:
282
+
283
+ | State | `install` behavior | `update` behavior |
284
+ | --- | --- | --- |
285
+ | Missing metadata | Proceeds as a new install | Stops and asks for `install` |
286
+ | Same version | Reinstalls deliberately | No-ops |
287
+ | Older version | Reinstalls deliberately | Runs reinstall |
288
+ | Newer version | Warns and reinstalls deliberately | Blocks downgrade |
289
+ | Malformed or incomplete metadata | Warns and reinstalls deliberately | Stops and asks for `install` |
239
290
 
240
291
  What gets updated in place:
241
292
 
@@ -342,10 +393,22 @@ Do not update `CLAUDE.md`:
342
393
  python3 scripts/install.py --target /path/to/target-repo --skip-claude
343
394
  ```
344
395
 
396
+ Do not update `.gitignore`:
397
+
398
+ ```bash
399
+ python3 scripts/install.py --target /path/to/target-repo --gitignore-mode none
400
+ ```
401
+
402
+ Use a local-only `.gitignore` policy:
403
+
404
+ ```bash
405
+ python3 scripts/install.py --target /path/to/target-repo --gitignore-mode local-only
406
+ ```
407
+
345
408
  ## Package Layout
346
409
 
347
410
  - `assets/.claude/tools/skill_agent.py`: resolver, search, scaffold, usage tracking, refresh review/update, and prune CLI
348
- - `assets/.claude/skills/project-skill-router/`: default reusable routing skill
411
+ - `assets/.claude/skills/`: packaged core default skills, including the router and four read-only helper skills
349
412
  - `scripts/package_layout.py`: shared package manifest loader and asset copy helpers
350
413
  - `templates/agents_block.md`: managed block for `AGENTS.md`
351
414
  - `templates/claude_block.md`: managed block for `CLAUDE.md`
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: core-change-summary
3
+ description: Summarize the current working tree or recent changes in a read-only way. Use when an agent needs fast change context without editing the repo.
4
+ ---
5
+
6
+ # Core Change Summary
7
+
8
+ ## Goal
9
+
10
+ Build a concise summary of what changed, where it changed, and what still looks important before deeper review or follow-up work.
11
+
12
+ ## Workflow
13
+
14
+ 1. Inspect the current working tree, changed files, and the smallest relevant diff or file set.
15
+ 2. Group changes by area, intent, or likely effect instead of listing raw filenames only.
16
+ 3. Call out obvious risks, open questions, or verification gaps that follow from the observed changes.
17
+ 4. Keep the output read-only and limited to summary, not repo modification.
18
+
19
+ ## Guardrails
20
+
21
+ - Do not create, edit, or delete repository files.
22
+ - Do not infer intent that is not supported by the observed changes.
23
+ - Prefer a compact status summary over a file-by-file changelog.
@@ -0,0 +1,31 @@
1
+ {
2
+ "category": "workflow",
3
+ "summary": "Summarize the current working tree or recent changes without editing the repository.",
4
+ "management_mode": "locked",
5
+ "tags": [
6
+ "changes",
7
+ "summary",
8
+ "read-only",
9
+ "review"
10
+ ],
11
+ "triggers": [
12
+ "summarize the current changes",
13
+ "what changed in this repo",
14
+ "give me a change summary",
15
+ "show me the current working tree status"
16
+ ],
17
+ "steps": [
18
+ "Inspect the current working tree, changed files, and the smallest relevant diff or file set.",
19
+ "Group changes by area, intent, or likely effect.",
20
+ "Call out obvious risks, open questions, or verification gaps from the observed changes.",
21
+ "Keep the result read-only and focused on summary."
22
+ ],
23
+ "validation": [
24
+ "Reference the files or diffs used to build the summary.",
25
+ "Avoid modifying repository files while summarizing changes."
26
+ ],
27
+ "examples": [
28
+ "Summarize the current working tree before I continue.",
29
+ "Explain the recent changes and any obvious follow-up risks."
30
+ ]
31
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: core-docs-entrypoint-guidance
3
+ description: Identify the best documentation entrypoints and reading order in a read-only way. Use when an agent needs to find canonical docs without changing the repo.
4
+ ---
5
+
6
+ # Core Docs Entrypoint Guidance
7
+
8
+ ## Goal
9
+
10
+ Point an agent to the smallest set of documents worth reading first and distinguish active references from lower-priority material.
11
+
12
+ ## Workflow
13
+
14
+ 1. Inspect the root `README.md`, `docs/` tree, and any obvious operations or reference directories.
15
+ 2. Separate canonical docs from working notes, archives, examples, or historical records.
16
+ 3. Recommend a practical reading order for the current task.
17
+ 4. Keep the output read-only and focused on navigation, not documentation edits.
18
+
19
+ ## Guardrails
20
+
21
+ - Do not create, edit, or delete repository files.
22
+ - Do not treat archived or draft material as current truth unless the repo says so.
23
+ - Prefer a short ordered list of entrypoints over a broad document dump.
@@ -0,0 +1,31 @@
1
+ {
2
+ "category": "docs",
3
+ "summary": "Identify the best documentation entrypoints and reading order from the existing repo.",
4
+ "management_mode": "locked",
5
+ "tags": [
6
+ "docs",
7
+ "navigation",
8
+ "read-only",
9
+ "onboarding"
10
+ ],
11
+ "triggers": [
12
+ "where should I start reading docs",
13
+ "find the canonical documentation",
14
+ "guide me through this repo's docs",
15
+ "what docs matter first"
16
+ ],
17
+ "steps": [
18
+ "Inspect the root README and the main docs directories.",
19
+ "Separate canonical references from working notes or archives.",
20
+ "Recommend the smallest useful reading order for the current task.",
21
+ "Keep the result read-only and grounded in the existing doc structure."
22
+ ],
23
+ "validation": [
24
+ "Call out which docs are active references and which are background material.",
25
+ "Avoid changing documentation files while guiding entrypoints."
26
+ ],
27
+ "examples": [
28
+ "Show me which docs to read first in this repository.",
29
+ "Point me to the canonical docs before I start implementation."
30
+ ]
31
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: core-project-summary
3
+ description: Produce a read-only summary of the repository's purpose, main components, and current constraints. Use when an agent needs fast onboarding context without changing the repo.
4
+ ---
5
+
6
+ # Core Project Summary
7
+
8
+ ## Goal
9
+
10
+ Build a short, source-grounded summary of what the repository does and what matters before deeper work.
11
+
12
+ ## Workflow
13
+
14
+ 1. Read the root `README.md`, top-level manifests, and the most relevant entry directories before summarizing.
15
+ 2. Identify the repository purpose, main subsystems, runtime or build stack, and current constraints from existing files only.
16
+ 3. Keep the output read-only: summarize what exists, cite the files you used, and do not create or modify repository files.
17
+ 4. End with the smallest set of next files or directories worth reading for the current task.
18
+
19
+ ## Guardrails
20
+
21
+ - Do not create, edit, or delete repository files.
22
+ - Do not invent architecture, roadmap, or ownership details that are not supported by source material.
23
+ - Prefer a short orientation summary over a long document.
@@ -0,0 +1,31 @@
1
+ {
2
+ "category": "docs",
3
+ "summary": "Summarize the repository purpose, main components, and current constraints from existing files.",
4
+ "management_mode": "locked",
5
+ "tags": [
6
+ "summary",
7
+ "onboarding",
8
+ "read-only",
9
+ "context"
10
+ ],
11
+ "triggers": [
12
+ "summarize this project",
13
+ "give me a project overview",
14
+ "what does this repo do",
15
+ "onboard me to this repository"
16
+ ],
17
+ "steps": [
18
+ "Read the root README, top-level manifests, and the most relevant entry directories.",
19
+ "Extract the repository purpose, main components, and current constraints from existing files only.",
20
+ "Keep the result read-only and grounded in the files you inspected.",
21
+ "Point to the next files or directories worth reading."
22
+ ],
23
+ "validation": [
24
+ "Name the files or directories used to build the summary.",
25
+ "Avoid unsupported claims or repo changes."
26
+ ],
27
+ "examples": [
28
+ "Summarize this repository before we start coding.",
29
+ "Give me a quick project overview and the most important files to read next."
30
+ ]
31
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: core-repo-structure-analysis
3
+ description: Map the repository layout, key entrypoints, and likely edit locations in a read-only way. Use when an agent needs to understand where code, docs, tests, or config live without changing the repo.
4
+ ---
5
+
6
+ # Core Repo Structure Analysis
7
+
8
+ ## Goal
9
+
10
+ Create a compact map of the repository layout so an agent can find the right area before making changes.
11
+
12
+ ## Workflow
13
+
14
+ 1. Inspect the top-level directories, manifests, and obvious entrypoint files before drawing conclusions.
15
+ 2. Group the repository into major areas such as application code, tests, docs, scripts, tooling, or generated assets.
16
+ 3. Explain which directories are likely to matter for the current task and why.
17
+ 4. Keep the analysis read-only and limit the output to navigation guidance rather than implementation advice.
18
+
19
+ ## Guardrails
20
+
21
+ - Do not create, edit, or delete repository files.
22
+ - Do not claim a directory is authoritative unless the repository structure supports that conclusion.
23
+ - Prefer a practical map of likely edit locations over a full file inventory.
@@ -0,0 +1,31 @@
1
+ {
2
+ "category": "workflow",
3
+ "summary": "Map the repository layout, entrypoints, and likely edit locations without changing files.",
4
+ "management_mode": "locked",
5
+ "tags": [
6
+ "structure",
7
+ "navigation",
8
+ "read-only",
9
+ "codebase"
10
+ ],
11
+ "triggers": [
12
+ "analyze this repo structure",
13
+ "map this codebase",
14
+ "where is everything in this repo",
15
+ "find the right directory for this task"
16
+ ],
17
+ "steps": [
18
+ "Inspect the top-level directories, manifests, and obvious entrypoints.",
19
+ "Group the repository into major areas such as app code, tests, docs, and tooling.",
20
+ "Call out the directories or files most relevant to the current task.",
21
+ "Keep the result read-only and focused on navigation."
22
+ ],
23
+ "validation": [
24
+ "Reference the directories or files that support the structure map.",
25
+ "Avoid changing the repository while analyzing it."
26
+ ],
27
+ "examples": [
28
+ "Map this repository before I start editing files.",
29
+ "Show me where the main code, tests, docs, and tooling live."
30
+ ]
31
+ }
@@ -452,6 +452,40 @@ class SkillAgentTests(unittest.TestCase):
452
452
  self.assertEqual(payload[0]["name"], "dusty-skill")
453
453
  self.assertEqual(payload[0]["status"], "candidate")
454
454
 
455
+ def test_usage_keeps_packaged_core_helper_protected(self) -> None:
456
+ old_timestamp = (datetime.now(UTC) - timedelta(days=60)).replace(microsecond=0).isoformat()
457
+ self.write_skill(
458
+ "core-project-summary",
459
+ description="Summarize the repository in a read-only way.",
460
+ metadata={
461
+ "category": "docs",
462
+ "summary": "Summarize the repository in a read-only way.",
463
+ "created_at": old_timestamp,
464
+ "updated_at": old_timestamp,
465
+ "management_mode": "locked",
466
+ },
467
+ )
468
+
469
+ result = subprocess.run(
470
+ [
471
+ sys.executable,
472
+ str(SCRIPT_PATH),
473
+ "usage",
474
+ "--repo-root",
475
+ str(self.repo_root),
476
+ "--status",
477
+ "protected",
478
+ "--json",
479
+ ],
480
+ check=True,
481
+ capture_output=True,
482
+ text=True,
483
+ )
484
+
485
+ payload = json.loads(result.stdout)
486
+ self.assertEqual(payload[0]["name"], "core-project-summary")
487
+ self.assertEqual(payload[0]["status"], "protected")
488
+
455
489
  def test_prune_apply_archives_candidate_skill(self) -> None:
456
490
  old_timestamp = (datetime.now(UTC) - timedelta(days=60)).replace(microsecond=0).isoformat()
457
491
  self.write_skill(
@@ -335,7 +335,14 @@ TITLE_CASE_OVERRIDES = {
335
335
  "xcode": "Xcode",
336
336
  }
337
337
 
338
- PROTECTED_SKILLS = {"project-skill-router", "omc-reference"}
338
+ PROTECTED_SKILLS = {
339
+ "project-skill-router",
340
+ "core-project-summary",
341
+ "core-repo-structure-analysis",
342
+ "core-docs-entrypoint-guidance",
343
+ "core-change-summary",
344
+ "omc-reference",
345
+ }
339
346
  ARCHIVE_DIRNAME = "_archived"
340
347
  USAGE_FILENAME = "usage.json"
341
348
  USAGE_HISTORY_LIMIT = 12
@@ -7,6 +7,7 @@ const fs = require("fs");
7
7
  const path = require("path");
8
8
 
9
9
  const MIN_PYTHON = { major: 3, minor: 10 };
10
+ const PACKAGE_NAME = "skill-automation-package";
10
11
  const INSTALL_METADATA_PATH = path.join(".claude", "skill-automation-package.json");
11
12
 
12
13
  function main() {
@@ -145,10 +146,35 @@ function detectInstallState(targetRoot, currentVersion) {
145
146
  try {
146
147
  const raw = fs.readFileSync(metadataPath, "utf8");
147
148
  const metadata = JSON.parse(raw);
149
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
150
+ return { status: "unknown", targetRoot, metadataPath, reason: "invalid-shape" };
151
+ }
152
+
153
+ if (typeof metadata.name !== "string" || metadata.name.trim() === "") {
154
+ return { status: "unknown", targetRoot, metadataPath, reason: "missing-name" };
155
+ }
156
+ const installedName = metadata.name.trim();
157
+ if (installedName !== PACKAGE_NAME) {
158
+ return {
159
+ status: "unknown",
160
+ targetRoot,
161
+ metadataPath,
162
+ reason: "wrong-package",
163
+ installedName,
164
+ };
165
+ }
166
+
148
167
  if (typeof metadata.version !== "string" || metadata.version.trim() === "") {
149
168
  return { status: "unknown", targetRoot, metadataPath, reason: "missing-version" };
150
169
  }
151
170
 
171
+ if (!Array.isArray(metadata.assets)) {
172
+ return { status: "unknown", targetRoot, metadataPath, reason: "invalid-assets" };
173
+ }
174
+ if (!metadata.assets.every((asset) => typeof asset === "string" && asset.trim() !== "")) {
175
+ return { status: "unknown", targetRoot, metadataPath, reason: "invalid-assets" };
176
+ }
177
+
152
178
  const installedVersion = metadata.version.trim();
153
179
  const installedParsed = parseVersion(installedVersion);
154
180
  const currentParsed = parseVersion(currentVersion);
@@ -252,12 +278,7 @@ function printLifecycleSafety() {
252
278
 
253
279
  function describeUnknownMetadata(state, mode, currentVersion) {
254
280
  const metadataPath = state?.metadataPath || INSTALL_METADATA_PATH;
255
- const reasonLabel = {
256
- "invalid-json": "could not be parsed as JSON",
257
- "missing-version": "does not contain a usable version",
258
- "invalid-version": "contains a version that could not be compared",
259
- };
260
- const detail = reasonLabel[state?.reason] || "could not be compared";
281
+ const detail = describeMetadataProblem(state);
261
282
 
262
283
  if (mode === "install") {
263
284
  return `skill-automation-package: existing install metadata at ${metadataPath} ${detail}; reinstalling with version ${currentVersion}.`;
@@ -266,6 +287,19 @@ function describeUnknownMetadata(state, mode, currentVersion) {
266
287
  return `skill-automation-package: existing install metadata at ${metadataPath} ${detail}; use install to force a reinstall.`;
267
288
  }
268
289
 
290
+ function describeMetadataProblem(state) {
291
+ const reasonLabel = {
292
+ "invalid-json": "could not be parsed as JSON",
293
+ "invalid-shape": "is not a JSON object",
294
+ "missing-name": "does not contain a usable package name",
295
+ "wrong-package": `belongs to package "${state?.installedName || "(unknown)"}" instead of ${PACKAGE_NAME}`,
296
+ "missing-version": "does not contain a usable version",
297
+ "invalid-version": "contains a version that could not be compared",
298
+ "invalid-assets": "does not contain a usable assets list",
299
+ };
300
+ return reasonLabel[state?.reason] || "could not be compared";
301
+ }
302
+
269
303
  function printUsage(subcommand) {
270
304
  if (subcommand) {
271
305
  console.error(`skill-automation-package: unsupported subcommand "${subcommand}".`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-automation-package",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Portable repo-local skill automation bundle for Codex and Claude Code.",
5
5
  "bin": {
6
6
  "skill-automation-package": "bin/skill-automation-package.js"
@@ -11,6 +11,10 @@
11
11
  "scripts/package_layout.py",
12
12
  "assets/.claude/tools/skill_agent.py",
13
13
  "assets/.claude/skills/project-skill-router/",
14
+ "assets/.claude/skills/core-project-summary/",
15
+ "assets/.claude/skills/core-repo-structure-analysis/",
16
+ "assets/.claude/skills/core-docs-entrypoint-guidance/",
17
+ "assets/.claude/skills/core-change-summary/",
14
18
  "assets/.claude/tests/test_skill_agent.py",
15
19
  "templates/agents_block.md",
16
20
  "templates/claude_block.md",
@@ -25,12 +29,17 @@
25
29
  "test:wrapper": "node --test tests/node/wrapper.test.js",
26
30
  "test:assets": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s assets/.claude/tests -p 'test_*.py'",
27
31
  "install:dry-run": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/install.py --target /tmp/skill-automation-package-dry-run --dry-run",
32
+ "release:integrity": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/check_release_integrity.py",
28
33
  "sync-assets": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/sync_assets.py",
29
- "release:check": "npm run test && npm run test:assets && npm run test:wrapper && npm run install:dry-run && npm pack --dry-run --cache /tmp/skill-automation-package-npm-cache"
34
+ "release:check": "npm run test && npm run test:assets && npm run test:wrapper && npm run release:integrity && npm run install:dry-run && npm pack --dry-run --cache /tmp/skill-automation-package-npm-cache"
30
35
  },
31
36
  "managed_assets": [
32
37
  ".claude/tools/skill_agent.py",
33
- ".claude/skills/project-skill-router"
38
+ ".claude/skills/project-skill-router",
39
+ ".claude/skills/core-project-summary",
40
+ ".claude/skills/core-repo-structure-analysis",
41
+ ".claude/skills/core-docs-entrypoint-guidance",
42
+ ".claude/skills/core-change-summary"
34
43
  ],
35
44
  "optional_assets": [
36
45
  ".claude/tests/test_skill_agent.py"
@@ -5,6 +5,7 @@ import argparse
5
5
  import json
6
6
  import subprocess
7
7
  import sys
8
+ from dataclasses import dataclass
8
9
  from datetime import datetime
9
10
  from pathlib import Path
10
11
 
@@ -21,6 +22,7 @@ from package_layout import (
21
22
  PackageLayout,
22
23
  TEMPLATES_ROOT,
23
24
  copy_assets,
25
+ iter_asset_files,
24
26
  load_package_layout,
25
27
  )
26
28
 
@@ -34,6 +36,44 @@ CLAUDE_MARKERS = (
34
36
  "<!-- SKILL-AUTOMATION:CLAUDE:END -->",
35
37
  )
36
38
 
39
+ GITIGNORE_MARKERS = (
40
+ "# SKILL-AUTOMATION:GITIGNORE:START",
41
+ "# SKILL-AUTOMATION:GITIGNORE:END",
42
+ )
43
+
44
+ GITIGNORE_PATTERNS = {
45
+ "generated": (
46
+ "# Generated skill automation state",
47
+ ".claude/skills/registry.json",
48
+ ".claude/skills/usage.json",
49
+ ".claude/skills/_archived/",
50
+ ".claude/skill-automation-package.json",
51
+ ".claude/**/__pycache__/",
52
+ ".claude/**/*.pyc",
53
+ ),
54
+ "local-only": (
55
+ "# Local-only skill automation install",
56
+ ".claude/",
57
+ "AGENTS.md",
58
+ "CLAUDE.md",
59
+ ),
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class ManagedBlockPlan:
65
+ changed: bool
66
+ state: str
67
+ updated: str
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class PackageFilePreview:
72
+ would_create: tuple[Path, ...]
73
+ would_update: tuple[Path, ...]
74
+ unchanged: tuple[Path, ...]
75
+ stale: tuple[Path, ...]
76
+
37
77
 
38
78
  def main() -> int:
39
79
  parser = build_parser()
@@ -52,11 +92,11 @@ def main() -> int:
52
92
  executable_assets=layout.executable_assets,
53
93
  dry_run=args.dry_run,
54
94
  )
55
- wrote_agents = False
56
- wrote_claude = False
95
+ agents_plan = ManagedBlockPlan(changed=False, state="skipped", updated="")
96
+ claude_plan = ManagedBlockPlan(changed=False, state="skipped", updated="")
57
97
 
58
98
  if not args.skip_agents:
59
- wrote_agents = install_managed_block(
99
+ agents_plan = install_managed_block(
60
100
  target_file=target / "AGENTS.md",
61
101
  template_path=TEMPLATES_ROOT / "agents_block.md",
62
102
  markers=AGENTS_MARKERS,
@@ -65,7 +105,7 @@ def main() -> int:
65
105
  )
66
106
 
67
107
  if not args.skip_claude:
68
- wrote_claude = install_managed_block(
108
+ claude_plan = install_managed_block(
69
109
  target_file=target / "CLAUDE.md",
70
110
  template_path=TEMPLATES_ROOT / "claude_block.md",
71
111
  markers=CLAUDE_MARKERS,
@@ -73,6 +113,12 @@ def main() -> int:
73
113
  dry_run=args.dry_run,
74
114
  )
75
115
 
116
+ gitignore_plan = install_gitignore_block(
117
+ target_file=target / ".gitignore",
118
+ mode=args.gitignore_mode,
119
+ dry_run=args.dry_run,
120
+ )
121
+
76
122
  manifest_path = target / ".claude" / "skill-automation-package.json"
77
123
  wrote_manifest = write_install_manifest(
78
124
  manifest_path=manifest_path,
@@ -90,12 +136,26 @@ def main() -> int:
90
136
  agents_label = "Would update AGENTS.md" if args.dry_run else "Updated AGENTS.md"
91
137
  claude_label = "Would update CLAUDE.md" if args.dry_run else "Updated CLAUDE.md"
92
138
  manifest_label = "Would write install manifest" if args.dry_run else "Wrote install manifest"
139
+ copied_label = "Would copy files" if args.dry_run else "Copied files"
140
+ gitignore_label = "Would update .gitignore" if args.dry_run else "Updated .gitignore"
93
141
  print(f"Installed skill automation package {layout.version} into {target}")
94
- print(f"Copied files: {len(copied_files)}")
95
- print(f"{agents_label}: {'yes' if wrote_agents else 'no'}")
96
- print(f"{claude_label}: {'yes' if wrote_claude else 'no'}")
142
+ print(f"{copied_label}: {len(copied_files)}")
143
+ print(f"{agents_label}: {'yes' if agents_plan.changed else 'no'}")
144
+ print(f"{claude_label}: {'yes' if claude_plan.changed else 'no'}")
145
+ print(f"{gitignore_label}: {'yes' if gitignore_plan.changed else 'no'}")
97
146
  print(f"{manifest_label}: {'yes' if wrote_manifest else 'no'}")
98
147
  print(f"Refreshed registry: {'yes' if refreshed else 'no'}")
148
+ if args.dry_run:
149
+ print_dry_run_preview(
150
+ build_package_file_preview(
151
+ layout=layout,
152
+ target_root=target,
153
+ asset_paths=selected_assets,
154
+ ),
155
+ agents_state=agents_plan.state,
156
+ claude_state=claude_plan.state,
157
+ gitignore_state=gitignore_plan.state,
158
+ )
99
159
  return 0
100
160
 
101
161
 
@@ -129,6 +189,16 @@ def build_parser() -> argparse.ArgumentParser:
129
189
  action="store_true",
130
190
  help="Preview the installation without writing files.",
131
191
  )
192
+ parser.add_argument(
193
+ "--gitignore-mode",
194
+ choices=("generated", "local-only", "none"),
195
+ default="generated",
196
+ help=(
197
+ "Manage a package-owned .gitignore block. `generated` ignores only "
198
+ "generated state, `local-only` ignores the whole local install, and "
199
+ "`none` leaves .gitignore untouched."
200
+ ),
201
+ )
132
202
  return parser
133
203
 
134
204
 
@@ -139,16 +209,85 @@ def install_managed_block(
139
209
  markers: tuple[str, str],
140
210
  title: str,
141
211
  dry_run: bool,
142
- ) -> bool:
212
+ ) -> ManagedBlockPlan:
213
+ plan = build_managed_block_plan(
214
+ target_file=target_file,
215
+ template_path=template_path,
216
+ markers=markers,
217
+ title=title,
218
+ )
219
+ if not dry_run:
220
+ target_file.parent.mkdir(parents=True, exist_ok=True)
221
+ target_file.write_text(plan.updated, encoding="utf-8")
222
+ return plan
223
+
224
+
225
+ def build_managed_block_plan(
226
+ *,
227
+ target_file: Path,
228
+ template_path: Path,
229
+ markers: tuple[str, str],
230
+ title: str,
231
+ ) -> ManagedBlockPlan:
143
232
  block = template_path.read_text(encoding="utf-8").strip() + "\n"
144
233
  start_marker, end_marker = markers
145
234
  existing = target_file.read_text(encoding="utf-8") if target_file.exists() else ""
146
235
  updated = upsert_block(existing, block, start_marker, end_marker, title)
236
+ state = classify_managed_block_state(existing, updated, start_marker, end_marker)
237
+ return ManagedBlockPlan(changed=updated != existing, state=state, updated=updated)
238
+
239
+
240
+ def install_gitignore_block(
241
+ *,
242
+ target_file: Path,
243
+ mode: str,
244
+ dry_run: bool,
245
+ ) -> ManagedBlockPlan:
246
+ if mode == "none":
247
+ return ManagedBlockPlan(changed=False, state="skipped", updated="")
248
+
249
+ block = build_gitignore_block(mode)
250
+ existing = target_file.read_text(encoding="utf-8") if target_file.exists() else ""
251
+ updated = upsert_block(
252
+ existing,
253
+ block,
254
+ GITIGNORE_MARKERS[0],
255
+ GITIGNORE_MARKERS[1],
256
+ "",
257
+ )
258
+ state = classify_managed_block_state(
259
+ existing,
260
+ updated,
261
+ GITIGNORE_MARKERS[0],
262
+ GITIGNORE_MARKERS[1],
263
+ )
147
264
 
148
265
  if not dry_run:
149
266
  target_file.parent.mkdir(parents=True, exist_ok=True)
150
267
  target_file.write_text(updated, encoding="utf-8")
151
- return updated != existing
268
+ return ManagedBlockPlan(changed=updated != existing, state=state, updated=updated)
269
+
270
+
271
+ def build_gitignore_block(mode: str) -> str:
272
+ patterns = GITIGNORE_PATTERNS[mode]
273
+ return "\n".join([GITIGNORE_MARKERS[0], *patterns, GITIGNORE_MARKERS[1]]) + "\n"
274
+
275
+
276
+ def classify_managed_block_state(
277
+ existing: str,
278
+ updated: str,
279
+ start_marker: str,
280
+ end_marker: str,
281
+ ) -> str:
282
+ if updated == existing:
283
+ return "unchanged"
284
+ if not existing.strip():
285
+ return "create"
286
+ start_index = existing.find(start_marker)
287
+ end_index = existing.find(end_marker)
288
+ if start_index != -1 and end_index != -1 and end_index >= start_index:
289
+ return "replace"
290
+ return "append"
152
291
 
153
292
 
154
293
  def upsert_block(
@@ -204,5 +343,94 @@ def refresh_registry(target: Path) -> None:
204
343
  )
205
344
 
206
345
 
346
+ def build_package_file_preview(
347
+ *,
348
+ layout: PackageLayout,
349
+ target_root: Path,
350
+ asset_paths: list[Path],
351
+ ) -> PackageFilePreview:
352
+ current_assets: set[Path] = set()
353
+ would_create: list[Path] = []
354
+ would_update: list[Path] = []
355
+ unchanged: list[Path] = []
356
+
357
+ for source_path, relative_path in iter_asset_files(ASSETS_ROOT, asset_paths):
358
+ current_assets.add(relative_path)
359
+ target_path = target_root / relative_path
360
+ if not target_path.exists():
361
+ would_create.append(relative_path)
362
+ continue
363
+ if files_match(source_path, target_path):
364
+ unchanged.append(relative_path)
365
+ continue
366
+ would_update.append(relative_path)
367
+
368
+ stale = [
369
+ path
370
+ for path in read_previous_install_assets(target_root)
371
+ if path not in current_assets and (target_root / path).exists()
372
+ ]
373
+
374
+ return PackageFilePreview(
375
+ would_create=tuple(sorted(would_create)),
376
+ would_update=tuple(sorted(would_update)),
377
+ unchanged=tuple(sorted(unchanged)),
378
+ stale=tuple(sorted(stale)),
379
+ )
380
+
381
+
382
+ def files_match(source_path: Path, target_path: Path) -> bool:
383
+ if not target_path.is_file():
384
+ return False
385
+ return source_path.read_bytes() == target_path.read_bytes()
386
+
387
+
388
+ def read_previous_install_assets(target_root: Path) -> list[Path]:
389
+ manifest_path = target_root / ".claude" / "skill-automation-package.json"
390
+ if not manifest_path.exists():
391
+ return []
392
+ try:
393
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
394
+ except json.JSONDecodeError:
395
+ return []
396
+ assets = payload.get("assets")
397
+ if not isinstance(assets, list):
398
+ return []
399
+
400
+ previous: list[Path] = []
401
+ for value in assets:
402
+ if not isinstance(value, str):
403
+ continue
404
+ path = Path(value)
405
+ if path.is_absolute():
406
+ continue
407
+ previous.append(path)
408
+ return previous
409
+
410
+
411
+ def print_dry_run_preview(
412
+ preview: PackageFilePreview,
413
+ *,
414
+ agents_state: str,
415
+ claude_state: str,
416
+ gitignore_state: str,
417
+ ) -> None:
418
+ print("Package file preview:")
419
+ print_preview_paths("would create", preview.would_create)
420
+ print_preview_paths("would update", preview.would_update)
421
+ print_preview_paths("unchanged", preview.unchanged)
422
+ print_preview_paths("previously installed but no longer shipped", preview.stale)
423
+ print("Managed file preview:")
424
+ print(f" AGENTS.md: {agents_state}")
425
+ print(f" CLAUDE.md: {claude_state}")
426
+ print(f" .gitignore: {gitignore_state}")
427
+
428
+
429
+ def print_preview_paths(label: str, paths: tuple[Path, ...]) -> None:
430
+ print(f" {label}: {len(paths)}")
431
+ for path in paths:
432
+ print(f" - {path}")
433
+
434
+
207
435
  if __name__ == "__main__":
208
436
  raise SystemExit(main())