Three polish items from post-audit parallel agent.
1. Dynamic schema whitelist (drops hardcoded const drift)
_assembler/src/schemas_export.rs (NEW, 136 LOC) — loader cube,
priority path $AGENT_ROOT/artifacts/schemas.json →
~/.claude/agents/artifacts/schemas.json → BUILTIN fallback.
Hand-rolled JSON parser (no serde_json dep).
_assembler/src/validator.rs delegates to schemas_export::load,
keeps KNOWN_ARTIFACT_SCHEMAS alias for back-compat.
_primitives/_rust/kei-artifact/src/export.rs (NEW, 82 LOC) —
write() + render() + default_path().
_primitives/_rust/kei-artifact/src/cli_cmds.rs (NEW, 126 LOC) —
extracted cmd_emit/get/list/chain so main stays <200 LOC.
ExportSchemas + ListSchemas subcommands; cmd_register
auto-refreshes export file (best-effort).
2. KNOWN_SCHEMAS SSoT — documented-dual-const + drift-test
(Option "simpler than new crate"). SSoT in kei-artifact's
BUILTIN; schemas_export::BUILTIN is a documented mirror;
builtin_schemas_do_not_drift_from_kei_artifact test in
validator.rs parses the primitive's source at test time and
diffs. <30 LOC change. No workspace structural change —
assembler stays decoupled from runtime primitive.
3. Agent-to-mode matrix + wizard Phase 3.6
_blocks/mode-matrix.md (NEW, 24 LOC) — 11-row table mapping
agent role × recommended mode blocks.
skills/new-agent/SKILL.md — new Phase 3.6 (between name-confirm
3.5 and manifest-write 4). AskUserQuestion with 5
cognitive-mode options (skeptic/devils-advocate/minimalist/
maximalist/first-principles, multiSelect). Appends picked
labels to manifest's blocks array. Defaults to NONE.
_blocks/README.md adds one-line reference to the matrix.
_assembler/tests/mode_blocks.rs (NEW, 78 LOC) — 3 integration
tests lock the wiring.
README.md — all accumulated count + pre-built-binaries + plugin
section edits from the v0.16 cycle consolidated here (will be
replaced by markers in v0.17 counts-autogen refactor).
Tests: assembler 24 → 33 (+9), kei-artifact 24 → 31 (+7), total
48 → 64. cargo check --workspace clean.
Constructor Pattern: largest new file validator.rs 180 LOC.
Pre-existing flagged for separate refactor: kei-artifact
validate.rs 268 LOC (not touched by this polish).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
3.6 KiB
Rust
115 lines
3.6 KiB
Rust
//! CLI entry: build [--validate] [--in-place] [<manifest.toml> ...]
|
|
//!
|
|
//! Default: read all _manifests/*.toml, write to _generated/*.md.
|
|
//! --in-place: write to agents/<name>.md (replaces generated file).
|
|
//! --validate: parse + validate only, no output.
|
|
//! Positional args: specific manifest files to process.
|
|
|
|
mod assembler;
|
|
mod manifest;
|
|
mod placeholders;
|
|
mod schemas_export;
|
|
mod validator;
|
|
|
|
use manifest::Manifest;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::ExitCode;
|
|
use std::{env, fs};
|
|
|
|
fn main() -> ExitCode {
|
|
let root = root_dir();
|
|
let blocks = root.join("_blocks");
|
|
let manifests = root.join("_manifests");
|
|
let generated = root.join("_generated");
|
|
|
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
let validate_only = args.iter().any(|a| a == "--validate");
|
|
let in_place = args.iter().any(|a| a == "--in-place");
|
|
let targets: Vec<&String> = args.iter().filter(|a| !a.starts_with("--")).collect();
|
|
|
|
let paths: Vec<PathBuf> = if targets.is_empty() {
|
|
collect_manifests(&manifests)
|
|
} else {
|
|
targets.iter().map(|t| PathBuf::from(t)).collect()
|
|
};
|
|
|
|
if paths.is_empty() {
|
|
eprintln!("no manifests found in {}", manifests.display());
|
|
return ExitCode::from(1);
|
|
}
|
|
|
|
let mut errors = 0u32;
|
|
for path in &paths {
|
|
match process(path, &blocks, &generated, &root, validate_only, in_place) {
|
|
Ok(out_path) => {
|
|
let name = path.file_name().unwrap_or_default().to_string_lossy();
|
|
match out_path {
|
|
Some(p) => println!("OK {name} → {}", relative_to(&p, root.parent().unwrap_or(root.as_path()))),
|
|
None => println!("OK {name}"),
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("FAIL {}: {e}", path.display());
|
|
errors += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if errors > 0 { ExitCode::from(1) } else { ExitCode::SUCCESS }
|
|
}
|
|
|
|
fn process(
|
|
path: &Path,
|
|
blocks: &Path,
|
|
generated: &Path,
|
|
root: &Path,
|
|
validate_only: bool,
|
|
in_place: bool,
|
|
) -> Result<Option<PathBuf>, String> {
|
|
let text = fs::read_to_string(path).map_err(|e| format!("read: {e}"))?;
|
|
let m: Manifest = toml::from_str(&text).map_err(|e| format!("parse: {e}"))?;
|
|
validator::validate(&m, blocks)?;
|
|
|
|
if validate_only {
|
|
return Ok(None);
|
|
}
|
|
|
|
let content = assembler::assemble(&m, blocks)?;
|
|
let out_path = if in_place {
|
|
root.join(format!("{}.md", m.name))
|
|
} else {
|
|
fs::create_dir_all(generated).map_err(|e| format!("mkdir generated: {e}"))?;
|
|
generated.join(format!("{}.md", m.name))
|
|
};
|
|
fs::write(&out_path, content).map_err(|e| format!("write {}: {e}", out_path.display()))?;
|
|
Ok(Some(out_path))
|
|
}
|
|
|
|
fn root_dir() -> PathBuf {
|
|
// Priority: AGENT_ROOT env > HOME/.claude/agents default.
|
|
// (exe-relative would break when the binary is symlinked or copied.)
|
|
if let Ok(v) = env::var("AGENT_ROOT") {
|
|
return PathBuf::from(v);
|
|
}
|
|
PathBuf::from(env::var("HOME").unwrap_or_default()).join(".claude/agents")
|
|
}
|
|
|
|
fn collect_manifests(dir: &Path) -> Vec<PathBuf> {
|
|
let mut out = Vec::new();
|
|
if let Ok(rd) = fs::read_dir(dir) {
|
|
for entry in rd.flatten() {
|
|
let p = entry.path();
|
|
if p.extension().and_then(|e| e.to_str()) == Some("toml") {
|
|
out.push(p);
|
|
}
|
|
}
|
|
}
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
fn relative_to(path: &Path, base: &Path) -> String {
|
|
path.strip_prefix(base)
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|_| path.display().to_string())
|
|
}
|