diff --git a/README.md b/README.md index c296bb5..260d945 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Thanks. ## What it is -KeiSeiKit is a comprehensive drop-in toolkit for [Claude Code](https://claude.com/claude-code). It ships a curated set of composable behavioral blocks, a Rust assembler that builds agent `.md` files from TOML manifests deterministically, 9 pre-wired PreToolUse/PostToolUse hooks (three of them dedicated to RULE 0.14 session self-audit), 38 portable skills (including an interactive `/new-agent` wizard, 10 hub-and-spoke pipelines, and the `/self-audit` retrospective skill), **23 Rust primitive crates**, 13 opt-in shell primitives (plus 3 always-copied sleep-sync helpers), and 11 cross-tool bridge templates. Everything follows a Constructor Pattern: one file per concern, manifests as single source of truth, and the generated agent files are regenerated on every relevant edit. +KeiSeiKit is a comprehensive drop-in toolkit for [Claude Code](https://claude.com/claude-code). It ships a curated set of composable behavioral blocks, a Rust assembler that builds agent `.md` files from TOML manifests deterministically, 9 pre-wired PreToolUse/PostToolUse hooks (three of them dedicated to RULE 0.14 session self-audit), 39 portable skills (including an interactive `/new-agent` wizard, 10 hub-and-spoke pipelines, and the `/self-audit` retrospective skill), **24 Rust primitive crates**, 13 opt-in shell primitives (plus 3 always-copied sleep-sync helpers), and 11 cross-tool bridge templates. Everything follows a Constructor Pattern: one file per concern, manifests as single source of truth, and the generated agent files are regenerated on every relevant edit. The kit is MIT-licensed and fully generic — install it on a fresh machine and you get a sane 12-agent fleet (implementers, critics, researchers, cost-guardians, and more — all namespaced under `kei-*` so they won't collide with your own same-named agents), a wizard for spinning up new project specialists, 10 pipeline skills that combine primitives end-to-end (`/compose-solution`, `/site-create`, `/schema-design`, `/observability-setup`, `/auth-setup`, `/api-design`, `/ci-scaffold`, `/test-matrix`, `/docs-scaffold`, `/new-project`, `/vm-provision`), and a build pipeline that keeps every agent derivable from its manifest. @@ -50,7 +50,33 @@ The kit is MIT-licensed and fully generic — install it on a fresh machine and `install.sh` checks only the deps relevant to the selected profile and soft-warns once per missing tool. -## Install +## Plugin install (v0.16+, recommended) + +If you have Claude Code 2026.04+ (plugin format supported), install via the Claude Code plugin system: + +```bash +# 1. Add this marketplace (one-time) +/plugin marketplace add KeiSei84/KeiSeiKit + +# 2. Install the plugin +/plugin install keisei@keisei-marketplace + +# Later: update or uninstall +/plugin update keisei +/plugin uninstall keisei@keisei-marketplace +``` + +The plugin layout matches the Anthropic plugin spec — all agents, skills, hooks, and the MCP server are registered automatically. No need to touch `~/.claude/settings.json` or run `install.sh`. + +**What the plugin registers:** agents in `agents/`, skills in `skills/*/SKILL.md`, hooks via `hooks/hooks.json`, MCP server via `.mcp.json`. Everything is sandboxed to the plugin install location via `${CLAUDE_PLUGIN_ROOT}` — no pollution of your personal `~/.claude/` config. + +**What the plugin does NOT install automatically:** the 24 Rust primitive crates. Those need `cargo` and are built per-profile via the classic installer below. If you want the Rust primitives (tomd, kei-ledger, provision-hetzner, etc.), fall back to the classic install path. A future plugin version may ship pre-built release binaries for common platforms. + +See [`PLUGIN.md`](./PLUGIN.md) for the full plugin layout, prerequisites, and known limitations. + +## Classic install (git clone + install.sh, still fully supported) + +For power users who want the Rust primitives, full control over the profile, or who don't have plugin-capable Claude Code yet: ```bash git clone KeiSeiKit @@ -58,6 +84,36 @@ cd KeiSeiKit ./install.sh # profile=minimal (default, no primitives) ``` +### Pre-built binaries (no Rust toolchain needed) + +From v0.16.0 on, every tagged release attaches pre-built Rust binaries, +so you can skip the local `cargo build --release` step entirely. +Download the archive for your platform from +[Releases](https://github.com/KeiSei84/KeiSeiKit/releases) and extract +it into the primitives workspace **before** running `./install.sh`: + +- `keisei-x86_64-unknown-linux-gnu.tar.gz` — Linux x86_64 +- `keisei-aarch64-unknown-linux-gnu.tar.gz` — Linux ARM64 (best-effort) +- `keisei-x86_64-apple-darwin.tar.gz` — macOS Intel +- `keisei-aarch64-apple-darwin.tar.gz` — macOS Apple Silicon + +```bash +# after cloning the kit: +mkdir -p _primitives/_rust/target/release +tar xzf keisei-.tar.gz -C _primitives/_rust/target/release +export KEI_SKIP_RUST_BUILD=1 # optional — forces skip +./install.sh --profile=full +``` + +`install.sh` auto-detects pre-built binaries in +`_primitives/_rust/target/release/` and skips the primitives +`cargo build --workspace --release` when they are present. Set +`KEI_SKIP_RUST_BUILD=1` to force-skip regardless of detection (useful on +air-gapped machines). Each release also ships a `*.sha256` checksum +file — verify with `sha256sum -c keisei-.tar.gz.sha256` before +extracting. Release notes are auto-generated from the git history by +`kei-changelog` — see [`CHANGELOG.md`](./CHANGELOG.md) for the full log. + `install.sh` is idempotent. It: 1. Creates `~/.claude/agents/{_blocks,_manifests,_primitives,_bridges,_templates,_assembler,_generated}`, `~/.claude/hooks`, `~/.claude/skills` @@ -65,9 +121,9 @@ cd KeiSeiKit 3. Copies primitives ONLY for the selected profile (default: `minimal` = none). Tracks installed set in `~/.claude/agents/_primitives/.installed`. 4. Copies generic manifests (skips if you already have a manifest with that name) 5. Builds the Rust assembler (`cargo build --release` in `_assembler/`) -6. If any Rust primitive is in the selected profile: writes a scoped workspace `Cargo.toml` listing ONLY the installed crates, then `cargo build --release` +6. If any Rust primitive is in the selected profile: writes a scoped workspace `Cargo.toml` listing ONLY the installed crates, then `cargo build --release` — **skipped when `KEI_SKIP_RUST_BUILD=1` or pre-built binaries are detected in `target/release/`** 7. Generates agent `.md` files in-place with `AGENT_ROOT=~/.claude/agents assemble --in-place` -8. Copies the 10 hooks and 38 skills +8. Copies the 9 hooks and 39 skills 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. @@ -91,7 +147,7 @@ By default `./install.sh` is **minimal** — agents + hooks + skills + bridges, | `frontend` | 8 site tools: `mock-render`, `visual-diff`, `tokens-sync`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode` | ~60s | ~80 MB | | `ops` | 8 infra tools: `kei-ledger`, `ssh-check`, `firewall-diff`, `provision-hetzner`, `provision-vultr`, `harden-base`, `metrics-scrape`, `log-ship` | ~90s | ~50 MB | | `dev` | 4 dev tools: `kei-migrate`, `kei-changelog`, `kei-ci-lint`, `kei-docs-scaffold` | ~60s | ~40 MB | -| `full` | everything (36 primitives) | ~5 min | ~200 MB | +| `full` | everything (37 primitives) | ~5 min | ~200 MB | Examples: @@ -108,22 +164,22 @@ Examples: Profile resolution lives in `_primitives/MANIFEST.toml` — one `[primitive.]` entry per primitive plus a `[profile]` block. Edit the manifest to define new profiles without touching `install.sh`. -> **Migrating from a full install:** if you're re-running `install.sh` after an earlier version that installed all primitives unconditionally, the new default (`minimal`) will REMOVE them. To preserve the old behaviour explicitly, pass `--profile=full` (currently 36 primitives). +> **Migrating from a full install:** if you're re-running `install.sh` after an earlier version that installed all primitives unconditionally, the new default (`minimal`) will REMOVE them. To preserve the old behaviour explicitly, pass `--profile=full` (currently 37 primitives). > **Re-install disclaimer:** `install.sh` is idempotent for clean state but **overwrites kit-owned `_blocks/`, `_primitives/`, `_bridges/`, `_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. ## Runtime hook controls -Every kit-shipped hook (v0.14.2+) honours two env vars so you can silence noise or isolate a failure without editing `~/.claude/settings.json`: +Every kit-shipped hook (v0.15.1+) honours two env vars so you can silence noise or isolate a failure without editing `~/.claude/settings.json`: -- `KEI_DISABLED_HOOKS` — comma- or space-list of hook base names (no `.sh`), e.g. `KEI_DISABLED_HOOKS=site-wysiwyd-check,milestone-commit-hook`. The literal `all` disables every hook. +- `KEI_DISABLED_HOOKS` — comma- or space-list of hook base names (no `.sh`), e.g. `KEI_DISABLED_HOOKS=site-wysiwyd-check,milestone-commit-hook`. Matching is **tokenized exact-match** (v0.15.1 fix — prior versions used substring-glob which let any value containing `all` disable every hook). The literal `all` token still disables every hook. - `KEI_HOOK_PROFILE` — one of `full` (default), `advisory-off`, `minimal`, `off`. | Profile | What stays on | |---|---| | `full` (default) | Every hook | | `advisory-off` | Disables pure-stderr advisories (`recurrence-suggest`, `citation-verify`, `error-spike-detector`, `milestone-commit-hook`). Safety gates stay on. | -| `minimal` | Only safety-critical: `no-github-push`, `genesis-leak-guard`, `no-hand-edit-agents`, `secrets-guard`, `assemble-validate`, `git-pre-commit-genesis`. Everything else off. | +| `minimal` | Only the four kit-shipped hooks that are either structural integrity (`no-hand-edit-agents`, `assemble-validate`) or observability-critical under RULE 0.12 / RULE 0.14 (`agent-fork-logger`, `session-end-dump`). Every other kit hook is off. User-global safety hooks such as `no-github-push`, `secrets-guard`, `genesis-leak-guard`, or `git-pre-commit-genesis` are not shipped by the kit but are respected when they are present in `~/.claude/hooks/`. | | `off` | Every hook off — escape hatch for debugging hook interactions. | ```bash @@ -143,16 +199,16 @@ Interactive wizard: run `/hooks-control` — click-only picker that shows curren | Category | Count | Examples | |---|---:|---| -| Behavioral blocks | 73 | `baseline`, `evidence-grading`, `rule-math-first`, `stack-rust-axum`, `stack-react-vite`, `stack-sveltekit`, `stack-astro`, `deploy-modal`, `api-fal-ai`, ... | +| Behavioral blocks | 77 | `baseline`, `evidence-grading`, `rule-math-first`, `stack-rust-axum`, `stack-react-vite`, `stack-sveltekit`, `stack-astro`, `deploy-modal`, `api-fal-ai`, ... | | Generic agents (manifests) | 12 | `kei-code-implementer`, `kei-critic`, `kei-validator`, `kei-security-auditor`, `kei-architect`, `kei-researcher`, `kei-ml-implementer`, `kei-cost-guardian`, `kei-modal-runner`, ... | | Hooks (PreToolUse / PostToolUse) | 9 | `assemble-agents`, `assemble-validate`, `no-hand-edit-agents`, `tomd-preread`, `agent-fork-logger`, `site-wysiwyd-check`, `session-end-dump`, `milestone-commit-hook`, `error-spike-detector` | -| Portable skills | 38 | `compose-solution`, `new-agent`, `new-project`, `site-create`, `schema-design`, `observability-setup`, `auth-setup`, `api-design`, `ci-scaffold`, `test-matrix`, `docs-scaffold`, `vm-provision`, ... | -| Primitives (Rust crates, opt-in) | 23 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync`, `kei-memory`, `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, `kei-store`, `kei-router`, `kei-sage`, `kei-task`, `kei-chat-store`, `kei-crossdomain`, `kei-search-core`, `kei-content-store`, `kei-social-store`, `kei-curator`, `kei-auth` | +| Portable skills | 39 | `compose-solution`, `new-agent`, `new-project`, `site-create`, `schema-design`, `observability-setup`, `auth-setup`, `api-design`, `ci-scaffold`, `test-matrix`, `docs-scaffold`, `vm-provision`, ... | +| Primitives (Rust crates, opt-in) | 24 | `kei-ledger`, `kei-migrate`, `kei-changelog`, `ssh-check`, `firewall-diff`, `mock-render`, `visual-diff`, `tokens-sync`, `kei-memory`, `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, `kei-store`, `kei-router`, `kei-sage`, `kei-task`, `kei-chat-store`, `kei-crossdomain`, `kei-search-core`, `kei-content-store`, `kei-social-store`, `kei-curator`, `kei-auth`, `kei-artifact` | | Primitives (shell, opt-in via profile) | 13 | `tomd`, `design-scrape`, `live-preview`, `figma-tokens`, `frontend-inspect`, `screenshot-decode`, `metrics-scrape`, `log-ship`, `provision-hetzner`, `provision-vultr`, `harden-base`, `kei-ci-lint`, `kei-docs-scaffold` | | Shell helpers (always copied) | 3 | `kei-sleep-setup`, `kei-sleep-sync`, `kei-sleep-queue` (dormant until you run `/sleep-setup`) | | Cross-tool bridges | 11 | Cursor legacy/MDC, Codex, Copilot, Windsurf, Junie, Continue, Gemini, Aider, Replit | -Of the 73 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 12 shipped manifests. The remaining blocks (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard and the hub-and-spoke pipeline skills: when you compose a project specialist or spin up a site, the wizard / pipeline picks the appropriate blocks and emits artefacts that reference them. +Of the 77 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 12 shipped manifests. The remaining blocks (`stack-*`, `deploy-*`, `api-*`, `scraper-*`, `domain-*`) are a library consumed by the `/new-agent` wizard and the hub-and-spoke pipeline skills: when you compose a project specialist or spin up a site, the wizard / pipeline picks the appropriate blocks and emits artefacts that reference them. **Cognitive mode blocks** (`_blocks/mode-*.md`) are composable behavioural skews — `mode-skeptic`, `mode-devils-advocate`, `mode-minimalist`, `mode-maximalist`, `mode-first-principles`. Add any combination to an agent's manifest `blocks = [...]` list to stack the mode. Modes compose: `mode-skeptic` + `mode-minimalist` gives you an adversarial pruner; `mode-devils-advocate` + `mode-first-principles` gives a constraint-driven steel-manner. See `_blocks/README.md` for the full list. @@ -283,7 +339,7 @@ Requires the new `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, ## Primitives (Rust) -`_primitives/_rust/` is a Cargo workspace with 23 single-binary crates (v0.13.0 added 4 deep-sleep primitives; v0.14.0 added 10 LBM-port MCP crates; v0.14.2 removed `genesis-scan` — internal-only tool, not shipped publicly). `install.sh` builds `--release` for the subset selected by the active profile and drops binaries at `~/.claude/agents/_primitives/_rust/target/release/`. +`_primitives/_rust/` is a Cargo workspace with 24 single-binary crates (v0.13.0 added 4 deep-sleep primitives; v0.14.0 added 10 LBM-port MCP crates; v0.14.2 removed `genesis-scan` — internal-only tool, not shipped publicly; v0.15.0 added `kei-artifact` for typed review-handoff). `install.sh` builds `--release` for the subset selected by the active profile and drops binaries at `~/.claude/agents/_primitives/_rust/target/release/`. | Crate | Purpose | |---|---| @@ -300,6 +356,7 @@ Requires the new `kei-conflict-scan`, `kei-refactor-engine`, `kei-graph-check`, | `kei-refactor-engine` | v0.13.0 — consumes `kei-conflict-scan` JSON; emits plan markdown + auto-resolve review markdown (NOT a unified diff; v0.14.1 retraction) | | `kei-graph-check` | v0.13.0 — post-refactor wikilink + handoff + block-ref resolver gate | | `kei-store` | v0.13.0 — memory-repo backend abstraction (GitHub / Forgejo / Gitea / Filesystem / S3) | +| `kei-artifact` | v0.15.0 — typed artifact handoff pipeline (schema-validated content pass-between agents) | ## Primitives (shell) diff --git a/_assembler/src/main.rs b/_assembler/src/main.rs index d2970fd..7b36f99 100644 --- a/_assembler/src/main.rs +++ b/_assembler/src/main.rs @@ -8,6 +8,7 @@ mod assembler; mod manifest; mod placeholders; +mod schemas_export; mod validator; use manifest::Manifest; diff --git a/_assembler/src/schemas_export.rs b/_assembler/src/schemas_export.rs new file mode 100644 index 0000000..025c726 --- /dev/null +++ b/_assembler/src/schemas_export.rs @@ -0,0 +1,136 @@ +//! Dynamic artifact-schema whitelist loader. +//! +//! v0.16: the assembler previously hardcoded the 5 builtin schema names. +//! That blocked any user who registered a custom schema via +//! `kei-artifact register-schema` — the assembler would reject manifests +//! referencing it. This cube loads the current registry from the export +//! file written by `kei-artifact export-schemas`. +//! +//! Priority (first hit wins): +//! 1. `$AGENT_ROOT/artifacts/schemas.json` (derived from `blocks_dir.parent()`) +//! 2. `~/.claude/agents/artifacts/schemas.json` +//! 3. Built-in fallback (5 names) +//! +//! Export file format: `{"schemas": ["spec", "plan", ...]}`. Builtins are +//! always unioned in, so a hand-crafted export cannot drop a core schema. +//! +//! Constructor Pattern: no dependency on serde_json — minimal hand-parser +//! keeps the assembler lean and free of transitive deps. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +/// Canonical artifact schema names shipped by `kei-artifact`. +/// +/// MIRROR OF `kei-artifact/src/schemas.rs::BUILTIN` (by design — assembler +/// crate must not link to the runtime primitive). Drift is detected by the +/// `builtin_schemas_do_not_drift` test in `validator.rs`. +pub const BUILTIN: &[&str] = &["spec", "plan", "patch", "review", "research"]; + +/// Union of builtins + any names found in an on-disk export, as a sorted set. +pub fn load(blocks_dir: &Path) -> BTreeSet { + load_with_home(blocks_dir, std::env::var("HOME").ok().as_deref()) +} + +/// Test-friendly variant that accepts an explicit HOME override. +pub fn load_with_home(blocks_dir: &Path, home: Option<&str>) -> BTreeSet { + let mut out: BTreeSet = BUILTIN.iter().map(|s| (*s).to_string()).collect(); + for path in candidate_paths(blocks_dir, home) { + if let Some(names) = read_export(&path) { + out.extend(names); + break; + } + } + out +} + +fn candidate_paths(blocks_dir: &Path, home: Option<&str>) -> Vec { + let mut v = Vec::new(); + if let Some(root) = blocks_dir.parent() { + v.push(root.join("artifacts/schemas.json")); + } + if let Some(h) = home { + v.push(PathBuf::from(h).join(".claude/agents/artifacts/schemas.json")); + } + v +} + +fn read_export(path: &Path) -> Option> { + let text = std::fs::read_to_string(path).ok()?; + parse_export(&text) +} + +/// Minimal parser for `{"schemas": ["a", "b"]}`. Tolerant of whitespace. +pub fn parse_export(text: &str) -> Option> { + let body = text.trim(); + let key = "\"schemas\""; + let i = body.find(key)?; + let rest = &body[i + key.len()..].trim_start_matches(|c: char| c == ':' || c.is_whitespace()); + let open = rest.find('[')?; + let close = rest[open..].find(']')?; + let inner = &rest[open + 1..open + close]; + let mut names = Vec::new(); + for tok in inner.split(',') { + let t = tok.trim().trim_matches('"').trim(); + if !t.is_empty() { + names.push(t.to_string()); + } + } + Some(names) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_happy_path() { + let body = r#"{"schemas": ["spec", "plan", "custom-one"]}"#; + assert_eq!(parse_export(body).unwrap(), vec!["spec", "plan", "custom-one"]); + } + + #[test] + fn parse_whitespace_and_newlines() { + let body = "{\n \"schemas\" : [\n \"a\",\n \"b\"\n ]\n}\n"; + assert_eq!(parse_export(body).unwrap(), vec!["a", "b"]); + } + + #[test] + fn parse_rejects_malformed() { + assert!(parse_export("{}").is_none()); + assert!(parse_export(r#"{"schemas":"spec"}"#).is_none()); + } + + #[test] + fn load_falls_back_to_builtin_when_no_export() { + let tmp = tempfile::tempdir().unwrap(); + let blocks_dir = tmp.path().join("_blocks"); + std::fs::create_dir_all(&blocks_dir).unwrap(); + // Isolated HOME (under tmp) — no real export file at that path. + let home = tmp.path().to_string_lossy().to_string(); + let known = load_with_home(&blocks_dir, Some(&home)); + for s in BUILTIN { + assert!(known.contains(*s)); + } + assert_eq!(known.len(), BUILTIN.len()); + } + + #[test] + fn load_unions_with_custom_export() { + let tmp = tempfile::tempdir().unwrap(); + let blocks_dir = tmp.path().join("_blocks"); + std::fs::create_dir_all(&blocks_dir).unwrap(); + let export = tmp.path().join("artifacts/schemas.json"); + std::fs::create_dir_all(export.parent().unwrap()).unwrap(); + std::fs::write( + &export, + r#"{"schemas": ["spec", "plan", "patch", "review", "research", "runbook"]}"#, + ) + .unwrap(); + let known = load_with_home(&blocks_dir, None); + assert!(known.contains("runbook")); + for s in BUILTIN { + assert!(known.contains(*s)); + } + } +} diff --git a/_assembler/src/validator.rs b/_assembler/src/validator.rs index d526f3b..7795b6e 100644 --- a/_assembler/src/validator.rs +++ b/_assembler/src/validator.rs @@ -2,19 +2,22 @@ //! Hard-fails on missing obligatory blocks, missing handoffs, unknown blocks. //! //! Detailed sub-checks live in their own cubes: -//! - `placeholders::check` — {{PLACEHOLDER}} substitution guard -//! - this file — structural checks + artifact-schema names +//! - `placeholders::check` — {{PLACEHOLDER}} substitution guard +//! - `schemas_export::load` — dynamic artifact-schema whitelist loader +//! - this file — structural checks + artifact-schema names use crate::manifest::Manifest; use crate::placeholders; +use crate::schemas_export; +use std::collections::BTreeSet; use std::path::Path; pub const OBLIGATORY: &[&str] = &["baseline", "evidence-grading", "memory-protocol"]; -/// v0.15: canonical artifact schema names shipped by `kei-artifact`. -/// Mirror of `kei_artifact::schema::KNOWN_SCHEMAS` — kept here as a string -/// literal to avoid coupling the assembler crate to the runtime primitive. -pub const KNOWN_ARTIFACT_SCHEMAS: &[&str] = &["spec", "plan", "patch", "review", "research"]; +/// Back-compat alias for external callers. The SSoT lives in +/// `schemas_export::BUILTIN`. +#[allow(dead_code)] +pub const KNOWN_ARTIFACT_SCHEMAS: &[&str] = schemas_export::BUILTIN; pub fn validate(m: &Manifest, blocks_dir: &Path) -> Result<(), String> { for required in OBLIGATORY { @@ -45,36 +48,36 @@ pub fn validate(m: &Manifest, blocks_dir: &Path) -> Result<(), String> { } placeholders::check(m)?; - check_artifact_schemas(m)?; + let known = schemas_export::load(blocks_dir); + check_artifact_schemas(m, &known)?; Ok(()) } /// v0.15: if a manifest references artifact schema names, they must be in the -/// known whitelist shipped with `kei-artifact`. Missing fields are allowed -/// (non-breaking extension). -fn check_artifact_schemas(m: &Manifest) -> Result<(), String> { +/// known whitelist. Missing fields are allowed (non-breaking extension). +fn check_artifact_schemas(m: &Manifest, known: &BTreeSet) -> Result<(), String> { if let Some(name) = &m.produces_artifact { - check_known(name, "produces_artifact")?; + check_known(name, "produces_artifact", known)?; } for (i, h) in m.handoff.iter().enumerate() { if let Some(name) = &h.expects_artifact { - check_known(name, &format!("handoff[{i}].expects_artifact"))?; + check_known(name, &format!("handoff[{i}].expects_artifact"), known)?; } if let Some(name) = &h.produces_artifact { - check_known(name, &format!("handoff[{i}].produces_artifact"))?; + check_known(name, &format!("handoff[{i}].produces_artifact"), known)?; } } Ok(()) } -fn check_known(name: &str, field: &str) -> Result<(), String> { - if KNOWN_ARTIFACT_SCHEMAS.iter().any(|s| *s == name) { +fn check_known(name: &str, field: &str, known: &BTreeSet) -> Result<(), String> { + if known.contains(name) { return Ok(()); } + let list: Vec<&str> = known.iter().map(String::as_str).collect(); Err(format!( - "unknown artifact schema '{name}' in field '{field}' — must be one of {:?}", - KNOWN_ARTIFACT_SCHEMAS + "unknown artifact schema '{name}' in field '{field}' — must be one of {list:?}" )) } @@ -107,10 +110,14 @@ mod tests { } } + fn builtin_set() -> BTreeSet { + schemas_export::BUILTIN.iter().map(|s| (*s).to_string()).collect() + } + #[test] fn artifact_schemas_absent_passes() { let m = base(); - assert!(check_artifact_schemas(&m).is_ok()); + assert!(check_artifact_schemas(&m, &builtin_set()).is_ok()); } #[test] @@ -119,14 +126,14 @@ mod tests { m.produces_artifact = Some("spec".into()); m.handoff[0].expects_artifact = Some("plan".into()); m.handoff[0].produces_artifact = Some("patch".into()); - assert!(check_artifact_schemas(&m).is_ok()); + assert!(check_artifact_schemas(&m, &builtin_set()).is_ok()); } #[test] fn artifact_schemas_reject_unknown_produces() { let mut m = base(); m.produces_artifact = Some("not-a-schema".into()); - let err = check_artifact_schemas(&m).unwrap_err(); + let err = check_artifact_schemas(&m, &builtin_set()).unwrap_err(); assert!(err.contains("not-a-schema"), "err: {err}"); assert!(err.contains("produces_artifact"), "err: {err}"); } @@ -135,8 +142,39 @@ mod tests { fn artifact_schemas_reject_unknown_expects_in_handoff() { let mut m = base(); m.handoff[0].expects_artifact = Some("zzz".into()); - let err = check_artifact_schemas(&m).unwrap_err(); + let err = check_artifact_schemas(&m, &builtin_set()).unwrap_err(); assert!(err.contains("zzz"), "err: {err}"); assert!(err.contains("handoff[0].expects_artifact"), "err: {err}"); } + + #[test] + fn builtin_schemas_do_not_drift_from_kei_artifact() { + // Structural drift test (no runtime dep on kei-artifact): read the + // primitive's source and confirm its BUILTIN list matches ours. + let primitive = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("_primitives/_rust/kei-artifact/src/schemas.rs"); + if !primitive.exists() { + eprintln!("skip drift test: primitive not at {}", primitive.display()); + return; + } + let src = std::fs::read_to_string(&primitive).unwrap(); + let mut names: Vec = Vec::new(); + for line in src.lines() { + let t = line.trim(); + if let Some(rest) = t.strip_prefix("(\"") { + if let Some(end) = rest.find("\",") { + names.push(rest[..end].to_string()); + } + } + } + let mine: Vec = schemas_export::BUILTIN + .iter() + .map(|s| (*s).to_string()) + .collect(); + assert_eq!( + names, mine, + "kei-artifact BUILTIN and schemas_export::BUILTIN drifted" + ); + } } diff --git a/_assembler/tests/mode_blocks.rs b/_assembler/tests/mode_blocks.rs new file mode 100644 index 0000000..9466d6c --- /dev/null +++ b/_assembler/tests/mode_blocks.rs @@ -0,0 +1,78 @@ +//! Mode-picker integration test. +//! +//! The `skills/new-agent` wizard Phase 3.6 appends `mode-*` block names to +//! the `blocks` array. This test locks the contract that such a manifest +//! validates cleanly AND the expected mode files ship in `_blocks/` (either +//! in the fixture set or alongside the real kit). +//! +//! We use the real `_blocks/` so the test protects the kit's mode surface — +//! if anyone renames or deletes a mode block, the wizard's Phase 3.6 +//! selection would silently break at runtime otherwise. + +use std::path::PathBuf; + +fn kit_root() -> PathBuf { + // `CARGO_MANIFEST_DIR` points at `_assembler/`; kit root is one up. + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf() +} + +#[test] +fn all_five_mode_blocks_ship_in_kit() { + let blocks = kit_root().join("_blocks"); + for mode in [ + "mode-skeptic", + "mode-devils-advocate", + "mode-minimalist", + "mode-maximalist", + "mode-first-principles", + ] { + let p = blocks.join(format!("{mode}.md")); + assert!( + p.exists(), + "mode block '{mode}' is missing from _blocks/ — Phase 3.6 of skills/new-agent would break" + ); + } +} + +#[test] +fn mode_matrix_doc_ships_in_kit() { + let p = kit_root().join("_blocks/mode-matrix.md"); + assert!( + p.exists(), + "mode-matrix.md is missing from _blocks/ — SKILL.md Phase 3.6 references it" + ); + let text = std::fs::read_to_string(&p).unwrap(); + // The matrix must enumerate each mode by block basename. + for mode in [ + "skeptic", + "devils-advocate", + "minimalist", + "maximalist", + "first-principles", + ] { + assert!( + text.contains(mode), + "mode-matrix.md is missing row for '{mode}'" + ); + } +} + +#[test] +fn skill_md_phase_3_6_wiring_exists() { + // The wizard adds mode-* blocks only if Phase 3.6 is present. + let p = kit_root().join("skills/new-agent/SKILL.md"); + assert!(p.exists(), "skills/new-agent/SKILL.md is missing"); + let text = std::fs::read_to_string(&p).unwrap(); + assert!( + text.contains("Phase 3.6"), + "SKILL.md is missing the Phase 3.6 mode picker" + ); + assert!( + text.contains("mode-skeptic") + || text.contains("skeptic — doubt-first"), + "SKILL.md Phase 3.6 does not reference the skeptic mode" + ); +} diff --git a/_blocks/README.md b/_blocks/README.md index 60a92be..a715955 100644 --- a/_blocks/README.md +++ b/_blocks/README.md @@ -27,6 +27,8 @@ Composable behavioural skews. Add any combination to a manifest's `blocks` list | `mode-maximalist.md` | Explore 10× scope; return both maximum and minimum bounds; only when user invokes exploration | | `mode-first-principles.md` | Derive from invariants; cite the physical / mathematical constraint, not "best practice" | +See `mode-matrix.md` for the **agent-role × recommended-modes** table used by the `skills/new-agent` wizard (Phase 3.6). It is the suggested starting set per role — modes remain a free pick per manifest. + ## Adding a new block 1. Pick a stable prefix (existing category or a new one documented here). diff --git a/_blocks/mode-matrix.md b/_blocks/mode-matrix.md new file mode 100644 index 0000000..fc1e3a9 --- /dev/null +++ b/_blocks/mode-matrix.md @@ -0,0 +1,24 @@ +# MODE — Agent × Cognitive-Mode Matrix + +Composable cognitive-mode blocks live in `_blocks/mode-*.md`. Any agent manifest can append them to its `blocks = [...]` list to stack the behavioural skew; modes compose (e.g. `mode-skeptic` + `mode-minimalist` = adversarial pruner). + +This table is the suggested starting set per agent role. It is a **guide, not a rule** — pick what fits the agent's actual job. + +| Agent role | Recommended modes | Reason | +|---|---|---| +| critic | `skeptic` · `devils-advocate` | Doubt-first review; name the strongest objection before agreeing | +| validator | `skeptic` | Every claim needs an E1/E2 grade — no plausibility shortcuts | +| security-auditor | `devils-advocate` · `skeptic` | Steel-man the attacker; threat-model the worst case | +| researcher | `skeptic` | Cross-check every source; honest gaps over confident guesses | +| ml-researcher | `skeptic` · `first-principles` | Paradigm-native measurement + invariant-derived priors | +| architect | `first-principles` · `minimalist` | Derive from constraints, prefer subtraction over addition | +| code-implementer | `minimalist` | Surgical edits; remove before adding | +| refactor specialist | `minimalist` | Delete dead code; prove every kept line | +| ml-implementer | `minimalist` · `first-principles` | Math-First — count params before code, derive over tune | +| brainstorm / concept-explorer | `maximalist` | Return 10× version + minimum bounds; user invokes exploration | +| physics-deriver | `first-principles` | Cite the invariant; no arguments from "best practice" | + +Intentionally **unbiased** roles (pick 0 modes by default): +- `infra-implementer`, `modal-runner`, `fal-ai-runner`, `cost-guardian`, most `kei--specialist` agents. + +Modes are not free — each one lands verbatim in the prompt and consumes context. Stack only what you need. diff --git a/_primitives/_rust/kei-artifact/src/artifact.rs b/_primitives/_rust/kei-artifact/src/artifact.rs index c5a6399..458ca97 100644 --- a/_primitives/_rust/kei-artifact/src/artifact.rs +++ b/_primitives/_rust/kei-artifact/src/artifact.rs @@ -5,7 +5,7 @@ use crate::hash::artifact_id; use crate::store::Store; -use crate::validate::validate_content; +use crate::validate::{validate_content, warn_unsupported_keywords}; use anyhow::{anyhow, Context, Result}; use chrono::Utc; use rusqlite::{params, OptionalExtension}; @@ -31,11 +31,18 @@ pub struct ArtifactFilter { } /// Insert a schema under `name`. Overwrite if present (idempotent registry). +/// +/// Non-fatal audit: unsupported JSON Schema keywords (`pattern`, `format`, +/// `oneOf`, `$ref`, etc.) are logged to stderr via +/// [`warn_unsupported_keywords`] so the operator knows they are stored but not +/// runtime-enforced. This keeps the validator surface minimal while letting +/// humans leave documentation-style keywords in place. pub fn register_schema(store: &Store, name: &str, json_schema: &str) -> Result<()> { let parsed: Value = serde_json::from_str(json_schema).context("schema is not valid JSON")?; if !parsed.is_object() { return Err(anyhow!("schema must be a JSON object")); } + warn_unsupported_keywords(&parsed); let now = Utc::now().timestamp(); store.conn().execute( "INSERT INTO schemas (name, json_schema, registered_at) VALUES (?1, ?2, ?3) diff --git a/_primitives/_rust/kei-artifact/src/cli_cmds.rs b/_primitives/_rust/kei-artifact/src/cli_cmds.rs new file mode 100644 index 0000000..980e9bd --- /dev/null +++ b/_primitives/_rust/kei-artifact/src/cli_cmds.rs @@ -0,0 +1,126 @@ +//! CLI command bodies for artifact CRUD (emit / get / list / chain). +//! +//! Constructor Pattern: one file for the read/write-artifact commands, +//! kept separate from main.rs so the binary file stays <200 LOC. +//! Each public `cmd_*` fn < 30 LOC. + +use kei_artifact::artifact::{chain, emit, get, list, ArtifactFilter}; +use kei_artifact::Store; +use std::path::Path; + +pub fn cmd_emit( + store: &Store, + schema: &str, + from: &str, + content_path: &Path, + meta: &[String], + parent: Option<&str>, +) -> anyhow::Result<()> { + let bytes = std::fs::read(content_path)?; + let meta_json = if meta.is_empty() { None } else { Some(encode_meta(meta)?) }; + let id = emit(store, schema, from, &bytes, meta_json.as_deref(), parent)?; + println!("{id}"); + Ok(()) +} + +fn encode_meta(kvs: &[String]) -> anyhow::Result { + let mut obj = serde_json::Map::new(); + for kv in kvs { + let (k, v) = kv + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("--meta expects key=value: {kv}"))?; + obj.insert(k.to_string(), serde_json::Value::String(v.to_string())); + } + Ok(serde_json::Value::Object(obj).to_string()) +} + +pub fn cmd_get(store: &Store, id: &str, format: &str) -> anyhow::Result<()> { + let a = get(store, id)?.ok_or_else(|| anyhow::anyhow!("artifact not found: {id}"))?; + match format { + "json" => print_json(&a)?, + "md" | "typed" => print_typed(&a)?, + other => return Err(anyhow::anyhow!("unknown format '{other}'")), + } + Ok(()) +} + +fn print_json(a: &kei_artifact::Artifact) -> anyhow::Result<()> { + println!("{}", serde_json::to_string_pretty(a)?); + Ok(()) +} + +fn print_typed(a: &kei_artifact::Artifact) -> anyhow::Result<()> { + let text = std::str::from_utf8(&a.content).unwrap_or(""); + println!("# artifact {} (schema={}, from={})", a.id, a.schema_name, a.source_agent); + println!("{text}"); + Ok(()) +} + +pub fn cmd_list( + store: &Store, + schema: Option<&str>, + from: Option<&str>, + since: Option<&str>, +) -> anyhow::Result<()> { + let filter = ArtifactFilter { + schema_name: schema.map(str::to_string), + source_agent: from.map(str::to_string), + since: since.and_then(parse_since), + }; + for a in list(store, &filter)? { + println!("{}\t{}\t{}\t{}", a.id, a.schema_name, a.source_agent, a.created_at); + } + Ok(()) +} + +fn parse_since(s: &str) -> Option { + // Accept raw epoch ints, or "1d" / "2h" / "30m" shorthands. + if let Ok(n) = s.parse::() { + return Some(n); + } + let (num, unit) = s.split_at(s.find(|c: char| !c.is_ascii_digit())?); + let n: i64 = num.parse().ok()?; + let secs = match unit { + "s" => n, + "m" => n * 60, + "h" => n * 3600, + "d" => n * 86400, + _ => return None, + }; + Some(chrono::Utc::now().timestamp() - secs) +} + +pub fn cmd_chain(store: &Store, id: &str) -> anyhow::Result<()> { + for a in chain(store, id)? { + println!("{}\t{}\t{}", a.id, a.schema_name, a.source_agent); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encode_meta_single_pair() { + let s = encode_meta(&["role=architect".to_string()]).unwrap(); + assert!(s.contains("\"role\"")); + assert!(s.contains("\"architect\"")); + } + + #[test] + fn encode_meta_rejects_missing_equals() { + let err = encode_meta(&["no-equals".to_string()]).unwrap_err(); + assert!(format!("{err}").contains("no-equals")); + } + + #[test] + fn parse_since_accepts_raw_epoch() { + assert_eq!(parse_since("1700000000"), Some(1700000000)); + } + + #[test] + fn parse_since_rejects_bad_unit() { + assert!(parse_since("5y").is_none()); + } +} diff --git a/_primitives/_rust/kei-artifact/src/export.rs b/_primitives/_rust/kei-artifact/src/export.rs new file mode 100644 index 0000000..fe60f46 --- /dev/null +++ b/_primitives/_rust/kei-artifact/src/export.rs @@ -0,0 +1,82 @@ +//! v0.16: schema-registry export. +//! +//! Writes the current list of registered schema names as JSON at a path the +//! assembler's manifest validator reads to accept custom-registered schemas +//! without a rebuild. +//! +//! Format: `{"schemas": ["spec", "plan", ...]}` with a trailing newline. +//! +//! Constructor Pattern: one cube, one responsibility. Tests live inline — +//! `render()` is pure, so we exercise it without a Store. + +use crate::artifact::list_schemas; +use crate::store::Store; +use anyhow::Result; +use std::path::{Path, PathBuf}; + +/// Write the current schemas registry to `override_path` or the default +/// umbrella path. Returns the number of schemas written + the final path. +pub fn write(store: &Store, override_path: Option<&Path>) -> Result<(usize, PathBuf)> { + let names = list_schemas(store)?; + let json = render(&names); + let target = override_path + .map(Path::to_path_buf) + .unwrap_or_else(default_path); + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&target, json)?; + Ok((names.len(), target)) +} + +/// Serialize `names` as `{"schemas": ["a", "b"]}\n`. +pub fn render(names: &[String]) -> String { + let quoted: Vec = names.iter().map(|n| format!("\"{n}\"")).collect(); + format!("{{\"schemas\": [{}]}}\n", quoted.join(", ")) +} + +/// `~/.claude/agents/artifacts/schemas.json` (consumed by the assembler). +pub fn default_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join(".claude/agents/artifacts/schemas.json") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_empty_list() { + assert_eq!(render(&[]), "{\"schemas\": []}\n"); + } + + #[test] + fn render_five_builtins() { + let names: Vec = + ["spec", "plan", "patch", "review", "research"].iter().map(|s| s.to_string()).collect(); + assert_eq!( + render(&names), + "{\"schemas\": [\"spec\", \"plan\", \"patch\", \"review\", \"research\"]}\n" + ); + } + + #[test] + fn write_creates_file_and_parent_dir() { + let tmp = tempfile::tempdir().unwrap(); + let store = Store::open_memory().unwrap(); + crate::schemas::register_builtins(&store).unwrap(); + crate::artifact::register_schema( + &store, + "custom", + r#"{"type":"object","additionalProperties":false,"properties":{}}"#, + ) + .unwrap(); + let target = tmp.path().join("nested/dir/schemas.json"); + let (n, path) = write(&store, Some(&target)).unwrap(); + assert_eq!(n, 6); + assert_eq!(path, target); + let body = std::fs::read_to_string(&target).unwrap(); + assert!(body.contains("custom")); + assert!(body.contains("spec")); + } +} diff --git a/_primitives/_rust/kei-artifact/src/lib.rs b/_primitives/_rust/kei-artifact/src/lib.rs index 401cb78..9f4d56a 100644 --- a/_primitives/_rust/kei-artifact/src/lib.rs +++ b/_primitives/_rust/kei-artifact/src/lib.rs @@ -7,11 +7,13 @@ //! - `schemas` — built-in schema registration (spec/plan/patch/review/research). //! - `validate` — minimal JSON Schema (strict subset of draft 2020-12). //! - `artifact` — CRUD on `artifacts` table (emit / get / list / chain). +//! - `export` — v0.16 schema-registry export for the assembler. //! //! Storage path (CLI default): `~/.claude/artifacts/artifacts.sqlite` or //! `$KEI_ARTIFACT_DB`. pub mod artifact; +pub mod export; pub mod hash; pub mod schema; pub mod schemas; @@ -21,4 +23,4 @@ pub mod validate; pub use artifact::{Artifact, ArtifactFilter}; pub use hash::artifact_id; pub use store::Store; -pub use validate::validate_content; +pub use validate::{validate_content, warn_unsupported_keywords}; diff --git a/_primitives/_rust/kei-artifact/src/main.rs b/_primitives/_rust/kei-artifact/src/main.rs index 699fdaa..e46b90d 100644 --- a/_primitives/_rust/kei-artifact/src/main.rs +++ b/_primitives/_rust/kei-artifact/src/main.rs @@ -1,9 +1,13 @@ //! kei-artifact CLI — register-schema / emit / get / list / validate / chain. //! //! Constructor Pattern: main.rs = dispatch only. Each `cmd_*` fn < 30 LOC. +//! Artifact-CRUD command bodies live in the `cli_cmds` sibling module. + +mod cli_cmds; use clap::{Parser, Subcommand}; -use kei_artifact::artifact::{chain, emit, get, list, register_schema, validate_by_id, ArtifactFilter}; +use kei_artifact::artifact::{list_schemas, register_schema, validate_by_id}; +use kei_artifact::export::write as export_write; use kei_artifact::schemas::register_builtins; use kei_artifact::Store; use std::path::PathBuf; @@ -22,11 +26,20 @@ struct Cli { enum Cmd { /// Initialise the db and register the 5 built-in schemas. Init, - /// Register a JSON Schema file under a name. + /// Register a JSON Schema file under a name. Also refreshes the export + /// consumed by the assembler (see `ExportSchemas`). RegisterSchema { #[arg(long)] name: String, #[arg(long)] path: PathBuf, }, + /// List all registered schema names, one per line (plain text). + ListSchemas, + /// v0.16: write the current schema-name list as JSON so the assembler's + /// manifest validator can accept custom-registered schemas without a + /// rebuild. Default path: `~/.claude/agents/artifacts/schemas.json`. + ExportSchemas { + #[arg(long)] path: Option, + }, /// Emit an artifact. Content file must be JSON. Emit { #[arg(long)] schema: String, @@ -73,15 +86,17 @@ fn dispatch(store: &Store, cmd: Cmd) -> anyhow::Result<()> { match cmd { Cmd::Init => cmd_init(store), Cmd::RegisterSchema { name, path } => cmd_register(store, &name, &path), + Cmd::ListSchemas => cmd_list_schemas(store), + Cmd::ExportSchemas { path } => cmd_export_schemas(store, path.as_deref()), Cmd::Emit { schema, from, content, meta, parent } => { - cmd_emit(store, &schema, &from, &content, &meta, parent.as_deref()) + cli_cmds::cmd_emit(store, &schema, &from, &content, &meta, parent.as_deref()) } - Cmd::Get { id, format } => cmd_get(store, &id, &format), + Cmd::Get { id, format } => cli_cmds::cmd_get(store, &id, &format), Cmd::List { schema, from, since } => { - cmd_list(store, schema.as_deref(), from.as_deref(), since.as_deref()) + cli_cmds::cmd_list(store, schema.as_deref(), from.as_deref(), since.as_deref()) } Cmd::Validate { id } => validate_by_id(store, &id), - Cmd::Chain { id } => cmd_chain(store, &id), + Cmd::Chain { id } => cli_cmds::cmd_chain(store, &id), } } @@ -95,95 +110,25 @@ fn cmd_register(store: &Store, name: &str, path: &std::path::Path) -> anyhow::Re let text = std::fs::read_to_string(path)?; register_schema(store, name, &text)?; println!("registered schema '{name}'"); - Ok(()) -} - -fn cmd_emit( - store: &Store, - schema: &str, - from: &str, - content_path: &std::path::Path, - meta: &[String], - parent: Option<&str>, -) -> anyhow::Result<()> { - let bytes = std::fs::read(content_path)?; - let meta_json = if meta.is_empty() { None } else { Some(encode_meta(meta)?) }; - let id = emit(store, schema, from, &bytes, meta_json.as_deref(), parent)?; - println!("{id}"); - Ok(()) -} - -fn encode_meta(kvs: &[String]) -> anyhow::Result { - let mut obj = serde_json::Map::new(); - for kv in kvs { - let (k, v) = kv - .split_once('=') - .ok_or_else(|| anyhow::anyhow!("--meta expects key=value: {kv}"))?; - obj.insert(k.to_string(), serde_json::Value::String(v.to_string())); - } - Ok(serde_json::Value::Object(obj).to_string()) -} - -fn cmd_get(store: &Store, id: &str, format: &str) -> anyhow::Result<()> { - let a = get(store, id)?.ok_or_else(|| anyhow::anyhow!("artifact not found: {id}"))?; - match format { - "json" => print_json(&a)?, - "md" | "typed" => print_typed(&a)?, - other => return Err(anyhow::anyhow!("unknown format '{other}'")), + // Best-effort auto-refresh the export so the assembler sees the new + // schema without a manual `export-schemas` call. A write failure + // (perm / missing parent) is non-fatal: register succeeded. + if let Err(e) = cmd_export_schemas(store, None) { + eprintln!("warning: export refresh failed: {e}"); } Ok(()) } -fn print_json(a: &kei_artifact::Artifact) -> anyhow::Result<()> { - println!("{}", serde_json::to_string_pretty(a)?); - Ok(()) -} - -fn print_typed(a: &kei_artifact::Artifact) -> anyhow::Result<()> { - let text = std::str::from_utf8(&a.content).unwrap_or(""); - println!("# artifact {} (schema={}, from={})", a.id, a.schema_name, a.source_agent); - println!("{text}"); - Ok(()) -} - -fn cmd_list( - store: &Store, - schema: Option<&str>, - from: Option<&str>, - since: Option<&str>, -) -> anyhow::Result<()> { - let filter = ArtifactFilter { - schema_name: schema.map(str::to_string), - source_agent: from.map(str::to_string), - since: since.and_then(parse_since), - }; - for a in list(store, &filter)? { - println!("{}\t{}\t{}\t{}", a.id, a.schema_name, a.source_agent, a.created_at); +fn cmd_list_schemas(store: &Store) -> anyhow::Result<()> { + for name in list_schemas(store)? { + println!("{name}"); } Ok(()) } -fn parse_since(s: &str) -> Option { - // Accept raw epoch ints, or "1d" / "2h" / "30m" shorthands. - if let Ok(n) = s.parse::() { - return Some(n); - } - let (num, unit) = s.split_at(s.find(|c: char| !c.is_ascii_digit())?); - let n: i64 = num.parse().ok()?; - let secs = match unit { - "s" => n, - "m" => n * 60, - "h" => n * 3600, - "d" => n * 86400, - _ => return None, - }; - Some(chrono::Utc::now().timestamp() - secs) -} - -fn cmd_chain(store: &Store, id: &str) -> anyhow::Result<()> { - for a in chain(store, id)? { - println!("{}\t{}\t{}", a.id, a.schema_name, a.source_agent); - } +fn cmd_export_schemas(store: &Store, override_path: Option<&std::path::Path>) -> anyhow::Result<()> { + let (count, target) = export_write(store, override_path)?; + println!("exported {count} schemas → {}", target.display()); Ok(()) } diff --git a/skills/new-agent/SKILL.md b/skills/new-agent/SKILL.md index df78650..4fa6b21 100644 --- a/skills/new-agent/SKILL.md +++ b/skills/new-agent/SKILL.md @@ -291,6 +291,39 @@ Store the resolved value as `FINAL_NAME`. All subsequent phases use `FINAL_NAME` --- +## Phase 3.6 — Cognitive modes (AskUserQuestion, ONE call, optional) + +Cognitive-mode blocks (`_blocks/mode-*.md`) add a behavioural skew to the generated agent. They compose — multi-selection is expected. **Default: pick NONE** if unsure; modes are not free (each lands verbatim in the prompt). + +See `_blocks/mode-matrix.md` for the recommended starting set per agent role. + +```json +{ + "questions": [ + { + "question": "Add cognitive mode blocks?", + "header": "Modes", + "multiSelect": true, + "options": [ + {"label": "skeptic — doubt-first", "description": "Ask 'what's the evidence?' on every claim. Good for critics, validators, researchers."}, + {"label": "devils-advocate — steel-man opposite", "description": "Name the strongest objection before agreeing. Good for security auditors, code reviewers."}, + {"label": "minimalist — subtract over add", "description": "Justify every addition against existing code. Good for refactor, architect, ml-implementer."}, + {"label": "maximalist — 10× version", "description": "Return both maximum and minimum bounds. Good for brainstorm / concept-exploration agents."}, + {"label": "first-principles — derive from invariants", "description": "Cite the physical / mathematical constraint, not 'best practice'. Good for architects, physicists, systems-designers."} + ] + } + ] +} +``` + +Resolve: map each selected label to its block name (`mode-skeptic`, `mode-devils-advocate`, `mode-minimalist`, `mode-maximalist`, `mode-first-principles`) and APPEND them to the `blocks` array computed in Phase 3.1 — after the stack/deploy/domain blocks, in the order the user picked them. + +If the user selected nothing — skip. The manifest is valid with zero mode blocks (the 12 existing kit manifests ship that way). + +Pre-flight still applies: Phase 3.2 existence-check must cover any mode blocks added here. + +--- + ## Phase 4 — Fill the template + write the manifest 1. Read `~/.claude/agents/_templates/specialist.toml.template` via the Read tool.