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: