diff --git a/README.md b/README.md index 023d4ea..77f26ec 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ cd KeiSeiKit 5. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place` 6. Copies the three hooks and six skills -After install, the only remaining step is merging `settings-snippet.json` into your `~/.claude/settings.json` to activate the hooks. +After install, the only remaining step is merging `settings-snippet.json` into your `~/.claude/settings.json` to activate the hooks. You can do this automatically with `./install.sh --activate-hooks` or answer `y` at the end-of-install TTY prompt. + +> **Re-install disclaimer (v0.1.1):** `install.sh` is idempotent for clean state but **overwrites kit-owned `_blocks/`, `_templates/`, `_assembler/`, `hooks/`, and `skills/` on re-run** — local modifications under those directories are backed up to `.bak-TIMESTAMP/` (or, for shared hook files, to `.bak-TIMESTAMP`). User-owned `_manifests/*.toml` are never overwritten. ## What you get @@ -38,6 +40,8 @@ After install, the only remaining step is merging `settings-snippet.json` into y | Hooks | 3 | `assemble-agents` (PostToolUse), `assemble-validate` (PreToolUse Bash), `no-hand-edit-agents` (PreToolUse Edit/Write) | | Skills | 6 | `new-agent`, `research`, `test-gen`, `pr-review`, `refactor`, `debug-deep` | +Of the 34 blocks, the **8 base blocks** (`baseline`, `evidence-grading`, `memory-protocol`, `rule-pre-dev-gate`, `rule-test-first`, `rule-error-budget`, `rule-double-audit`, `rule-math-first`) are referenced directly by the 14 shipped manifests. The remaining **26 blocks** (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard: when you compose a project specialist, the wizard picks the appropriate stack / deploy / API / scraper / domain blocks and emits a manifest that references them. The kit's generic 14 agents do not import them by default. + ## Creating a new agent Run the wizard in Claude Code: diff --git a/_assembler/src/validator.rs b/_assembler/src/validator.rs index 5fb3c69..e59864c 100644 --- a/_assembler/src/validator.rs +++ b/_assembler/src/validator.rs @@ -34,5 +34,120 @@ pub fn validate(m: &Manifest, blocks_dir: &Path) -> Result<(), String> { return Err("role must not be empty".into()); } + check_no_placeholders(m)?; + Ok(()) } + +/// Reject manifests that still carry `{{PLACEHOLDER}}` tokens — the wizard +/// should have substituted them. Emitted literally into generated .md they +/// produce broken agents. Matches `{{...}}` conservatively (not single braces). +fn check_no_placeholders(m: &Manifest) -> Result<(), String> { + let check = |field: &str, value: &str| -> Result<(), String> { + if contains_placeholder(value) { + Err(format!( + "Unsubstituted template placeholder in field '{field}': {value}. Did the wizard skip a substitution?" + )) + } else { + Ok(()) + } + }; + + check("name", &m.name)?; + check("description", &m.description)?; + check("model", &m.model)?; + check("role", &m.role)?; + for (i, t) in m.tools.iter().enumerate() { + check(&format!("tools[{i}]"), t)?; + } + for (i, b) in m.blocks.iter().enumerate() { + check(&format!("blocks[{i}]"), b)?; + } + for (i, d) in m.domain_in.iter().enumerate() { + check(&format!("domain_in[{i}]"), d)?; + } + for (i, d) in m.forbidden_domain.iter().enumerate() { + check(&format!("forbidden_domain[{i}]"), d)?; + } + for (i, h) in m.handoff.iter().enumerate() { + check(&format!("handoff[{i}].target"), &h.target)?; + check(&format!("handoff[{i}].trigger"), &h.trigger)?; + } + for (i, o) in m.output_extra_fields.iter().enumerate() { + check(&format!("output_extra_fields[{i}]"), o)?; + } + if let Some(v) = &m.memory_project { + check("memory_project", v)?; + } + if let Some(v) = &m.project_claudemd { + check("project_claudemd", v)?; + } + if let Some(r) = &m.references { + for (i, e) in r.extra.iter().enumerate() { + check(&format!("references.extra[{i}]"), e)?; + } + } + Ok(()) +} + +fn contains_placeholder(s: &str) -> bool { + // Look for a `{{` with a matching `}}` later in the same string. + if let Some(start) = s.find("{{") { + if s[start + 2..].contains("}}") { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::manifest::{Handoff, Manifest}; + + fn base() -> Manifest { + Manifest { + name: "test".into(), + description: "d".into(), + tools: vec!["Read".into()], + model: "opus".into(), + role: "r".into(), + blocks: vec!["baseline".into(), "evidence-grading".into(), "memory-protocol".into()], + domain_in: vec!["x".into()], + forbidden_domain: vec!["y".into()], + handoff: vec![Handoff { target: "a".into(), trigger: "b".into() }], + output_extra_fields: vec![], + memory_project: None, + project_claudemd: None, + references: None, + } + } + + #[test] + fn rejects_placeholder_in_memory_project() { + let mut m = base(); + m.memory_project = Some("{{MEMORY_PROJECT}}".into()); + let err = check_no_placeholders(&m).unwrap_err(); + assert!(err.contains("memory_project"), "err = {err}"); + assert!(err.contains("{{MEMORY_PROJECT}}"), "err = {err}"); + } + + #[test] + fn accepts_single_braces() { + let mut m = base(); + m.description = "hello {world}".into(); + assert!(check_no_placeholders(&m).is_ok()); + } + + #[test] + fn accepts_empty_manifest() { + assert!(check_no_placeholders(&base()).is_ok()); + } + + #[test] + fn rejects_placeholder_in_role() { + let mut m = base(); + m.role = "do {{THING}}".into(); + assert!(check_no_placeholders(&m).is_err()); + } +} diff --git a/hooks/assemble-agents.sh b/hooks/assemble-agents.sh index ca5ed3a..ca42f7d 100755 --- a/hooks/assemble-agents.sh +++ b/hooks/assemble-agents.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PostToolUse(Edit|Write) — auto-regenerate agent .md files. # # Trigger logic: @@ -27,9 +27,15 @@ case "$FILE" in "$ASSEMBLER" --in-place "$FILE" 2>&1 | sed 's/^/[assemble-agents] /' ;; */agents/_blocks/*.md) - # Block changed → rebuild everything (block is shared) + # Block changed → rebuild everything (block is shared). + # Always surface FAIL/ERROR lines; truncate only the OK tail. echo "[assemble-agents] block changed, rebuilding all agents..." - "$ASSEMBLER" --in-place 2>&1 | sed 's/^/[assemble-agents] /' | head -40 + OUTPUT=$("$ASSEMBLER" --in-place 2>&1 || true) + FAILS=$(printf '%s\n' "$OUTPUT" | grep -E '^(FAIL|ERROR)' || true) + if [ -n "$FAILS" ]; then + printf '%s\n' "$FAILS" | sed 's/^/[assemble-agents] /' + fi + printf '%s\n' "$OUTPUT" | sed 's/^/[assemble-agents] /' | head -40 ;; *) exit 0 diff --git a/hooks/assemble-validate.sh b/hooks/assemble-validate.sh index 5e281c6..06de144 100755 --- a/hooks/assemble-validate.sh +++ b/hooks/assemble-validate.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PreToolUse(Bash) — validate all agent manifests before `git commit` in ~/.claude. # # Trigger: Bash command contains "git commit" AND current directory is under ~/.claude. diff --git a/hooks/no-hand-edit-agents.sh b/hooks/no-hand-edit-agents.sh index ce1c0cf..92b89bd 100755 --- a/hooks/no-hand-edit-agents.sh +++ b/hooks/no-hand-edit-agents.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # PreToolUse(Edit|Write) — block hand-editing generated agent .md files. # # Generated files start with: