Skip to content

Commit 74dc3f0

Browse files
committed
feat(skills): install full skill bundles
1 parent 90208e7 commit 74dc3f0

7 files changed

Lines changed: 155 additions & 81 deletions

File tree

build.rs

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,101 @@ use edit_ops_spec::{
1111
};
1212
use std::error::Error;
1313
use std::fs;
14-
use std::path::Path;
14+
use std::path::{Path, PathBuf};
15+
16+
// Project-local assets installed by `govctl init-skills`.
17+
// Plugin/global-only skills such as `init` are intentionally omitted.
18+
const DISTRIBUTED_SKILL_DIRS: &[&str] = &[
19+
"discuss",
20+
"gov",
21+
"quick",
22+
"spec",
23+
"rfc-writer",
24+
"adr-writer",
25+
"wi-writer",
26+
"guard-writer",
27+
"commit",
28+
"migrate",
29+
"decision-analysis",
30+
"detach",
31+
];
1532

1633
fn main() {
1734
// Recompile if any embedded .claude/ assets change
18-
// Skills
19-
println!("cargo:rerun-if-changed=.claude/skills/discuss/SKILL.md");
20-
println!("cargo:rerun-if-changed=.claude/skills/gov/SKILL.md");
21-
println!("cargo:rerun-if-changed=.claude/skills/quick/SKILL.md");
22-
println!("cargo:rerun-if-changed=.claude/skills/spec/SKILL.md");
23-
println!("cargo:rerun-if-changed=.claude/skills/rfc-writer/SKILL.md");
24-
println!("cargo:rerun-if-changed=.claude/skills/adr-writer/SKILL.md");
25-
println!("cargo:rerun-if-changed=.claude/skills/wi-writer/SKILL.md");
26-
println!("cargo:rerun-if-changed=.claude/skills/commit/SKILL.md");
27-
println!("cargo:rerun-if-changed=.claude/skills/migrate/SKILL.md");
28-
println!("cargo:rerun-if-changed=.claude/skills/decision-analysis/SKILL.md");
29-
println!("cargo:rerun-if-changed=.claude/skills/detach/SKILL.md");
30-
// Agents
31-
println!("cargo:rerun-if-changed=.claude/agents/rfc-reviewer.md");
32-
println!("cargo:rerun-if-changed=.claude/agents/adr-reviewer.md");
33-
println!("cargo:rerun-if-changed=.claude/agents/wi-reviewer.md");
34-
println!("cargo:rerun-if-changed=.claude/agents/compliance-checker.md");
35+
println!("cargo:rerun-if-changed=.claude/skills");
36+
println!("cargo:rerun-if-changed=.claude/agents");
3537

3638
// Edit rules SSOT + schema (ADR-0030)
3739
println!("cargo:rerun-if-changed=gov/schema/edit-ops.schema.json");
3840
println!("cargo:rerun-if-changed=gov/schema/edit-ops.json");
3941

42+
generate_skill_assets().expect("failed to generate skill asset manifest");
4043
generate_edit_rules().expect("failed to generate edit rules from SSOT");
4144
generate_codex_agent_templates().expect("failed to generate codex agent templates");
4245
}
4346

47+
fn generate_skill_assets() -> Result<(), Box<dyn Error>> {
48+
let skill_root = Path::new(".claude/skills");
49+
let mut files = Vec::new();
50+
for skill_dir in DISTRIBUTED_SKILL_DIRS {
51+
collect_files(&skill_root.join(skill_dir), &mut files)?;
52+
}
53+
files.sort();
54+
55+
let mut out = String::new();
56+
out.push_str("// @generated by build.rs from distributed .claude/skills/* bundles\n");
57+
out.push_str("// Do not edit manually.\n\n");
58+
out.push_str("pub const SKILL_ASSETS: &[(&str, &str)] = &[\n");
59+
for file in files {
60+
let skill_rel = file
61+
.strip_prefix(skill_root)
62+
.map_err(|e| format!("failed to relativize skill asset {}: {e}", file.display()))?;
63+
let skill_rel = path_to_slash_string(skill_rel)?;
64+
let output_rel = format!("skills/{skill_rel}");
65+
let source_rel = format!("/.claude/skills/{skill_rel}");
66+
out.push_str(&format!(
67+
" ({:?}, include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), {:?}))),\n",
68+
output_rel, source_rel
69+
));
70+
}
71+
out.push_str("];\n");
72+
73+
let out_dir = std::env::var("OUT_DIR")?;
74+
let out_path = Path::new(&out_dir).join("skill_assets.rs");
75+
fs::write(out_path, out)?;
76+
Ok(())
77+
}
78+
79+
fn collect_files(root: &Path, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
80+
for entry in
81+
fs::read_dir(root).map_err(|e| format!("failed to read {}: {e}", root.display()))?
82+
{
83+
let entry = entry?;
84+
let path = entry.path();
85+
let file_type = entry.file_type()?;
86+
if file_type.is_dir() {
87+
collect_files(&path, files)?;
88+
} else if file_type.is_file() {
89+
files.push(path);
90+
}
91+
}
92+
Ok(())
93+
}
94+
95+
fn path_to_slash_string(path: &Path) -> Result<String, Box<dyn Error>> {
96+
let mut parts = Vec::new();
97+
for component in path.components() {
98+
let component = component.as_os_str().to_str().ok_or_else(|| {
99+
format!(
100+
"skill asset path is not valid UTF-8: {}",
101+
path.to_string_lossy()
102+
)
103+
})?;
104+
parts.push(component.to_string());
105+
}
106+
Ok(parts.join("/"))
107+
}
108+
44109
fn generate_edit_rules() -> Result<(), Box<dyn Error>> {
45110
let spec_path = Path::new("gov/schema/edit-ops.json");
46111
let schema_path = Path::new("gov/schema/edit-ops.schema.json");

docs/rfc/RFC-0002.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<!-- GENERATED: do not edit. Source: RFC-0002 -->
2-
<!-- SIGNATURE: sha256:f77cd633c97a2b62f7d830fdb04bb0de811727ebe2c3d9abfdc2dd9c4983eed7 -->
2+
<!-- SIGNATURE: sha256:dd126a9195850f89dbb0ba90abee032ce072eb83017a465fd798276b9a7dbd3f -->
33

44
# RFC-0002: CLI Resource Model and Command Architecture
55

6-
> **Version:** 0.10.1 | **Status:** normative | **Phase:** test
6+
> **Version:** 0.10.2 | **Status:** normative | **Phase:** test
77
88
---
99

@@ -552,8 +552,9 @@ Syntax: `govctl init-skills [--force] [--format <claude|codex>] [--dir PATH]`
552552
Behavior:
553553
- `--dir` overrides the output directory for this invocation. Resolution order: `--dir` flag > `agent_dir` from config > format-implied default (`.claude` for claude, `.codex` for codex)
554554
- `--format` selects the output format for agent definitions (default `claude`):
555-
- `claude`: skills as `skills/*/SKILL.md`, agents as `agents/*.md` with YAML frontmatter (compatible with Claude Code, Cursor, Windsurf, and similar editors)
556-
- `codex`: skills as `skills/*/SKILL.md` (same format), agents as `agents/*.toml` with `developer_instructions` field (compatible with Codex CLI)
555+
- `claude`: skills as full `skills/*/` bundles rooted at `SKILL.md`, agents as `agents/*.md` with YAML frontmatter (compatible with Claude Code, Cursor, Windsurf, and similar editors)
556+
- `codex`: skills as full `skills/*/` bundles rooted at `SKILL.md` (same format), agents as `agents/*.toml` with `developer_instructions` field (compatible with Codex CLI)
557+
- Skill bundle resources under directories such as `references/`, `assets/`, and `scripts/` MUST be installed with their parent skill; agent output remains format-specific
557558
- Skips files that already exist unless `--force` is used
558559
- Reports created/updated/skipped counts
559560
- This command is separate from `init` because plugin users receive skills globally and do not need local copies
@@ -731,6 +732,14 @@ Search is discovery across the governance corpus, not a resource-specific CRUD o
731732

732733
## Changelog
733734

735+
### v0.10.2 (2026-06-08)
736+
737+
Clarify init-skills skill bundle installation
738+
739+
#### Changed
740+
741+
- init-skills installs full skill bundles rooted at SKILL.md, including bundled references/assets/scripts
742+
734743
### v0.10.1 (2026-06-04)
735744

736745
Set search clause version metadata

gov/rfc/RFC-0002/clauses/C-GLOBAL-COMMANDS.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ Syntax: `govctl init-skills [--force] [--format <claude|codex>] [--dir PATH]`
150150
Behavior:
151151
- `--dir` overrides the output directory for this invocation. Resolution order: `--dir` flag > `agent_dir` from config > format-implied default (`.claude` for claude, `.codex` for codex)
152152
- `--format` selects the output format for agent definitions (default `claude`):
153-
- `claude`: skills as `skills/*/SKILL.md`, agents as `agents/*.md` with YAML frontmatter (compatible with Claude Code, Cursor, Windsurf, and similar editors)
154-
- `codex`: skills as `skills/*/SKILL.md` (same format), agents as `agents/*.toml` with `developer_instructions` field (compatible with Codex CLI)
153+
- `claude`: skills as full `skills/*/` bundles rooted at `SKILL.md`, agents as `agents/*.md` with YAML frontmatter (compatible with Claude Code, Cursor, Windsurf, and similar editors)
154+
- `codex`: skills as full `skills/*/` bundles rooted at `SKILL.md` (same format), agents as `agents/*.toml` with `developer_instructions` field (compatible with Codex CLI)
155+
- Skill bundle resources under directories such as `references/`, `assets/`, and `scripts/` MUST be installed with their parent skill; agent output remains format-specific
155156
- Skips files that already exist unless `--force` is used
156157
- Reports created/updated/skipped counts
157158
- This command is separate from `init` because plugin users receive skills globally and do not need local copies

gov/rfc/RFC-0002/rfc.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
[govctl]
44
id = "RFC-0002"
55
title = "CLI Resource Model and Command Architecture"
6-
version = "0.10.1"
6+
version = "0.10.2"
77
status = "normative"
88
phase = "test"
99
owners = ["@govctl-org"]
1010
created = "2026-01-19"
11-
updated = "2026-06-04"
11+
updated = "2026-06-08"
1212
tags = [
1313
"cli",
1414
"editing",
1515
"lifecycle",
1616
"validation",
1717
"release",
1818
]
19-
signature = "681093335724498e211e93fb519c9e16f9990da7986a2f50db914b9e60e88339"
19+
signature = "dd126a9195850f89dbb0ba90abee032ce072eb83017a465fd798276b9a7dbd3f"
2020

2121
[[sections]]
2222
title = "Summary"
@@ -36,6 +36,12 @@ clauses = [
3636
"clauses/C-SEARCH-COMMAND.toml",
3737
]
3838

39+
[[changelog]]
40+
version = "0.10.2"
41+
date = "2026-06-08"
42+
notes = "Clarify init-skills skill bundle installation"
43+
changed = ["init-skills installs full skill bundles rooted at SKILL.md, including bundled references/assets/scripts"]
44+
3945
[[changelog]]
4046
version = "0.10.1"
4147
date = "2026-06-04"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#:schema ../schema/work.schema.json
2+
3+
[govctl]
4+
id = "WI-2026-06-08-002"
5+
title = "Support bundled skill installation"
6+
status = "done"
7+
created = "2026-06-08"
8+
started = "2026-06-08"
9+
completed = "2026-06-08"
10+
refs = [
11+
"ADR-0024",
12+
"ADR-0028",
13+
"ADR-0035",
14+
"RFC-0002",
15+
]
16+
tags = ["skills-agents"]
17+
18+
[content]
19+
description = "Support skill directory bundles so init-skills installs all files under explicitly distributed skill roots instead of relying on SKILL.md-only template entries."
20+
21+
[[content.acceptance_criteria]]
22+
text = "init-skills installs full skill directory bundles, including bundled references and assets"
23+
status = "done"
24+
category = "changed"
25+
26+
[[content.acceptance_criteria]]
27+
text = "govctl check and relevant tests pass"
28+
status = "done"
29+
category = "chore"

src/cmd/new/skills.rs

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,9 @@ use crate::diagnostic::{DiagnosticResult, Diagnostics};
55
use crate::ui;
66
use crate::write::{WriteOp, create_dir_all, write_file};
77

8-
/// Skill templates: (relative_path, content) pairs.
9-
/// Source of truth: .claude/skills/; embedded at compile time.
10-
/// Per ADR-0028, all workflow commands are now skills for cross-platform compatibility.
11-
const SKILL_TEMPLATES: &[(&str, &str)] = &[
12-
(
13-
"skills/discuss/SKILL.md",
14-
include_str!("../../../.claude/skills/discuss/SKILL.md"),
15-
),
16-
(
17-
"skills/gov/SKILL.md",
18-
include_str!("../../../.claude/skills/gov/SKILL.md"),
19-
),
20-
(
21-
"skills/quick/SKILL.md",
22-
include_str!("../../../.claude/skills/quick/SKILL.md"),
23-
),
24-
(
25-
"skills/spec/SKILL.md",
26-
include_str!("../../../.claude/skills/spec/SKILL.md"),
27-
),
28-
(
29-
"skills/rfc-writer/SKILL.md",
30-
include_str!("../../../.claude/skills/rfc-writer/SKILL.md"),
31-
),
32-
(
33-
"skills/adr-writer/SKILL.md",
34-
include_str!("../../../.claude/skills/adr-writer/SKILL.md"),
35-
),
36-
(
37-
"skills/wi-writer/SKILL.md",
38-
include_str!("../../../.claude/skills/wi-writer/SKILL.md"),
39-
),
40-
(
41-
"skills/guard-writer/SKILL.md",
42-
include_str!("../../../.claude/skills/guard-writer/SKILL.md"),
43-
),
44-
(
45-
"skills/commit/SKILL.md",
46-
include_str!("../../../.claude/skills/commit/SKILL.md"),
47-
),
48-
(
49-
"skills/migrate/SKILL.md",
50-
include_str!("../../../.claude/skills/migrate/SKILL.md"),
51-
),
52-
(
53-
"skills/decision-analysis/SKILL.md",
54-
include_str!("../../../.claude/skills/decision-analysis/SKILL.md"),
55-
),
56-
(
57-
"skills/detach/SKILL.md",
58-
include_str!("../../../.claude/skills/detach/SKILL.md"),
59-
),
60-
];
8+
// Skill bundle assets are generated recursively from .claude/skills/** by build.rs.
9+
// Implements [[RFC-0002:C-GLOBAL-COMMANDS]] and [[ADR-0028]].
10+
include!(concat!(env!("OUT_DIR"), "/skill_assets.rs"));
6111

6212
/// Agent templates: (relative_path, content) pairs.
6313
/// Source of truth: .claude/agents/; embedded at compile time.
@@ -130,7 +80,7 @@ pub fn sync_skills(
13080
let mut synced = 0;
13181
let mut skipped = 0;
13282

133-
for (rel_path, template) in SKILL_TEMPLATES.iter().chain(agent_templates.iter()) {
83+
for (rel_path, template) in SKILL_ASSETS.iter().chain(agent_templates.iter()) {
13484
let path = agent_dir.join(rel_path);
13585
let display_path = config.display_path(&path);
13686

tests/test_agent_dir.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ fn test_default_agent_dir() -> common::TestResult {
2525
Ok(())
2626
}
2727

28+
#[test]
29+
fn test_init_skills_excludes_plugin_only_init_skill() -> common::TestResult {
30+
let temp_dir = init_project()?;
31+
32+
run_commands(temp_dir.path(), &[&["init-skills"]])?;
33+
34+
let init_skill = temp_dir.path().join(".claude/skills/init/SKILL.md");
35+
assert!(
36+
!init_skill.exists(),
37+
"init is a plugin/global onboarding skill, not a project-local init-skills asset"
38+
);
39+
Ok(())
40+
}
41+
2842
#[test]
2943
fn test_wi_writer_recommends_verification_guards() -> common::TestResult {
3044
let temp_dir = init_project()?;

0 commit comments

Comments
 (0)