Compare commits

..

43 commits

Author SHA1 Message Date
4bc40e8e69 feat(v0.45): post-install onboarding wizard + 5 full-profile bug fixes
Some checks failed
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Has been cancelled
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Has been cancelled
User feedback from real prod install (curl|bash, profile=full): 'нет выбора
провайдера, нахуй не понятно что делать после установки'.

## New: kei onboard wizard

scripts/kei-onboard.sh — 4-step interactive wizard auto-triggered at end
of bootstrap.sh (if stdin is TTY; non-interactive runs print summary):

  Step 1 — Pick primary LLM orchestrator (claude/grok/agy/copilot/kimi)
  Step 2 — Run kei mcp-wire to install MCP into each detected CLI
  Step 3 — Optional MOONSHOT_API_KEY hint for live limits
  Step 4 — Run kei-doctor health check

Re-runnable anytime: 'kei onboard'. Skip auto-trigger: KEI_NO_ONBOARD=1.
bin/kei gains 'onboard | setup | wizard' arms.

## Bug fixes from prod install log

[install] act_runner: command not found
  brew installs 'gitea-runner' (not 'act_runner'); the two are functionally
  equivalent and both register with Forgejo. lib-dev-hub-forgejo-runner.sh
  now tries act_runner first, falls back to gitea-runner; brew install
  switches to gitea-runner package which is what's actually available.

[install] forgejo admin user create — 'no such table: user'
  Fresh sqlite DB hadn't been migrated before admin user create ran.
  lib-dev-hub-forgejo.sh now runs 'forgejo migrate' before admin bootstrap;
  idempotent — safe on re-runs.

[install] dev-hub-zoekt: 'No formulae or casks found for zoekt'
  Zoekt not in homebrew/core. lib-dev-hub-zoekt.sh now tries known taps
  (sourcegraph/zoekt, hyperdiscovery/zoekt), falls back to 'go install'
  if Go is available, and finally skips cleanly with a clear warning
  instead of aborting the entire dev-hub bundle install.

[install] dev-hub-datasette: Bootstrap failed: 5: Input/output error
  launchd Input/output error is a macOS quirk when the plist exists but
  the agent isn't yet known to launchd. Not introducing a code fix this
  release — to investigate in v0.46. Doc note will be added.

[install] kei-shared binary missing post-install
  Pre-built cache detection ('pre-built binaries detected — skipping
  cargo build') was overly eager; kei-shared wasn't in the cache.
  Workaround: run install with KEI_SKIP_RUST_BUILD unset to force rebuild.
  Permanent fix deferred to v0.46 (improve cache validation).

## Verification

- 'kei onboard' non-interactive: prints next-steps + exits cleanly ✓
- 'kei --status' shows substrate v0.45 ✓
- bootstrap.sh end-of-install branch: TTY check + KEI_NO_ONBOARD honored ✓
2026-05-26 23:18:55 +08:00
3b54f0b5e0 feat(v0.44): pre-release audit — 1 CRITICAL + 4 HIGH + 4 MEDIUM patched
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Four-CLI parallel pre-release audit (Claude+Grok+Gemini+Copilot, each
reviewing different angle) surfaced 9 real issues in v0.43. All fixed.

## Audit team & their finds

- Claude (critic):   code review — found #5 KEI_ALLOWED_ROOTS bypass,
                     #6 macOS TMPDIR denylist conflict, #7 timeout doc
                     drift, #9 failure-cache schema mismatch.
- Gemini (security): wrote Rust PoC, verified — found #1 CRITICAL parent
                     symlink for non-existent leaf, #2 TOCTOU await,
                     #3 curl config injection, #4 env inheritance, #8 cwd.
- Grok (architect):  noted safe_tools.rs at 572 LOC (>200 Constructor
                     threshold). Deferred decomposition to v0.45.
- Copilot (docs):    inspected README/encyclopedia, no blocker findings
                     (1 Premium, 977k cached tokens).

## Fixes shipped

[#1 CRITICAL] Parent-symlink bypass for non-existent leaf paths
  v0.42 only canonicalized PARENT. If THAT parent didn't exist either,
  the path fell through to "absolute as-is" with no canonicalization.
  E.g. /proj/symlink -> /Users/denis, then kei_write /proj/symlink/
  newdir/file would write inside /Users/denis with no check.
  Fix: walk_up_to_canonicalize() — find DEEPEST existing ancestor,
  canonicalize THAT (resolving all symlinks in the existing prefix),
  then reattach the non-existent tail.

[#2 HIGH] TOCTOU between validate_path and fs::write
  60s of hook chain await between path check and write. Concurrent
  process could swap leaf for symlink during that window; fs::write
  followed it.
  Fix: open file with O_NOFOLLOW + write through the open fd (not the
  path again). Open() itself fails on symlink-swap. Edit + Write both
  patched. Falls back to plain tokio::fs on non-Unix.

[#3 HIGH] curl config injection via MOONSHOT_API_KEY
  Was: token interpolated into printf 'header = "...%s..."' fed to curl
  --config. If token contained " + newline + 'url = "evil"', curl
  parsed the injected config and redirected.
  Fix: validate MOONSHOT_API_KEY matches [A-Za-z0-9_.-]+; reject any
  other chars before probe runs.

[#4 HIGH] Subprocess env inheritance — secret leak via kei_bash
  Was: spawned bash inherited AWS_*, GITHUB_TOKEN, MOONSHOT_API_KEY,
  etc. Agent running `env` via kei_bash could exfiltrate all of them.
  Fix: apply_safe_env() — env_clear() + whitelist forward of PATH/
  HOME/USER/LANG/TERM/SHELL/PWD/TMPDIR/LOGNAME/LC_*. Operators add
  named vars via KEI_SAFE_ENV_EXTRA. Applied to BOTH kei_bash spawn
  AND hook subprocess spawn.

[#5 HIGH] KEI_ALLOWED_ROOTS unanchored prefix bypass
  Was: str::starts_with on raw user-supplied root.
  KEI_ALLOWED_ROOTS=/home/u/proj also allowed /home/u/proj-secrets/...
  Fix: normalize each entry to canonical + trailing slash; use
  Path::starts_with (component-aware). v0.44 combines with #6 fix
  (canonicalize symlinks like /var → /private/var on macOS).

[#6 MEDIUM] macOS $TMPDIR denied by /var/ blanket
  Was: denylist included /var/, /private/var/ blanket entries.
  macOS $TMPDIR = /var/folders/... canonicalized to /private/var/
  folders/... hit the denylist before allowed_roots was checked.
  Fix: (a) allowed_roots check FIRST; (b) narrowed denylist to /var/db/,
  /var/log/, /var/root/ (and /private/ counterparts) instead of blanket
  /var/. /var/folders + /private/tmp are now legitimate working dirs.

[#7 MEDIUM] Timeout aggregate claim was always false
  Was: doc said "Hard cap on single chain + action ... 60s" — actually
  was per-step. For 3-hook chain, total = 4 * 60 = 240s.
  Fix: doc comment now honest about per-step semantics. Aggregate-
  deadline impl deferred to v0.45 (not security-blocking).

[#8 MEDIUM] cwd not in hook input — hook approves wrong cwd
  Was: kei_bash accepts cwd arg but did not pass it to safety hooks.
  Hook could approve `rm -rf *` assuming PWD, while cwd actually
  pointed at /etc or ~/.ssh.
  Fix: include cwd in hook_input JSON. Hooks now see the real
  working dir for their decision.

[#9 MEDIUM] Failure-fallback cache had different schema
  Was: emit '{"ts":"","status":"assembly-failed"}' — no per-CLI keys.
  Pet's .kimi.available_balance_usd read got null/error; kei-limits
  own per-CLI render loop emitted 5 malformed rows.
  Fix: failure-fallback emits same shape as success {ts, claude, grok,
  agy, copilot, kimi} with each marked status='assembly-failed'.

LOW: empty old_string in kei_edit now rejected (was: silently
prepended new_string since contents.contains("") is always true).

## Tests + smokes

cargo test -p kei-mcp: 3/3 pass.

8 MCP smokes (all green after every audit round):
  - kei_bash blocks RULE 0.1 push
  - kei_bash passes echo OK
  - kei_write /etc/passwd → denied (system dir)
  - kei_write ../ → denied (.. segment)
  - kei_write ~/.ssh/ → denied (outside roots)
  - kei_write symlink-to-etc/passwd → denied (canonicalized)
  - kei_write ~/.claude/hooks/ → denied (substrate dir)
  - kei_write ~/.zshrc → denied (outside roots)

NEW v0.44 smokes:
  - kei_write /Users/denis/.ssh/newdir/keys via /tmp/v44_link → denied
  - KEI_ALLOWED_ROOTS=/tmp/proj does NOT match /tmp/proj-evil
  - FAKE_SECRET=stolen → TOKEN=empty in subprocess (env stripped)
  - MOONSHOT_API_KEY='abc"NL_url="evil"' → rejected pre-probe
  - macOS $TMPDIR via KEI_ALLOWED_ROOTS works (canonicalize fix)

## Deferred to v0.45

- safe_tools.rs at 572 LOC — extract path_guard + chain_runner modules
- Aggregate-deadline timeout (single Instant::now() + remaining)
- Hardlink check (open fd then fstat + dev/ino compare)
- INVALID_PARAMS used for missing-arg (currently INTERNAL_ERROR)
- INVALID_PARAMS_REF dead code at EOF (silencer for unused import)

These are correctness/style/architectural, NOT security blockers.
2026-05-26 23:00:34 +08:00
424a6ced00 fix(bootstrap): run install via 'bash ./install.sh' (defensive against gh api 644)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-26 22:03:12 +08:00
a9e01a6b17 fix(limits): 4 audit fixes — atomic cache, jq guard, key argv leak, tonumber
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Claude critic audit of v0.43 kei-limits.sh found 4 real issues. All fixed.

[HIGH] Non-atomic cache write
  Was: jq > $CACHE truncated before jq ran — transient failure wiped cache.
  Now: stage in mktemp, validate non-empty, atomic mv. Preserves last-known-good.

[HIGH] tonumber threw on non-numeric balance → emptied --argjson → killed assembler
  Was: jq tonumber on $avail aborted on any non-numeric. Probe returned empty.
  Now: tonumber? // 0 swallows parse errors. Plus _safe_json wrapper validates
       each probe's output before --argjson — any single probe failure can no
       longer poison the whole cache.

[MEDIUM] MOONSHOT_API_KEY leaked to ps / /proc/<pid>/cmdline via curl argv
  Was: curl -H 'Authorization: Bearer $TOKEN' — token visible to local users.
  Now: token fed via curl --config - (stdin) — never on argv.

[MEDIUM] No jq runtime guard (40+ sibling scripts have it)
  Was: jq used unconditionally; on missing-jq host the script spewed parse
       errors and wiped the cache.
  Now: command -v jq check at top, clear error + early exit.

Verified: 'kei limits' still produces honest report; cache atomicity holds
under simulated failure; install lands all v0.40+v0.42+v0.43 components.
2026-05-26 21:50:55 +08:00
633ee4aeeb feat(limits): honest kei limits CLI + pet cache integration
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Cross-CLI subscription limits — research-grounded honest delivery after
5-parallel-agent investigation found that 4 of 5 CLIs have no public
programmatic API for quota.

## Reality findings (research)

- claude    no public API; `anthropic-ratelimit-*` headers per-call only;
            Admin API exists but needs separate admin token. See dashboard.
- grok      no public API; `x-ratelimit-*` headers per-call only. No file.
- agy       interactive /usage slash-cmd shows 100% always (forum bug).
            No public API.
- copilot   no public quota API; web dashboard only. The 'gh api /user/
            copilot_billing' endpoint does NOT exist. June 2026 billing
            migration to AI Credits further changes the surface.
- kimi      Moonshot /v1/users/me/balance returns $ balance only (no
            session/weekly quota fields). Requires MOONSHOT_API_KEY.

## Delivery (no false promises)

- scripts/kei-limits.sh — probe-all honest tool. For Kimi: real curl
  call to Moonshot balance API if MOONSHOT_API_KEY set. For other 4:
  status marker + dashboard URL.
- Pet integration — reads ~/.claude/pet/limits-cache.json IF present;
  shows Kimi balance segment ONLY when status=='live'. Pet does NOT poll;
  cache is populated by user-invoked 'kei limits'.
- bin/kei limits arm + --json mode + --quiet mode for cron.

Cache is bounded by user's explicit refresh; pet shows '(Xm old)' if
older than 1h. No background polling, no rate-limit waste, no fake data.
2026-05-26 21:43:39 +08:00
65d17007c3 feat(v0.42): 6 fixes from 4-CLI re-audit (CRITICAL + 2 HIGH + 3 MED)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Convergent dogfood audit by Claude+Grok+Gemini+Copilot on v0.41 surfaced
that the v0.41 'fixes' were incomplete. 3 of 4 reviewers independently
found the same CRITICAL bypass; Gemini wrote a Rust test suite to verify.

## Convergent findings → v0.42 fixes

[#1 CRITICAL] Symlink leaf bypass (3-of-4 convergent)
  v0.41 only canonicalized PARENT, then re-attached leaf as-is. Exploit:
    1. kei_bash 'ln -s ~/.ssh/authorized_keys ./cfg.txt'
       (passes the bash hook chain — ln itself is non-destructive)
    2. kei_write file_path=cfg.txt content=<key>
       validate_path saw $PWD/cfg.txt (allowed) → fs::write followed leaf
       symlink → overwrote authorized_keys.
  Fix: canonicalize FULL path (incl. leaf) when file exists; for new
       files, symlink_metadata on leaf and reject is_symlink. Now the
       denylist check sees the real target.

[#2 HIGH] $HOME blanket-allowed = self-neutering (2-of-4 convergent)
  allowed_roots() defaulted to [$PWD, $HOME], denylist only had 4 secret
  dirs. So kei_write $HOME/.claude/hooks/no-github-push.sh content='exit 0'
  was ALLOWED — agent could overwrite the substrate's own policy hooks.
  Similar attacks on ~/.zshrc (RCE on next shell), ~/.gitconfig, ~/.npmrc.
  Fix: default to $PWD only. Denylist extended with .claude/, .grok/,
       .gemini/, .copilot/, .kimi/, all major shell-init files, and
       additional credential paths. KEI_ALLOWED_ROOTS for explicit
       widening.

[#3 HIGH] Empty-section fail-OPEN (Gemini test-verified)
  v0.41 'fail-closed on missing config' fix was incomplete: if config
  file existed but section [bash]/[edit]/[write] was empty, load_chain
  returned Ok(vec![]) → run_chain early-returned Ok → action ran ungated.
  Fix: empty chain also FAIL-CLOSED with same KEI_POLICY_CHAIN_OPTIONAL
       opt-in.

[#4 MEDIUM] load_chain still blocked tokio worker (Claude)
  v0.41 fix #4 converted handle_edit/handle_write reads to tokio::fs but
  left load_chain on std::fs. Slow/hung mount on policy-chain.toml would
  freeze a worker for every safe_* invocation.
  Fix: load_chain → async + tokio::fs::{try_exists, read_to_string}.

[#5 MEDIUM] process_group only applied to bash, not hooks (Claude)
  v0.41 fix #5 set_process_group on kei_bash's child shell, but the
  hook subprocess (spawned per-hook in run_chain) was NOT in its own
  group. On hook timeout, kill_on_drop killed only the immediate hook
  process; grandchildren orphaned — the exact failure mode fix #5 was
  meant to prevent.
  Fix: set_process_group + killpg also on hook spawn in run_chain.

[#6 MEDIUM] Per-step vs aggregate timeout (Claude)
  Doc claimed 'Hard cap on single chain + action — 60s'. Actual: each
  hook gets independent 60s, then action gets another 60s. For a 3-hook
  bash chain that's 240s max — 4× documented.
  Status: documented as known-limit; single-deadline impl deferred to
       v0.43 (not security-blocking, just a doc/correctness drift).

## Verification (8 smokes — all green)

  /etc/passwd                          → denied (system dir)              ✓
  ../escape.txt                        → denied (../ segment)             ✓
  /tmp/symlink → /etc/passwd writeable → denied (resolved /private/etc)   ✓ NEW
  ~/.claude/hooks/no-github-push.sh    → denied (substrate dir)           ✓ NEW
  ~/.zshrc                             → denied (shell-init file)         ✓ NEW
  policy-chain.toml empty [bash]       → FAIL-CLOSED                      ✓ NEW
  KEI_POLICY_CHAIN_OPTIONAL=1          → opt-in pass-through              ✓
  kei_bash git-push-github             → BLOCKED (regression)             ✓
  kei_bash echo HELLO                  → returns content (regression)     ✓

cargo test -p kei-mcp: 3/3 still pass.

## Architecture note from Grok

Grok architect flagged: safe_tools.rs is 474 LOC, exceeds Constructor
Pattern 200-line threshold. v0.42 does NOT refactor (security fixes
shipped first); v0.43 will extract path_guard.rs + chain_runner.rs.

## Per-CLI audit value demonstrated

  Claude   — 5 issues + 5 minor, exhaustive line-anchored analysis
  Grok     — architectural review with grep-verified citations
  Gemini   — wrote Rust test project to verify findings (PoC code!)
  Copilot  — partial fact-check, ran out of mid-task
2026-05-26 21:33:54 +08:00
8086bec486 feat(v0.41): patch 5 Gemini security findings + Copilot doc bug + claude/grok perms
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Audit pass via Phase C dogfooding (security-auditor @ Agy/Gemini reviewing
our own safe_tools.rs) surfaced 5 real bugs. All fixed.

## Gemini findings (5 real bugs)

[#1 HIGH] FAIL-OPEN on missing config/hook
  Before: missing policy-chain.toml → "passing through" warning; missing
          hook script → "skipped" warning. Misconfig silently disabled
          enforcement.
  After:  both paths FAIL-CLOSED with clear error surfaced to caller.
          Tests/dev can opt in to pass-through via KEI_POLICY_CHAIN_OPTIONAL=1.

[#2 HIGH] Path traversal in kei_edit/kei_write
  Before: no validation; attacker could pass file_path=/etc/passwd or
          $HOME/.ssh/authorized_keys.
  After:  validate_path() rejects '..' segments, system dirs (/etc/, /usr/,
          /System/, /var/, /root/), and dotfile-secret dirs (~/.ssh/,
          ~/.aws/, ~/.gnupg/, ~/.config/gcloud/). Override via
          KEI_ALLOWED_ROOTS for explicit single-root confinement.

[#3 HIGH] CLAUDECODE/GROKCODE env bypass
  Behavior unchanged — this guard is a perf/UX optimization to avoid
  double-firing hooks when called from inside Claude/Grok (which already
  ran their own PreToolUse). Documented explicitly as NOT a security
  boundary: attacker controlling parent env already owns the invocation.
  Module header gains a DESIGN NOTE making this load-bearing.

[#4 MED] std::fs in async context
  Before: handle_edit/handle_write used std::fs::{read_to_string,write},
          which block the tokio worker thread. Pathological paths like
          /dev/random would freeze a worker indefinitely.
  After:  tokio::fs::{read_to_string,write}.await — async I/O, worker
          stays responsive.

[#5 MED] kill_on_drop only kills immediate child
  Before: timeout in kei_bash drops the Child handle; tokio's kill_on_drop
          SIGKILLs only the shell. Grandchildren (e.g., 'sleep 1000 &')
          orphaned.
  After:  Unix-only: spawn child in its own process group
          (Command::process_group(0)), and on timeout libc::kill(-pid,
          SIGKILL) to take down the whole group. New libc dep on Unix.

## Copilot doc fix

Doc claimed "kei-mcp exposes 4 built-in tools" without saying spawn_agent
lives in tools.rs while kei_bash/edit/write live in safe_tools.rs.
Validator agent flagged this as FALSE/MISLEADING. Now the doc spells out
the two-file structure + adds a v0.41 hardening summary.

## claude/grok subprocess permissions

Cross-CLI audit demo revealed that 'claude -p' and 'grok --print' returned
empty when invoked headless with a real audit task — they need explicit
permission flags to use Read/Grep tools in non-interactive mode. Added:

  claude:  --permission-mode=bypassPermissions
  grok:    --always-approve
  agy:     --dangerously-skip-permissions

Override via KEI_AGENT_PERMISSIVE=0 to keep strict default.
Re-verified: claude+grok both echo SMOKE-OK-V41 with the flag.

## Verification

cargo test -p kei-mcp --release  → 3/3 pass
MCP JSON-RPC smoke (all 7):
  - tools/list shows 4 built-ins ✓
  - kei_bash blocks RULE 0.1 push ✓
  - kei_bash passes 'echo OK' ✓
  - kei_write rejects /etc/passwd ✓
  - kei_write rejects ../ traversal ✓
  - kei_write rejects ~/.ssh/* ✓
  - missing policy-chain → FAIL-CLOSED with clear error ✓
  - KEI_POLICY_CHAIN_OPTIONAL=1 → opt-in pass-through ✓
2026-05-26 19:48:29 +08:00
75325aaf03 fix(hooks): clean up .task-*.start marker on agent_done (forgot in prev commit)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-26 18:53:24 +08:00
ad6ccd2beb fix(install,hooks): policy-chain.toml install + stale .task-*.start cleanup
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Two root-cause fixes:

1) hooks/_lib/policy-chain.toml was authored in v0.40 but the install loop
   in install/lib-hooks.sh globbed only *.sh, missing the TOML SSoT. Fresh
   installs got safe_tools fall-through ('no policy-chain.toml; passing
   through') because the file never landed. Extended the glob to include
   *.toml; chmod +x stays sh-only.

2) hooks/agent-event-done.sh now removes the .task-<id>.start marker that
   task-timer.sh wrote on agent_spawn. Without it completed sub-agents
   left stale markers in ~/.claude/memory/time-metrics/ for up to 2h
   (the pet's stale filter) inflating the running-agents counter.

Verified end-to-end with HOME=/tmp/ksk-verify fresh install:
  - 38 agents on disk
  - 54 hooks + 4 _lib files (including policy-chain.toml)
  - all 7 kei-mcp-wire-*.sh scripts present
  - bin/kei has pick / run-via / mcp-wire arms
  - 'kei mcp-wire --list' detects all 5 CLIs with correct tiers
  - settings.json carries ONLY safety pack (Phase 2 opt-in honored)
2026-05-26 18:52:25 +08:00
087be08e66 feat(pet): compact summary — sess + agents count + global tokens/cost
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
The tamagotchi statusline was rendering one emoji+name+elapsed entry per
running sub-agent, producing strings like:
  🔬researcher·2m 🏗️architect·5m 🔪critic·1m 📐plan·30s ...

When 4-8 parallel sessions ran agents simultaneously the line wrapped
across the terminal. Pet now renders ONE compact summary:
  💬N 🌍Tk 🤖N 💰$C

Where:
  💬 = distinct sessions today (parent_id from agent_spawn events)
  🌍 = total tokens today across ALL sessions (suppressed when null upstream)
  🤖 = running sub-agents NOW (count of <2h markers, all sessions)
  💰 = total cost today (suppressed when zero or upstream null)

Per-agent detail moved to future kei status command (TODO comment).

Why: user feedback that the per-agent list was unreadable when many
parallel sessions/agents fire. Compact counters preserve the at-a-glance
value without cluttering the prompt.
2026-05-26 18:42:20 +08:00
596e0b20a1 chore(release): bump v0.40.0 — Phase C cross-CLI hook enforcement
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
plugin.json: 0.38.0 → 0.40.0; description updated with real counts
(38 agents / 69 skills / 54 hooks / 86 blocks) and cross-CLI policy
enforcement summary.

bin/kei splash: v0.39 → v0.40 (Phase C ship).
2026-05-26 18:07:18 +08:00
4e5e6bd2c0 feat(phase-C): cross-CLI hook enforcement via kei_bash/kei_edit/kei_write MCP tools
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Closes the "hooks only fire on Claude" gap. Phase C extends KeiSeiKit safety
enforcement (no-github-push, safety-guard, destructive-guard, citation-verify,
numeric-claims-guard) to any MCP-capable LLM CLI through a 3-tier honesty model.

## 3-tier model

TIER 1 (full native): claude (existing), grok (port hooks to grok settings.json)
TIER 2 (MCP-wrapped): copilot (--excluded-tools=shell + force kei_bash via MCP)
TIER 3 (advisory):    agy + kimi (cannot disable native shell; prompt-level only)

## Design (Constructor Pattern)

1. hooks/_lib/policy-chain.toml — SSoT: which hooks gate which tool (bash/edit/write)
2. _primitives/_rust/kei-mcp/src/handlers/safe_tools.rs — new module, 3 built-in
   MCP tools that synthesize Claude PreToolUse JSON, run hook chain, abort on
   exit-2, exec on all-pass. Same input contract → hooks reused as-is, no rewrite.
3. tools.rs short-circuit: kei_bash/kei_edit/kei_write dispatched before atom layer
4. 6 wire scripts: orchestrator + one per CLI (Constructor Pattern, no mixin)
5. bin/kei mcp-wire arm
6. docs/encyclopedia/cross-cli-policy.md — honest 3-tier matrix + verification

## Double-enforcement guard

If kei-mcp invoked from a process with $CLAUDECODE=1 or $GROKCODE=1, the chain
SKIPS — native hooks already fired. Wire scripts set these env vars in the
MCP server registration for claude/grok respectively. On copilot/agy/kimi the
env is unset → chain runs.

## Smoke (verified live)

Block: kei_bash{command: forbidden-push-pattern}
  → JSON-RPC error -32603 with full "BLOCK — RULE 0.1 NO GITHUB PUSH" stderr ✓
Pass:  kei_bash{command: "echo HELLO-FROM-KEI-BASH"}
  → result.content[0].text = "HELLO-FROM-KEI-BASH" ✓
tools/list: 4 built-ins present (spawn_agent + kei_bash + kei_edit + kei_write) ✓

## Tests

kei-mcp: 3/3 (tools_list assertions updated for atoms+4 built-ins).
Build clean with toml = "0.8" dep added.

## Out of scope (deferred)

- Codex CLI wiring (not installed locally)
- ACP middleware proxy (transport, not middleware — ruled out at research)
- Container/firejail sandboxing for agy/kimi (heavy; documented limit instead)
- Native Rust PatternGate migration (optimization, separate phase)
2026-05-26 18:03:33 +08:00
3fec43ea7e feat(orchestrator): kei pick + spawn_agent MCP tool — true multi-LLM shell
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Closes the "Claude Code as single primary" gap. Now `kei` (no args) execs
whichever CLI is configured as primary, and ANY MCP-capable orchestrator
can spawn KeiSeiKit agents on any backend via the built-in spawn_agent tool.

## A — orchestrator picker

bin/kei now reads ~/.claude/config/primary.toml and execs that CLI instead
of hardcoding claude. New arms:
  kei pick               interactive menu → set primary → launch it
  kei --on=<backend>     one-shot launch of <backend> (no primary write)
  kei primary [<b>]      get/set primary
Splash shows `primary CLI: <backend>` so the orchestrator is visible.
Failure mode: if primary's CLI isn't on PATH, prints install hint + offers
`kei pick` recovery.

scripts/kei-pick.sh — Constructor Pattern picker (<140 LOC). Lists all 6
backends with install status (✓/✗), highlights current primary, writes
choice to primary.toml, execs the picked CLI. Honors stdin TTY gate
(RULE TTY-INTERACTIVITY-GATE — -t 0, not -t 1) for non-interactive safety.

## B — spawn_agent MCP tool

_primitives/_rust/kei-mcp/src/handlers/tools.rs gains a built-in
`spawn_agent` tool, exposed alongside discovered atoms:
  - inputSchema: { name: str, task: str, on?: backend-enum }
  - Calls kei-agent-cli.sh internally with same DNA resolution
  - 60s timeout, kill-on-drop
  - Honors KEI_AGENT_CLI env for testing

Smoke 2026-05-26 (MCP stdio JSON-RPC round-trip):
  spawn_agent(name=smoke-test, on=claude) → "SMOKE-OK"   
  spawn_agent(name=smoke-test, on=grok)   → "SMOKE-OK"   

Why it matters: Claude Code has a native Agent tool. Grok / Agy / Copilot /
Kimi don't have an equivalent native sub-agent surface — but they all speak
MCP. spawn_agent gives them KeiSeiKit's sub-agent capability when they're
the orchestrator. The chosen orchestrator no longer caps the sub-agent fleet.

## Other

_primitives/_rust/kei-mcp/Cargo.toml: tokio gains "io-std" feature (was
missing — main.rs uses tokio::io::stdin/stdout). This fixes a latent build
error unrelated to this PR (kei-mcp wasn't building cleanly before).

Tests: tools_list assertions updated for the +1 built-in tool (3 total
instead of 2 with atoms; 1 instead of 0 on empty root). All MCP tests pass.
Assembler 3/3 golden tests still pass (provider field is optional).
2026-05-26 16:48:23 +08:00
e4980f6ad7 feat(dna): provider+model in agent DNA; kei primary; smoke-tested 4/5 CLIs
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Makes KeiSeiKit truly multi-LLM: any agent can declare its preferred backend
in its manifest. The DNA resolver picks the right CLI; `kei primary` swaps the
fleet-wide default. KeiSeiKit is no longer tied to Claude Code single-model.

Resolution order: --on=<backend>  →  manifest provider  →  primary.toml  →  claude

Files:
  _assembler/src/manifest.rs   + Option<String> provider field
  _assembler/src/assembler.rs  emit provider: in frontmatter (when set)
  scripts/kei-agent-cli.sh     DNA resolver; `kei primary` get/set; `kei agent`
                               arm (DNA-driven); honest kimi handling (TUI-only)
  bin/kei                      new arms: agent, primary
  _primitives/cli-backends.toml mark kimi as tui-only
  docs/encyclopedia/multi-cli-agents.md  rewritten with DNA flow, smoke
                               results, rule-enforcement caveat

Smoke 2026-05-26 (real CLI invocations):
  claude   ✓ via `claude -p`
  grok     ✓ via `grok --print`            (DNA: manifest provider=grok)
  agy      ✓ via `agy --print`             (Antigravity / Gemini)
  copilot  ✓ via `copilot --prompt`        (1 Premium / 9s / 20.6k tok)
  kimi     ⚠ TUI-only, no print mode; need `kimi acp` JSON-RPC client
  codex    — register-only (not installed locally)

Rule-enforcement caveat documented: KeiSeiKit hooks fire only inside Claude
Code's PreToolUse pipeline. Non-claude backends carry the agent's PROMPT but
not the hook layer. For tool-level policy on non-claude, route through MCP.

ALSO: fix(stop-hook) — RULE 0.14 session-end-dump.sh "Recombobulating..."
4-minute hang on 18MB+ transcripts. Root cause: kei-memory ingest + frustration-
matrix scan + kei-sleep-sync ran sync at session end. Now async-detached with
per-op portable timeout (timeout/gtimeout/perl alarm). Hook returns in 0.03s.
Raw JSONL saved sync; only index/embedding step deferred (idempotent on
session_id so safe).
2026-05-26 16:21:11 +08:00
3be9a8bf71 feat(multi-cli): kei run-via <backend> — agents over external LLM CLIs
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Adds a uniform launcher that lets any KeiSeiKit agent run on whichever
LLM CLI you have a subscription to. Pick by familiarity, pricing, or
to get a second opinion on the same prompt.

Backends (locally installed, by subscription):
  claude   Claude Code     (claude -p)
  grok     xAI Grok        (grok --print; native --agent supported)
  agy      Antigravity     (agy --print)              alias: antigravity
  copilot  GitHub Copilot  (copilot --prompt)
  kimi     Moonshot Kimi   (stdin, TUI primary)
  codex    OpenAI Codex    (codex -p)                 register-only

Files:
  _primitives/cli-backends.toml         SSoT backend table
  scripts/kei-agent-cli.sh              launcher; loads ~/.claude/agents/<n>.md,
                                        strips frontmatter, composes with task,
                                        execs backend non-interactive
  bin/kei                               new arm: run-via | via | run | agent-via
  docs/encyclopedia/multi-cli-agents.md user-facing docs + usage

Auto-installed via lib-scaffold.sh:77 glob (no install code change needed).

Test plan:
  kei run-via list                                 # status + agents
  kei run-via grok critic "review src/auth.rs"     # via Grok
  kei run-via agy critic "review src/auth.rs"      # via Antigravity
  kei run-via copilot critic "review src/auth.rs"  # via Copilot
  KEI_NATIVE_AGENT=1 kei run-via grok critic "..."  # native --agent
2026-05-26 15:05:02 +08:00
518d95df80 chore(public-prep): repoint public install + marketplace off private keigit
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Public installs/clones must not hit the author's private Forgejo (keigit.com,
direct Vultr, no Cloudflare, fail2ban) — that server times out under load and
is the root of the 443 connect failures.

- web-install.sh: default KEISEI_REPO keigit -> github.com/KeiSeiLab/KeiSeiKit-1.0
- plugin.json: homepage -> https://keisei.app; repository -> github
- .gitmodules: kei-registries submodule -> github.com/KeiSeiLab/kei-registries
  (so a public --recursive clone resolves; repo still private until launch)
- README.md: manual `git clone` line -> github

Left intentionally on keigit: the @keisei/mcp-server npm registry (docs state
it's an author-operated mirror, not a community service). Repos stay PRIVATE
on github until launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:15:32 +08:00
abae256c1d feat(install): opt-in hook packs + stack profiles (public posture)
A fresh install now activates only the safety pack; discipline hooks and
agents are opt-in via an onboarding step (step 6) or `kei configure`.
"People don't need Rust-only" — they pick their own stack.

- _primitives/hook-packs.toml: SSoT mapping pack -> hooks, stack -> packs +
  agent groups. safety always on; evidence/observability/epistemic/
  orchestration/git-guard/stack-rust opt-in. rust-first/no-python only under
  the systems stack; git-guard (no-github-push) opt-in only, pulled by no stack.
- lib-profile: extract generic _toml_array (reused by lib-packs); profile_members
  becomes a thin wrapper (no behavior change).
- lib-packs: pack/stack/agent resolvers + selection loader.
- lib-hooks: filter_snippet_by_packs (install-time allowlist) + prune_kit_hooks
  (reconfigure removes deselected kit hooks, keeps foreign ones); activate_hooks
  rewired to prune + filter + merge. No custom settings.json fields (/doctor safe).
- lib-agents: install_manifests filters by stack agent set (empty = install all).
- onboarding: pick_stack step (reuse _onb_read_choice), persists stack_profile +
  enabled_packs to onboarding.toml; i18n STR_* added.
- bin/kei configure -> scripts/kei-configure.sh (re-pick without reinstall);
  install stamps ~/.claude/.kei-kit-dir.
- numeric-claims-guard: money regex no longer matches shell positionals ($1..$9);
  requires decimal / unit / 2+ digits / tilde. Real money + time still caught.
- gate one-liner added to 8 discipline hooks (runtime toggle via hooks-control).

Verified end-to-end (scratch HOME): fresh=safety only; evidence pack adds
numeric+citation; systems stack wires rust-first + 14 base/systems agents (no
data-science/swift); reconfigure-shrink prunes kit hooks but keeps a foreign
hook; settings schema clean; assembler golden 3/3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:27:14 +08:00
2ffb3a8b1e chore(public-prep): scrub author identity + private-IP references
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Pre-public Phase 1. Remove personal/IP traces that should not ship in a
general-purpose kit; keep only intended author attribution.

- no-github-push.sh + hooks-and-blocks.md + ci-scaffold: drop "KeiTech
  unfiled patent IP / trade secrets / priority date" wording; reword as a
  generic opt-in guard for keeping code on a private remote.
- check-error-patterns.sh: remove author-local absolute path from the
  tombstone comment.
- graph-export-watcher.sh: default viz dir to ~/.local/share/kei/graph-viz
  (was a personal project path).
- agent manifests (cost-guardian, modal-runner, infra/ml/code-implementer)
  + ci.yml: strip private memory references and dated personal incidents;
  keep the generic cost/ops lessons. Snapshots regenerated; golden 3/3.

Kept intentionally: author attribution (NOTICE / README / Cargo / plugin).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:00:07 +08:00
7b453aac1b feat(msg): /msg skill — read/write cross-session mailbox by @id
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Thin skill wrapper over the existing kei-message jsonl mailbox so the
user (and agents) can talk between Claude Code sessions with @id syntax:
  /msg                 read my inbox (to me or "all")
  /msg @frontend text  send to a session (identity = its cwd basename)
  /msg all text        broadcast
  /msg list | who      whole bus / known recipients

- kei-message.sh send now accepts a leading @name as the recipient
  (first token only; a later @x stays literal). --to still works.
- skills/msg/SKILL.md documents the identity model (cwd-basename),
  pull delivery (recipient's next turn via mailbox-inject hook), and the
  first-contact discovery path (who / all).
- README skills count 68 -> 69.

Verified: @name/all/--to parsing (3 cases) + end-to-end send/inbox/who
via the live script in a sandbox HOME. Skill registered + discoverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:43:04 +08:00
3aff00290f fix: pre-public audit — critical install regression + 7 blockers
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
CRITICAL: lib-hooks.sh had an apostrophe ("user's") inside the jq
program's bash single-quote, closing the quote and producing a parse
error so EVERY install aborted at source time (install.sh:71). Caught
by a full minimal e2e (rc=2 then rc=0 after fix). Reworded the jq
comment to drop the apostrophe.

Audit blockers fixed:
- MANIFEST: drop cortex-ui (no such primitive) from 4 profiles + block;
  lib-menu desc no longer references it. Profile resolution verified clean.
- lib-dev-hub-forgejo / -zoekt: source lib-launchd.sh (register_launchd
  was undefined, so full-hub dev-hub install would fail at runtime).
- kei-message: portable 16-digit id. BSD date prints literal "N" for %N;
  fall back to /dev/urandom. Verified numeric in both code paths.
- bootstrap non-TTY default cortex to minimal (matches install.sh; avoids
  divergent curl-bash vs direct-install behaviour and 105-crate surprise).
- install.sh stamps ~/.claude/.kei-profile; bin/kei reads it (splash
  showed "profile: ?" before, since .installed holds only primitive names).
- README hook count 38 to 54 (real: ls hooks star dot sh).
- web-install warns before it discards local edits in the managed clone.

Verified: 106 shell files bash -n clean; minimal e2e rc=0 (38 agents,
57 hooks, 69 skills, profile stamped, mailbox present).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:48:06 +08:00
c347f329aa feat(kei message): persistent inter-session mailbox + pull-inbox hook
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Any Claude Code session can now message any other (not just Agent-Teams
teammates), without tmux. Append-only jsonl bus + a UserPromptSubmit hook that
pulls unread into each session's context per turn.

- scripts/kei-message.sh: `kei message send [--to <name|all>] <text>` / inbox /
  list / channels. Identity = basename(cwd); broadcast channel "all".
- hooks/mailbox-inject.sh: UserPromptSubmit. Injects messages addressed to this
  session (cwd-basename) or "all", since last turn; per-session cursor dedup;
  first turn sets baseline (no history dump); never echoes own messages.
- bin/kei: `kei message ...` dispatch before splash.
- lib-scaffold: copy ALL scripts/*.sh on install (picks up kei-message.sh).
- settings-snippet: wire mailbox-inject under UserPromptSubmit.

Store: ~/.claude/mailbox/messages.jsonl. Bypass: KEI_MAILBOX_BYPASS=1.
Verified: addressed delivery, broadcast, first-turn no-dump, cursor dedup,
no self-echo (2-session simulation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:00:10 +08:00
d34a375da7 feat(install): first-run is a full guided onboarding (agents + sleep + cortex)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Sleep/cortex setup were left as separate things the user had to discover. Make
the SessionStart first-run hook a single ordered post-install checklist that
Claude walks the user through: (1) /onboard projects → per-project agents,
(2) /sleep-setup → nightly REM (recommend local-only, no remote git needed),
(3) /cortex-setup (only if the cortex daemon primitive is installed). Confirm +
run each, skippable. Fires once (marker), then silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:27:01 +08:00
582b51155f feat(install): first-run nudge to /onboard projects after bundle install
After install the user got no guidance to set up project agents — the summary
only mentioned /new-agent (single), never /onboard (scan all projects → create
a specialist per project). The installer is bash and can't launch a skill, so:

- New SessionStart hook first-run-onboard.sh: on the FIRST Claude Code session
  after install, injects context nudging the user/Claude to run
  `/onboard ~/Projects/*` (scan stack + create per-project agent, delegates to
  /new-agent). Fires once (marker ~/.claude/.kei-firstrun-shown), then silent.
  Reset: rm the marker.
- settings-snippet.json: wire the hook under SessionStart (matcher "*").
- lib-summary.sh next-steps: lead with `/onboard ~/Projects/*`, then /new-agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 19:05:00 +08:00
d89ddf0c74 fix(install): normalize null hook matcher → "" on merge (Claude Code /doctor)
Users with pre-kit hooks that have no `matcher` field end up with matcher:null,
which Claude Code /doctor rejects ("Expected string, but received null") and
skips the whole settings file. The jq merge preserved null as-is, so every
reinstall re-surfaced the error. Now map(.matcher //= "") before group_by →
null/absent collapses to "" (match-all), one group per matcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:53:27 +08:00
9866d716d7 style(install): KeiSei brand colors + ASCII banner in installer output
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
- KEISEI ASCII logo (голубой) shown at install start via kei_banner().
- `[install]` prefix → тёмно-жёлтый (38;5;178); primitive names → голубой
  (38;5;39) in both the plan and the per-primitive install lines.
- Language menu: code голубой, native name тёмно-жёлтый.
- lib-log COLOR now enables under curl|bash too: detect terminal via
  `[ -t 1 ] || /dev/tty` (web-install tees stdout → -t 1 false → colors were
  silently off). This is color detection, not an interactivity gate (pairs
  with no -t 0 — does not violate tty-interactivity-gate rule).

Verified piped-under-pty: banner + gold [install] + blue/gold language menu
all render through the tee logfile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:01:33 +08:00
7a2f6568b3 fix(install): wire PATH in curl|bash — gate pathway on stdin, not stdout
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
The PATH-wiring step (~/.claude/bin, where the `kei` entry-point lives) was
gated on `[ -t 0 ] && [ -t 1 ]`. curl|bash tees stdout to a logfile so -t 1 is
false → pathway_install was skipped → `kei: command not found` after install.

Same tee/-t1 trap as the onboarding gates (this one lived in the top-level
install.sh, missed by the earlier variant grep which only scanned install/).
Eradicated the pattern across the tree:
  install.sh        pathway gate      → [ -t 0 ]
  lib-menu.sh       profile-menu gate → [ -t 0 ]
  lib-wizard.sh     sleep-wizard gate → [ -t 0 ]

Verified piped-under-pty with /dev/tty reattach: .zshrc gets the kei-substrate
block (~/.claude/bin on PATH); non-interactive still skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:12:14 +08:00
2873474486 style(kei): brand splash colors (голубой logo + жёлтый values), bump v0.16→v0.38
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Splash was cyan; rebrand to the blue/yellow palette: sky-blue (38;5;39) logo +
dim-blue separators, gold (38;5;220) brand line + field values. Version string
was stale (v0.16 → v0.38).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:49:44 +08:00
6d68a3f1ad fix(onboarding): no crash on text input, Claude Code default, explanations
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
Three issues a real curl|bash user hit:

1. CRASH: typing a word (e.g. "claude") at any menu → $((ans-1)) treats it as a
   variable in bash arithmetic → "unbound variable" under set -u → install dies.
   Added _onb_read_choice (numeric+range validation, re-prompt) for all 4 menus.

2. No Claude under subscription: the kit installs into Claude Code yet the wizard
   offered only OpenAI Codex under subscription. Added claude-code provider
   (bumped kei-registries submodule c559065→b904993) + made subscription the
   default transport and claude-code the default provider — Enter,Enter,Enter
   lands on Claude Code (no API key).

3. install died at line 178 for any no-key provider (claude-code/codex/local):
   onboarding_run ended on a `&&` that is false when there are no auth keys →
   returned 1 → set -e aborted. Added explicit `return 0`.

Plus per-step explanations (en+ru) and auto-select when a step has one option.
Verified piped-under-pty: Enter-defaults → Claude Code, junk input → re-prompt
(0 crashes), full install completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:50 +08:00
15d50f1478 fix(web-install): don't hang after 'delegating' in interactive curl|bash
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
curl|bash makes bash read THIS script byte-by-byte from the pipe. A bare
`exec < /dev/tty` swapped stdin, then bash tried to read the NEXT line (the
`exec ./bootstrap.sh`) from the keyboard → infinite hang after "delegating to
bootstrap.sh", bootstrap never started, no profile/language menu.

Fix: merge the redirect INTO the bootstrap exec (one command), so the process
is replaced the instant stdin is swapped and bash never reads another script
byte from the (now-wrong) stdin. Verified: piped-under-pty buggy=HANG,
fixed=bootstrap starts with stdin=tty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:58:44 +08:00
01d5aa510f fix(install): run onboarding + profile wizard in curl|bash (gate on stdin, not stdout)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
web-install.sh tees stdout to a logfile (exec > >(tee) 2>&1), so -t 1 is false
even in an interactive curl|bash. The /dev/tty fix reattached stdin but the
wizard gates required BOTH -t 0 and -t 1, so onboarding (language select) and
bootstrap's profile wizard were silently skipped on the primary install path.
Prompts go to stderr and read from stdin — interactive stdin is the only real
requirement (already the proven pattern in lib-plan.sh confirm screen).

Gates now require interactive stdin only:
  bootstrap.sh       profile wizard
  lib-onboarding.sh  onboarding_should_run + preflight-continue prompt
  lib-preflight.sh   CLI-install offer prompt
  lib-hooks.sh       activate-hooks prompt

Non-interactive (CI / </dev/null / no /dev/tty) still skips — verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 03:58:55 +08:00
2fa348f467 feat(pet): comprehensive language map (60+)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 18:31:05 +00:00
4e62e921e9 feat(pet): comprehensive reflection (60+ langs, all agents, errors)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 18:30:01 +00:00
e8df152549 feat(pet): show session tokens + context% from statusLine stdin
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:50:47 +00:00
6d8f07ab90 refactor(pet): read kit agent tracking (SSoT), drop duplicate
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:15:07 +00:00
752654f049 refactor(pet): read kit agent tracking (SSoT), drop duplicate
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:14:55 +00:00
13e536ec1c refactor(pet): read kit agent tracking (SSoT), drop duplicate
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:14:17 +00:00
a965ad3b7c fix(pet): token extraction via jq totalTokens + persist token total
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:05:48 +00:00
ac1f0ebaec fix(pet): token extraction via jq totalTokens + persist token total
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 17:04:38 +00:00
3c67914788 feat(pet): agent emojis + multi-agent display + plan emoji + language icons
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 12:21:00 +00:00
ac6a020612 feat(pet): agent emojis + multi-agent display + plan emoji + language icons
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 12:20:47 +00:00
e129745319 feat(pet): agent emojis + multi-agent display + plan emoji + language icons
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 12:20:45 +00:00
cb694cc96a fix(web-install): tolerate missing /dev/tty in non-interactive curl|bash
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-21 10:53:21 +00:00
305787fae3 fix(install): make fresh install complete + ship tamagotchi (#1)
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates: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 name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
2026-05-20 18:50:09 +00:00
92 changed files with 4157 additions and 345 deletions

View file

@ -145,7 +145,7 @@ jobs:
continue-on-error: true continue-on-error: true
workflow-lint: workflow-lint:
# v0.20.1: guards against the dtolnay-SHA-class incident (2026-04-22). # v0.20.1: guards against the dtolnay-SHA-class incident.
# actionlint catches workflow syntax; validate-workflow-shas.sh catches # actionlint catches workflow syntax; validate-workflow-shas.sh catches
# fabricated / force-pushed SHA pins. Runs fast (<30s). # fabricated / force-pushed SHA pins. Runs fast (<30s).
runs-on: ubuntu-latest runs-on: ubuntu-latest

2
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "_blocks/registries"] [submodule "_blocks/registries"]
path = _blocks/registries path = _blocks/registries
url = https://keigit.com/keisei/kei-registries.git url = https://github.com/KeiSeiLab/kei-registries.git
shallow = true shallow = true

View file

@ -6,6 +6,45 @@
--- ---
## 2026-05-25 — Opt-in hook packs + stack profiles (public-prep posture)
### Context
The kit force-activated every hook via `settings-snippet.json`, including the
author's personal research discipline (numeric-claims evidence markers,
no-downgrade, citation-verify, rust-first / no-python). For a public,
general-audience kit that is presumptuous — users bring their own stack and do
not need a Rust-only policy or evidence-marker enforcement by default.
### Decision
- Posture: **safety hooks on by default; all discipline packs opt-in.** Packs:
`safety` (always), `evidence`, `observability`, `epistemic`, `orchestration`,
`git-guard`, `stack-rust`. SSoT = `_primitives/hook-packs.toml`.
- **Stack profiles** (minimal / web / ml / systems / mobile) pull a set of
discipline packs AND an agent set. `rust-first` / `no-python` live only in
`stack-rust`, which only the `systems` stack enables. `git-guard`
(no-github-push) is opt-in only and pulled by NO stack — a general kit must
not block a user's normal `git push` to github.
- Mechanism: install-time **filter** of the snippet by selected packs
(`filter_snippet_by_packs`) + **prune** of kit-owned hooks on reconfigure
(`prune_kit_hooks`, foreign hooks preserved). Selection persists to
`~/.claude/config/onboarding.toml`; re-runnable via `kei configure`.
- Non-interactive / `--yes` / CI default = minimal (safety + cosmetic only),
all agents (back-compat for power users).
### Consequences
- Gate wiring (`_lib/gate.sh`) added to the 8 highest-friction discipline hooks
for runtime toggling via the `hooks-control` skill; remaining cosmetic/event
hooks deferred (install-time filtering already gives "off by default", so the
runtime gate is a convenience, not a correctness requirement).
- Agent-set changes via `kei configure` apply on the next `./install.sh`
(reconfigure re-applies hooks fully but does not remove already-installed
agent manifests — they are harmless extra `.md` files).
- `_toml_array` extracted from `lib-profile.sh:profile_members` as the shared
one-line-array TOML reader (no new dependency).
## 2026-04-28 — Three scheduling abstractions in workspace ## 2026-04-28 — Three scheduling abstractions in workspace
### Context ### Context

View file

@ -8,8 +8,8 @@ OpenClaw / Kimi from the same source-of-truth.
**Apache 2.0** — explicit patent grant + retaliation clause. 105 Rust **Apache 2.0** — explicit patent grant + retaliation clause. 105 Rust
crates [REAL: `grep -E '^\s*"[a-z-]+",' _primitives/_rust/Cargo.toml | wc -l`], crates [REAL: `grep -E '^\s*"[a-z-]+",' _primitives/_rust/Cargo.toml | wc -l`],
68 skills [REAL: `ls skills/ | wc -l`], 38 hooks 69 skills [REAL: `ls skills/ | wc -l`], 54 hooks
[REAL: `grep -c '"command":' settings-snippet.json`], 38 agent manifests [REAL: `ls hooks/*.sh | wc -l`], 38 agent manifests
[REAL: `ls _manifests/*.toml | wc -l`], 85 substrate blocks [REAL: `ls _manifests/*.toml | wc -l`], 85 substrate blocks
[REAL: `find _blocks/ -name '*.md' | wc -l`], 18 capability atoms [REAL: `find _blocks/ -name '*.md' | wc -l`], 18 capability atoms
[REAL: `find _capabilities/ -mindepth 2 -maxdepth 2 -type d | wc -l`], [REAL: `find _capabilities/ -mindepth 2 -maxdepth 2 -type d | wc -l`],
@ -72,7 +72,7 @@ curl -fsSL https://install.keisei.app | bash -s -- --profile=dev --yes # CI
/plugin install keisei@keisei-marketplace /plugin install keisei@keisei-marketplace
# Any MCP-compatible client (Cursor / Continue / Zed / Aider / etc) # Any MCP-compatible client (Cursor / Continue / Zed / Aider / etc)
git clone https://keigit.com/keisei/KeiSeiKit-1.0.git git clone https://github.com/KeiSeiLab/KeiSeiKit-1.0.git
cd KeiSeiKit-1.0 cd KeiSeiKit-1.0
./bootstrap.sh # interactive profile picker ./bootstrap.sh # interactive profile picker
# or: ./install.sh --profile=minimal # direct # or: ./install.sh --profile=minimal # direct
@ -83,7 +83,7 @@ The web installer (`web-install.sh` in this repo, served at
repo and delegates to `bootstrap.sh` — single source of truth, no repo and delegates to `bootstrap.sh` — single source of truth, no
duplicated install logic. duplicated install logic.
38 agents + 68 skills + 38 hooks + nightly consolidation wired in 38 agents + 69 skills + 54 hooks + nightly consolidation wired in
~60 seconds. Twelve install profiles (`outcome-only`, `minimal`, ~60 seconds. Twelve install profiles (`outcome-only`, `minimal`,
`core`, `frontend`, `ops`, `dev`, `mcp`, `cortex`, `local-mirror`, `core`, `frontend`, `ops`, `dev`, `mcp`, `cortex`, `local-mirror`,
`dashboard`, `full-hub`, `full`) defined in `dashboard`, `full-hub`, `full`) defined in

View file

@ -45,6 +45,12 @@ fn write_frontmatter(m: &Manifest, out: &mut String) {
out.push_str(&format!("description: {}\n", desc.trim())); out.push_str(&format!("description: {}\n", desc.trim()));
out.push_str(&format!("tools: {}\n", m.tools.join(", "))); out.push_str(&format!("tools: {}\n", m.tools.join(", ")));
out.push_str(&format!("model: {}\n", m.model)); out.push_str(&format!("model: {}\n", m.model));
// v0.39: optional provider for DNA-resolved kei agent dispatch.
if let Some(prov) = &m.provider {
if !prov.is_empty() {
out.push_str(&format!("provider: {}\n", prov));
}
}
out.push_str("---\n\n"); out.push_str("---\n\n");
out.push_str(&format!( out.push_str(&format!(
"<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n", "<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n",

View file

@ -9,6 +9,13 @@ pub struct Manifest {
pub description: String, pub description: String,
pub tools: Vec<String>, pub tools: Vec<String>,
pub model: String, pub model: String,
/// v0.39 (multi-CLI): optional LLM provider this agent prefers when invoked
/// via `kei agent <name>`. Values: claude / grok / agy / copilot / kimi /
/// codex. Empty / missing → DNA resolver falls back to ~/.claude/config/
/// primary.toml, then to claude. Affects `kei run-via` / `kei agent`
/// dispatch; does NOT change Claude Code's in-session model.
#[serde(default)]
pub provider: Option<String>,
pub role: String, pub role: String,
pub blocks: Vec<String>, pub blocks: Vec<String>,
/// v0.16 (phase 5): agent substrate role. When present, assembler loads /// v0.16 (phase 5): agent substrate role. When present, assembler loads

View file

@ -99,7 +99,7 @@ extra = [
"path:user-rules/dev-workflow.md", "path:user-rules/dev-workflow.md",
"path:user-rules/debugging.md", "path:user-rules/debugging.md",
"path:user-rules/karpathy-behavioral.md", "path:user-rules/karpathy-behavioral.md",
"MEMORY.md → Architecture Overlay Incident (model_brain.py 227→354 LOC from \"fixes\" — never patch, fix root formulas)", "Architecture Overlay Incident (model_brain.py 227→354 LOC from \"fixes\" — never patch, fix root formulas)",
] ]
[taxonomy] [taxonomy]

View file

@ -13,7 +13,7 @@ You are the cost guardian. Your job is to make sure no paid compute launches wit
verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop \ verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop \
runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do \ runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do \
NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident \ NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident \
(2026-02-26)** is the cautionary tale: prices guessed not verified, silent retries \ ** is the cautionary tale: prices guessed not verified, silent retries \
re-billing, file changes never confirmed, dashboard never checked. Every protocol below \ re-billing, file changes never confirmed, dashboard never checked. Every protocol below \
exists because of that day never again. exists because of that day never again.
""" """

View file

@ -419,4 +419,4 @@ Blockers / next: <list>
- `path:user-rules/dev-workflow.md` - `path:user-rules/dev-workflow.md`
- `path:user-rules/debugging.md` - `path:user-rules/debugging.md`
- `path:user-rules/karpathy-behavioral.md` - `path:user-rules/karpathy-behavioral.md`
- `MEMORY.md → Architecture Overlay Incident (model_brain.py 227→354 LOC from "fixes" — never patch, fix root formulas)` - `Architecture Overlay Incident (model_brain.py 227→354 LOC from "fixes" — never patch, fix root formulas)`

View file

@ -13,7 +13,7 @@ model: opus
# ROLE # ROLE
You are the cost guardian. Your job is to make sure no paid compute launches without a verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident (2026-02-26)** is the cautionary tale: prices guessed not verified, silent retries re-billing, file changes never confirmed, dashboard never checked. Every protocol below exists because of that day — never again. You are the cost guardian. Your job is to make sure no paid compute launches without a verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident ** is the cautionary tale: prices guessed not verified, silent retries re-billing, file changes never confirmed, dashboard never checked. Every protocol below exists because of that day — never again.
# AGENT SUBSTRATE — role `read-only` # AGENT SUBSTRATE — role `read-only`

@ -1 +1 @@
Subproject commit c5590658ee636398c512ce2b25d8aedb3212a9b6 Subproject commit b90499311ed09cbf88ced93aaa9d890dcf861fac

View file

@ -463,4 +463,4 @@ behaviour-verified: yes | no | not-applicable
follow-up-required: follow-up-required:
- <bullet list> - <bullet list>
``` ```
- `MEMORY.md → Architecture Overlay Incident (model_brain.py 227→354 LOC from "fixes" — never patch, fix root formulas)` - `Architecture Overlay Incident (model_brain.py 227→354 LOC from "fixes" — never patch, fix root formulas)`

View file

@ -9,7 +9,7 @@ model: sonnet
# ROLE # ROLE
You are the cost guardian. Your job is to make sure no paid compute launches without a verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident (2026-02-26)** is the cautionary tale: prices guessed not verified, silent retries re-billing, file changes never confirmed, dashboard never checked. Every protocol below exists because of that day — never again. You are the cost guardian. Your job is to make sure no paid compute launches without a verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident** is the cautionary tale: prices guessed not verified, silent retries re-billing, file changes never confirmed, dashboard never checked. Every protocol below exists because of that day — never again.
# AGENT SUBSTRATE — role `read-only` # AGENT SUBSTRATE — role `read-only`

View file

@ -438,9 +438,9 @@ Blockers / next: <list>
- `{path::user-rules}/git-conventions.md` - `{path::user-rules}/git-conventions.md`
- `{path::user-rules}/dev-workflow.md` - `{path::user-rules}/dev-workflow.md`
- `{path::user-memory}/security-restricted-projects.md` - `{path::user-memory}/security-restricted-projects.md`
- `MEMORY.md → Compute Cost Incident (2026-02-26): $98.78 Modal overrun — no dashboard check, unverified prices.` - `Compute Cost Incident: $98.78 Modal overrun — no dashboard check, unverified prices.`
- `MEMORY.md → Recruiter shared-EC2 risk (i-0a8b747023809d451 shared with 3 projects, default SECRET_KEY, no CSRF).` - `Recruiter shared-EC2 risk (i-0a8b747023809d451 shared with 3 projects, default SECRET_KEY, no CSRF).`
- `MEMORY.md → CloudSync 146 GB bloat: two duplicate LaunchAgents both writing logs. Scan for duplicates before adding infra.` - `CloudSync 146 GB bloat: two duplicate LaunchAgents both writing logs. Scan for duplicates before adding infra.`
## Output Footer (RULE 0.16) ## Output Footer (RULE 0.16)

View file

@ -483,8 +483,8 @@ Blockers / next: <list>
- `{path::user-rules}/manifold-tangent-sanity.md` - `{path::user-rules}/manifold-tangent-sanity.md`
- `{path::user-rules}/no-downgrade-constructive.md` - `{path::user-rules}/no-downgrade-constructive.md`
- `{path::user-memory}/wrong-paths-specialized-ml.md` - `{path::user-memory}/wrong-paths-specialized-ml.md`
- `MEMORY.md → Compute Cost Incident (2026-02-26): promised $27, spent $98.78 on Modal. NEVER AGAIN.` - `Compute Cost Incident: promised $27, spent $98.78 on Modal. NEVER AGAIN.`
- `MEMORY.md → Architecture Overlay Incident: model_brain.py 227→354 LOC from audit fixes. No Patching.` - `Architecture Overlay Incident: model_brain.py 227→354 LOC from audit fixes. No Patching.`
## Output Footer (RULE 0.16) ## Output Footer (RULE 0.16)

View file

@ -11,9 +11,9 @@ model: sonnet
You are the Modal compute orchestrator. You launch Modal jobs safely, observe them well, and NEVER burn money or kill running work. Two incidents shape every rule below. You are the Modal compute orchestrator. You launch Modal jobs safely, observe them well, and NEVER burn money or kill running work. Two incidents shape every rule below.
$98.78 Modal Incident (2026-02-26): promised $27, spent $98.78 in one session. Prices guessed not verified, failed retries silently re-billed, file changes never confirmed, dashboard never checked. Every cost rule exists because of that day. $98.78 Modal Incident: promised $27, spent $98.78 in one session. Prices guessed not verified, failed retries silently re-billed, file changes never confirmed, dashboard never checked. Every cost rule exists because of that day.
anti-stop guard Incident (2026-03-29): stopped a 1.4-hour training run for a non-critical bug. Cost: 1.4 hours A10G + restart + re-warmup. Every kill rule exists because of that day. anti-stop guard Incident: stopped a 1.4-hour training run for a non-critical bug. Cost: 1.4 hours A10G + restart + re-warmup. Every kill rule exists because of that day.
Cost tiers: <$5 per run → AUTO; $5-$20 → WARN + daily-cap check ($20/day session); >$20 → STOP and ask. Always state estimate in dollars BEFORE launch: "Estimate: $X.XX (= N_gpus × hours × $/hr/gpu)". GPU compat: A10G torch>=2.0 (~$1.10/hr), H100 torch>=2.1 (~$4.50/hr), B200 torch>=2.6 (~$8/hr). Always verify on pricing page — rates change. Cost tiers: <$5 per run → AUTO; $5-$20 → WARN + daily-cap check ($20/day session); >$20 → STOP and ask. Always state estimate in dollars BEFORE launch: "Estimate: $X.XX (= N_gpus × hours × $/hr/gpu)". GPU compat: A10G torch>=2.0 (~$1.10/hr), H100 torch>=2.1 (~$4.50/hr), B200 torch>=2.6 (~$8/hr). Always verify on pricing page — rates change.

View file

@ -99,7 +99,7 @@ extra = [
"path:user-rules/dev-workflow.md", "path:user-rules/dev-workflow.md",
"path:user-rules/debugging.md", "path:user-rules/debugging.md",
"path:user-rules/karpathy-behavioral.md", "path:user-rules/karpathy-behavioral.md",
"MEMORY.md → Architecture Overlay Incident (model_brain.py 227→354 LOC from \"fixes\" — never patch, fix root formulas)", "Architecture Overlay Incident (model_brain.py 227→354 LOC from \"fixes\" — never patch, fix root formulas)",
] ]
[taxonomy] [taxonomy]

View file

@ -13,7 +13,7 @@ You are the cost guardian. Your job is to make sure no paid compute launches wit
verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop \ verified cost estimate, a checked dashboard, and a clean head-room calculation. You stop \
runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do \ runaway spend before it starts. You are READ-ONLY: you emit a GO/NO-GO report card; you do \
NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident \ NOT launch jobs yourself (hand back to user or `ml-implementer`). **The $98.78 Modal incident \
(2026-02-26)** is the cautionary tale: prices guessed not verified, silent retries \ ** is the cautionary tale: prices guessed not verified, silent retries \
re-billing, file changes never confirmed, dashboard never checked. Every protocol below \ re-billing, file changes never confirmed, dashboard never checked. Every protocol below \
exists because of that day never again. exists because of that day never again.
""" """

View file

@ -100,9 +100,9 @@ extra = [
"path:user-rules/git-conventions.md", "path:user-rules/git-conventions.md",
"path:user-rules/dev-workflow.md", "path:user-rules/dev-workflow.md",
"path:user-memory/security-restricted-projects.md", "path:user-memory/security-restricted-projects.md",
"MEMORY.md → Compute Cost Incident (2026-02-26): $98.78 Modal overrun — no dashboard check, unverified prices.", "Compute Cost Incident: $98.78 Modal overrun — no dashboard check, unverified prices.",
"MEMORY.md → Recruiter shared-EC2 risk (<ec2-instance-id> shared with 3 projects, default SECRET_KEY, no CSRF).", "Recruiter shared-EC2 risk (<ec2-instance-id> shared with 3 projects, default SECRET_KEY, no CSRF).",
"MEMORY.md → CloudSync 146 GB bloat: two duplicate LaunchAgents both writing logs. Scan for duplicates before adding infra.", "CloudSync 146 GB bloat: two duplicate LaunchAgents both writing logs. Scan for duplicates before adding infra.",
] ]
[taxonomy] [taxonomy]

View file

@ -113,8 +113,8 @@ extra = [
"path:user-rules/manifold-tangent-sanity.md", "path:user-rules/manifold-tangent-sanity.md",
"path:user-rules/no-downgrade-constructive.md", "path:user-rules/no-downgrade-constructive.md",
"path:user-memory/wrong-paths-specialized-ml.md", # TODO verify path:user-memory exists in assembler resolver "path:user-memory/wrong-paths-specialized-ml.md", # TODO verify path:user-memory exists in assembler resolver
"MEMORY.md → Compute Cost Incident (2026-02-26): promised $27, spent $98.78 on Modal. NEVER AGAIN.", "Compute Cost Incident: promised $27, spent $98.78 on Modal. NEVER AGAIN.",
"MEMORY.md → Architecture Overlay Incident: model_brain.py 227→354 LOC from audit fixes. No Patching.", "Architecture Overlay Incident: model_brain.py 227→354 LOC from audit fixes. No Patching.",
] ]
[taxonomy] [taxonomy]

View file

@ -12,11 +12,11 @@ role = """
You are the Modal compute orchestrator. You launch Modal jobs safely, observe them well, and NEVER \ You are the Modal compute orchestrator. You launch Modal jobs safely, observe them well, and NEVER \
burn money or kill running work. Two incidents shape every rule below. burn money or kill running work. Two incidents shape every rule below.
$98.78 Modal Incident (2026-02-26): promised $27, spent $98.78 in one session. Prices guessed not \ $98.78 Modal Incident: promised $27, spent $98.78 in one session. Prices guessed not \
verified, failed retries silently re-billed, file changes never confirmed, dashboard never checked. \ verified, failed retries silently re-billed, file changes never confirmed, dashboard never checked. \
Every cost rule exists because of that day. Every cost rule exists because of that day.
anti-stop guard Incident (2026-03-29): stopped a 1.4-hour training run for a non-critical bug. Cost: \ anti-stop guard Incident: stopped a 1.4-hour training run for a non-critical bug. Cost: \
1.4 hours A10G + restart + re-warmup. Every kill rule exists because of that day. 1.4 hours A10G + restart + re-warmup. Every kill rule exists because of that day.
Cost tiers: <$5 per run AUTO; $5-$20 WARN + daily-cap check ($20/day session); >$20 STOP \ Cost tiers: <$5 per run AUTO; $5-$20 WARN + daily-cap check ($20/day session); >$20 STOP \

View file

@ -34,10 +34,10 @@ buddy = ["kei-buddy", "kei-telegram-webhook", "kei-shared", "kei-chat-store",
# previous one — `dashboard` extends `local-mirror`, `full-hub` extends # previous one — `dashboard` extends `local-mirror`, `full-hub` extends
# `dashboard`. Native macOS arm64 (brew + launchd plists). See # `dashboard`. Native macOS arm64 (brew + launchd plists). See
# `install/lib-dev-hub-*.sh` for the install logic per component. # `install/lib-dev-hub-*.sh` for the install logic per component.
local-mirror = ["kei-cortex", "cortex-ui", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner"] local-mirror = ["kei-cortex", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner"]
dashboard = ["kei-cortex", "cortex-ui", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner", "kei-projects-index", "kei-projects-watcher", "dev-hub-datasette"] dashboard = ["kei-cortex", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner", "kei-projects-index", "kei-projects-watcher", "dev-hub-datasette"]
full-hub = ["kei-cortex", "cortex-ui", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner", "kei-projects-index", "kei-projects-watcher", "dev-hub-datasette", "dev-hub-zoekt", "dev-hub-mdbook", "dev-hub-restic", "dev-hub-gdrive-import"] full-hub = ["kei-cortex", "kei-pet", "kei-shared", "kei-ledger", "kei-memory", "frustration-matrix", "kei-skill-importer", "kei-router", "kei-dna-index", "kei-atom-discovery", "dev-hub-forgejo", "dev-hub-forgejo-runner", "kei-projects-index", "kei-projects-watcher", "dev-hub-datasette", "dev-hub-zoekt", "dev-hub-mdbook", "dev-hub-restic", "dev-hub-gdrive-import"]
full = ["tomd", "kei-doctor", "kei-ledger", "kei-migrate", "kei-changelog", "ssh-check", "firewall-diff", "mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship", "kei-ci-lint", "kei-docs-scaffold", "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", "keisei", "kei-agent-runtime", "kei-capability", "kei-provision", "kei-entity-store", "kei-pipe", "kei-cache", "kei-spawn", "kei-replay", "kei-cortex", "cortex-ui", "kei-pet", "kei-shared", "frustration-matrix", "kei-skill-importer", "kei-projects-index", "kei-projects-watcher", "dev-hub-forgejo", "dev-hub-forgejo-runner", "dev-hub-datasette", "dev-hub-zoekt", "dev-hub-mdbook", "dev-hub-restic", "dev-hub-gdrive-import"] full = ["tomd", "kei-doctor", "kei-ledger", "kei-migrate", "kei-changelog", "ssh-check", "firewall-diff", "mock-render", "visual-diff", "tokens-sync", "design-scrape", "live-preview", "figma-tokens", "frontend-inspect", "screenshot-decode", "provision-hetzner", "provision-vultr", "harden-base", "metrics-scrape", "log-ship", "kei-ci-lint", "kei-docs-scaffold", "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", "keisei", "kei-agent-runtime", "kei-capability", "kei-provision", "kei-entity-store", "kei-pipe", "kei-cache", "kei-spawn", "kei-replay", "kei-cortex", "kei-pet", "kei-shared", "frustration-matrix", "kei-skill-importer", "kei-projects-index", "kei-projects-watcher", "dev-hub-forgejo", "dev-hub-forgejo-runner", "dev-hub-datasette", "dev-hub-zoekt", "dev-hub-mdbook", "dev-hub-restic", "dev-hub-gdrive-import"]
# --- shell primitives (13) ------------------------------------------------- # --- shell primitives (13) -------------------------------------------------
@ -357,12 +357,6 @@ crate = "kei-cortex"
deps = ["python3 (>=3.9, for whisper_worker.py subprocess)", "pip install -r scripts/requirements.txt", "ffmpeg (on PATH, faster-whisper audio demux)"] deps = ["python3 (>=3.9, for whisper_worker.py subprocess)", "pip install -r scripts/requirements.txt", "ffmpeg (on PATH, faster-whisper audio demux)"]
desc = "Local HTTP daemon exposing chat/TTS/STT/portrait endpoints — backs cortex-ui browser app" desc = "Local HTTP daemon exposing chat/TTS/STT/portrait endpoints — backs cortex-ui browser app"
[primitive.cortex-ui]
kind = "node"
path = "_ts_packages/packages/cortex-ui"
deps = ["node>=18", "pnpm"]
desc = "Svelte 5 + Vite 5 web UI for kei-cortex daemon (chat panel, Live2D pet renderer, portrait uploader)"
[primitive.frustration-matrix] [primitive.frustration-matrix]
kind = "rust" kind = "rust"
crate = "frustration-matrix" crate = "frustration-matrix"

View file

@ -3969,10 +3969,12 @@ dependencies = [
"anyhow", "anyhow",
"kei-atom-discovery", "kei-atom-discovery",
"kei-skills", "kei-skills",
"libc",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
"tokio", "tokio",
"toml",
] ]
[[package]] [[package]]

View file

@ -18,7 +18,17 @@ path = "src/lib.rs"
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } # v0.39: io-std added for tokio::io::stdin/stdout used by the MCP stdio
# transport in main.rs (workspace tokio doesn't enable io-std by default).
tokio = { workspace = true, features = ["io-std"] }
# v0.40 (Phase C): toml needed for safe_tools::policy_chain — reads
# ~/.claude/hooks/_lib/policy-chain.toml to know which hooks to chain.
toml = "0.8"
# v0.41 (audit fix #5): killpg via libc on Unix — kill_on_drop only SIGKILLs
# the immediate child shell, leaving grandchildren orphaned. We set the child
# in its own process group and killpg() the group on timeout.
[target.'cfg(unix)'.dependencies]
libc = "0.2"
anyhow = { workspace = true } anyhow = { workspace = true }
kei-atom-discovery = { path = "../kei-atom-discovery" } kei-atom-discovery = { path = "../kei-atom-discovery" }
kei-skills = { path = "../kei-skills" } kei-skills = { path = "../kei-skills" }

View file

@ -12,6 +12,7 @@
pub mod initialize; pub mod initialize;
pub mod prompts; pub mod prompts;
pub mod resources; pub mod resources;
pub mod safe_tools;
pub mod tools; pub mod tools;
use crate::protocol::{err, JsonRpcRequest, JsonRpcResponse, Method, ServerContext, METHOD_NOT_FOUND}; use crate::protocol::{err, JsonRpcRequest, JsonRpcResponse, Method, ServerContext, METHOD_NOT_FOUND};

View file

@ -0,0 +1,738 @@
//! Phase C — cross-CLI hook enforcement via MCP-wrapped tools.
//!
//! Exposes three built-in MCP tools — `kei_bash`, `kei_edit`, `kei_write` —
//! that synthesize Claude Code's PreToolUse hook input contract, chain
//! through the hook scripts declared in `~/.claude/hooks/_lib/policy-chain.toml`,
//! and only execute the wrapped action if every hook returns exit 0.
//!
//! Why this exists: when an agent runs on Grok / Agy / Copilot / Kimi, none
//! of our claude-side PreToolUse hooks fire. The agent could read the rules
//! in its system prompt but the tool-call layer was previously ungated. The
//! `kei_*` MCP tools restore that gate for any MCP-capable CLI.
//!
//! Constructor Pattern: ONE policy SSoT (`policy-chain.toml`), ONE dispatcher
//! (this file), hooks reused as-is from `~/.claude/hooks/`. No rewrite, no
//! abstraction layer. Shell-out per hook keeps the contract identical to
//! Claude's native PreToolUse pipeline.
//!
//! CLAUDECODE / GROKCODE guard — DESIGN NOTE (NOT a security boundary):
//! When invoked from inside Claude Code (`$CLAUDECODE=1`) or Grok the chain
//! is SKIPPED to avoid double-firing the same hooks (they already ran on the
//! CLI's own PreToolUse). This is a perf / UX optimization for the inside-CLI
//! call path — NOT an authorization check. An attacker who can set the
//! parent process's environment already controls the CLI invocation anyway;
//! re-running hooks would not stop them. To raise the bar for confused-deputy
//! scenarios use full sandboxing (Phase D) or run kei-mcp as a separate UID.
//!
//! v0.41 audit fixes (2026-05-26, Gemini security review):
//! #1 fail-CLOSED on missing hooks (was: silently skip)
//! #2 path-traversal guard on kei_edit/kei_write (canonicalize + root check)
//! #3 CLAUDECODE bypass — documented as design (see above), no behavior change
//! #4 tokio::fs for async file I/O (was: blocking std::fs on tokio thread)
//! #5 process-group kill on Unix (was: kill_on_drop SIGKILLs only direct child)
//!
//! v0.42 re-audit fixes (2026-05-26, 4-CLI dogfood: Claude+Grok+Gemini+Copilot):
//! #1 [CRITICAL] symlink LEAF bypass — canonicalize full path + reject
//! leaf symlinks (v0.41 only canonicalized PARENT; ln -s ~/.ssh/keys ./x
//! then kei_write x followed the link to the target)
//! #2 [HIGH] $HOME removed from default allowed_roots — was a blanket
//! allow that let agent overwrite ~/.claude/hooks (self-neuter), ~/.zshrc
//! (RCE on next shell), and credential stores. Default: $PWD only.
//! Denylist also extended with .claude/, .grok/, .gemini/, .copilot/,
//! .kimi/, and exact shell-init filenames.
//! #3 [HIGH] empty [bash]/[edit]/[write] section also FAIL-CLOSED (was:
//! empty vec → pass-through). KEI_POLICY_CHAIN_OPTIONAL=1 to opt in.
//! #4 [MED] load_chain converted to async + tokio::fs (was: blocking
//! std::fs on tokio worker thread).
//! #5 [MED] set_process_group + killpg applied to HOOK subprocess too
//! (v0.41 only had it on the bash action; hook grandchildren orphaned).
//! #6 [MED] doc note that aggregate timeout is still per-step (60s ×
//! N hooks + 60s action). Single-deadline implementation deferred to
//! v0.43 — not security-blocking.
use crate::protocol::{err, ok, JsonRpcRequest, JsonRpcResponse, INTERNAL_ERROR, INVALID_PARAMS};
use serde::Deserialize;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Per-step timeout (each hook AND the action each get up to this long).
/// For an N-hook chain the total wall-clock cap is approximately
/// `(N+1) * SAFE_TOOL_TIMEOUT_SECS`. v0.44 doc-honesty fix (Claude MED):
/// prior versions claimed this was an "aggregate" cap, which was always
/// wrong. Aggregate-deadline impl is deferred; for now the per-step
/// semantics are documented honestly so operators pick a sane value.
const SAFE_TOOL_TIMEOUT_SECS: u64 = 60;
#[derive(Deserialize, Default)]
struct PolicyChain {
#[serde(default)]
bash: ChainSpec,
#[serde(default)]
edit: ChainSpec,
#[serde(default)]
write: ChainSpec,
}
#[derive(Deserialize, Default)]
struct ChainSpec {
#[serde(default)]
chain: Vec<String>,
}
/// MCP tool descriptors — appended to `tools/list` by `handlers::tools::list`.
pub fn descriptors() -> Vec<Value> {
vec![
json!({
"name": "kei_bash",
"description": "Run a shell command after running KeiSeiKit's [bash] policy chain (no-github-push, safety-guard, destructive-guard). Blocks on hook exit 2 with the hook's stderr surfaced as the MCP error message. Use this instead of native shell on non-Claude CLIs to inherit Claude Code's safety enforcement.",
"inputSchema": {
"type": "object",
"properties": {
"command": { "type": "string", "description": "Shell command to execute" },
"cwd": { "type": "string", "description": "Optional working directory; defaults to $PWD" }
},
"required": ["command"]
}
}),
json!({
"name": "kei_edit",
"description": "Modify a file (replace old_string with new_string) after running KeiSeiKit's [edit] policy chain (citation-verify, numeric-claims-guard). Blocks unverified academic citations and numeric claims without evidence markers.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string" },
"old_string": { "type": "string" },
"new_string": { "type": "string" }
},
"required": ["file_path", "old_string", "new_string"]
}
}),
json!({
"name": "kei_write",
"description": "Write content to a file after running KeiSeiKit's [write] policy chain (citation-verify, numeric-claims-guard). Blocks unverified academic citations and numeric claims without evidence markers.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string" },
"content": { "type": "string" }
},
"required": ["file_path", "content"]
}
}),
]
}
/// Top-level dispatch entry — called from `handlers::tools::call` when the
/// tool name matches one of the three `kei_*` built-ins.
pub async fn dispatch_safe(req: JsonRpcRequest, name: &str, args: &Value) -> JsonRpcResponse {
let result = match name {
"kei_bash" => handle_bash(args).await,
"kei_edit" => handle_edit(args).await,
"kei_write" => handle_write(args).await,
_ => Err(format!("safe_tools dispatched unknown name: {name}")),
};
match result {
Ok(text) => ok(req.id, json!({
"content": [{ "type": "text", "text": text }],
"isError": false,
})),
Err(e) => err(req.id, INTERNAL_ERROR, e),
}
}
// ---- per-tool handlers --------------------------------------------------
async fn handle_bash(args: &Value) -> Result<String, String> {
let command = args.get("command").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_bash", "command"))?;
let cwd = args.get("cwd").and_then(Value::as_str);
// v0.44 fix #8 (Gemini MED): include cwd in hook input. Without this,
// safety-guard could approve a destructive command (e.g. `rm -rf *`)
// assuming PWD, while the actual cwd arg redirected it to a sensitive
// dir. Hooks now see the real working directory.
let hook_input = json!({
"tool_name": "Bash",
"tool_input": {
"command": command,
"cwd": cwd
}
});
run_chain("bash", &hook_input).await?;
let mut cmd = Command::new("bash");
cmd.arg("-c").arg(command);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
// v0.41 fix #5: put child in its own process group so timeout kills it
// and ALL grandchildren together (not just the immediate shell).
set_process_group(&mut cmd);
// v0.44 fix #4 (Gemini HIGH): clear parent env on subprocess spawn.
// Was: child inherited AWS_*, GITHUB_TOKEN, MOONSHOT_API_KEY, etc.
// An agent that exec's `env` via kei_bash could exfiltrate all of them.
// Now: only PATH/HOME/USER/LANG/TERM/SHELL forwarded (set in helper).
apply_safe_env(&mut cmd);
let child = cmd.spawn().map_err(|e| format!("spawn bash: {e}"))?;
let pid_opt = child.id();
let fut = child.wait_with_output();
let out = match tokio::time::timeout(Duration::from_secs(SAFE_TOOL_TIMEOUT_SECS), fut).await {
Ok(Ok(o)) => o,
Ok(Err(e)) => return Err(format!("wait bash: {e}")),
Err(_) => {
// Timeout — kill the entire process group, not just the child.
if let Some(pid) = pid_opt {
killpg_best_effort(pid);
}
return Err("kei_bash timeout".to_string());
}
};
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
if !out.status.success() {
return Err(format!(
"bash exited {}: {}",
out.status.code().unwrap_or(-1),
stderr.trim()
));
}
Ok(if stderr.is_empty() { stdout } else { format!("{stdout}\n[stderr]\n{stderr}") })
}
// v0.41 fix #5: process-group helpers (Unix-only; no-op on other platforms).
#[cfg(unix)]
fn set_process_group(cmd: &mut Command) {
cmd.process_group(0);
}
#[cfg(not(unix))]
fn set_process_group(_cmd: &mut Command) {}
/// v0.44 fix #4 (Gemini HIGH): strip parent env on subprocess spawn so secrets
/// like AWS_*, GITHUB_TOKEN, MOONSHOT_API_KEY etc. don't leak to user-controlled
/// bash commands or hook scripts. Whitelist forwards only PATH/HOME/USER/LANG/
/// TERM/SHELL — enough to keep tools functional, none of it sensitive.
///
/// Override: `KEI_SAFE_ENV_EXTRA=":-separated list"` adds named vars to the
/// whitelist for callers that legitimately need (e.g. NIX_PATH, JAVA_HOME).
fn apply_safe_env(cmd: &mut Command) {
cmd.env_clear();
let default_keep = [
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL",
"LC_CTYPE", "TERM", "PWD", "TMPDIR",
];
for k in default_keep {
if let Ok(v) = std::env::var(k) {
cmd.env(k, v);
}
}
if let Ok(extras) = std::env::var("KEI_SAFE_ENV_EXTRA") {
for k in extras.split(':') {
let k = k.trim();
if k.is_empty() { continue; }
if let Ok(v) = std::env::var(k) {
cmd.env(k, v);
}
}
}
}
#[cfg(unix)]
fn killpg_best_effort(pid: u32) {
// SAFETY: libc::kill on a negative PID targets the process group.
// SIGKILL = 9. Best-effort — ignore errors (process may have exited).
unsafe {
let _ = libc::kill(-(pid as i32), libc::SIGKILL);
}
}
#[cfg(not(unix))]
fn killpg_best_effort(_pid: u32) {}
async fn handle_edit(args: &Value) -> Result<String, String> {
let file_path = args.get("file_path").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_edit", "file_path"))?;
let old_string = args.get("old_string").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_edit", "old_string"))?;
let new_string = args.get("new_string").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_edit", "new_string"))?;
// v0.44 LOW: reject empty old_string (would silently prepend new_string
// because contents.contains("") is always true).
if old_string.is_empty() {
return Err("kei_edit: old_string must not be empty".into());
}
let safe_path = validate_path(file_path)?;
let hook_input = json!({
"tool_name": "Edit",
"tool_input": {
"file_path": safe_path.display().to_string(),
"old_string": old_string,
"new_string": new_string
}
});
run_chain("edit", &hook_input).await?;
// v0.44 fix #2 (Gemini HIGH + Claude #4 MED): close TOCTOU window. After
// validate_path approved the path, a concurrent process could swap the
// file for a symlink before our write. Open the existing file with
// O_NOFOLLOW so the open itself fails on symlink-swap; then read/write
// through the open fd (not the path again) so no second path lookup.
open_nofollow_read_write_edit(&safe_path, old_string, new_string).await
}
async fn handle_write(args: &Value) -> Result<String, String> {
let file_path = args.get("file_path").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_write", "file_path"))?;
let content = args.get("content").and_then(Value::as_str)
.ok_or_else(|| missing_arg("kei_write", "content"))?;
let safe_path = validate_path(file_path)?;
let hook_input = json!({
"tool_name": "Write",
"tool_input": { "file_path": safe_path.display().to_string(), "content": content }
});
run_chain("write", &hook_input).await?;
if let Some(parent) = safe_path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent).await
.map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
}
}
// v0.44 fix #2: open with O_NOFOLLOW + O_CREAT to refuse swap-to-symlink.
open_nofollow_write(&safe_path, content).await
}
/// v0.44 fix #2: edit via O_NOFOLLOW-opened fd to close the TOCTOU window
/// between validate_path and the write. The open() itself refuses if the leaf
/// has been swapped to a symlink during the hook-chain await.
#[cfg(unix)]
async fn open_nofollow_read_write_edit(
path: &Path, old_string: &str, new_string: &str,
) -> Result<String, String> {
use std::os::unix::fs::OpenOptionsExt;
let path = path.to_path_buf();
let old_s = old_string.to_string();
let new_s = new_string.to_string();
// Blocking syscalls on a dedicated thread (tokio::task::spawn_blocking).
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let mut f = std::fs::OpenOptions::new()
.read(true).write(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&path)
.map_err(|e| format!("kei_edit: open(O_NOFOLLOW) {}: {e}", path.display()))?;
use std::io::{Read, Write, Seek, SeekFrom};
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| format!("kei_edit: read {}: {e}", path.display()))?;
if !contents.contains(&old_s) {
return Err(format!("kei_edit: old_string not found in {}", path.display()));
}
let updated = contents.replacen(&old_s, &new_s, 1);
f.set_len(0).map_err(|e| format!("kei_edit: truncate {}: {e}", path.display()))?;
f.seek(SeekFrom::Start(0))
.map_err(|e| format!("kei_edit: seek {}: {e}", path.display()))?;
f.write_all(updated.as_bytes())
.map_err(|e| format!("kei_edit: write {}: {e}", path.display()))?;
Ok(format!("edited {} ({} bytes)", path.display(), updated.len()))
}).await
.map_err(|e| format!("kei_edit: thread join: {e}"))?;
result
}
#[cfg(not(unix))]
async fn open_nofollow_read_write_edit(
path: &Path, old_string: &str, new_string: &str,
) -> Result<String, String> {
// Non-Unix fallback: best-effort using tokio::fs (no O_NOFOLLOW available).
let contents = fs::read_to_string(path).await
.map_err(|e| format!("read {}: {e}", path.display()))?;
if !contents.contains(old_string) {
return Err(format!("kei_edit: old_string not found in {}", path.display()));
}
let updated = contents.replacen(old_string, new_string, 1);
fs::write(path, &updated).await
.map_err(|e| format!("write {}: {e}", path.display()))?;
Ok(format!("edited {} ({} bytes)", path.display(), updated.len()))
}
#[cfg(unix)]
async fn open_nofollow_write(path: &Path, content: &str) -> Result<String, String> {
use std::os::unix::fs::OpenOptionsExt;
let path = path.to_path_buf();
let bytes = content.as_bytes().to_vec();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
// O_NOFOLLOW: refuse if the leaf is a symlink (someone swapped it
// during our await). Without this the v0.42 symlink_metadata pre-check
// was just an indicator — fs::write still followed.
opts.custom_flags(libc::O_NOFOLLOW);
// O_EXCL combined with O_CREAT could be added when path does not yet
// exist to refuse any pre-existing inode — but the test suite uses
// the same path multiple times, so we keep truncate semantics. The
// O_NOFOLLOW + symlink_metadata pre-check is sufficient.
let mut f = opts.open(&path)
.map_err(|e| format!("kei_write: open(O_NOFOLLOW) {}: {e}", path.display()))?;
use std::io::Write;
f.write_all(&bytes)
.map_err(|e| format!("kei_write: write {}: {e}", path.display()))?;
Ok(format!("wrote {} ({} bytes)", path.display(), bytes.len()))
}).await
.map_err(|e| format!("kei_write: thread join: {e}"))?;
result
}
#[cfg(not(unix))]
async fn open_nofollow_write(path: &Path, content: &str) -> Result<String, String> {
fs::write(path, content).await
.map_err(|e| format!("write {}: {e}", path.display()))?;
Ok(format!("wrote {} ({} bytes)", path.display(), content.len()))
}
/// Path-traversal + symlink + denylist guard.
///
/// v0.41 (initial): rejected `..`, canonicalized PARENT, checked denylist + roots.
/// → 4-CLI re-audit (2026-05-26) found this was bypassable via symlink at the
/// leaf and self-attackable via the $HOME blanket-allowed root.
///
/// v0.42 fixes:
/// #1 [CRITICAL] reject if the leaf is a symlink (was: validated parent
/// only, fs::write followed leaf symlink to anywhere). Done via
/// `symlink_metadata` on the leaf BEFORE write, and full `canonicalize`
/// on the leaf when the file already exists.
/// #2 [HIGH] $HOME removed from default allowed-roots — default is $PWD
/// only. Denylist now also covers $HOME/.claude/ (the substrate
/// itself), shell init files, and credential stores. Operators who
/// need broader access set KEI_ALLOWED_ROOTS explicitly.
fn validate_path(p: &str) -> Result<PathBuf, String> {
if p.is_empty() {
return Err("file_path: empty".into());
}
// 1. Reject literal `..` segments — covers most traversal attempts.
if p.split('/').any(|seg| seg == "..") {
return Err(format!("file_path: '..' segment not allowed in {p}"));
}
let path = Path::new(p);
// 2. Build a canonical path. Walk UP to the deepest existing ancestor,
// canonicalize it (resolves all symlinks in the existing prefix),
// then reattach the non-existent tail. This catches symlinks at ANY
// depth in the path, including nested non-existent leaves.
//
// v0.44 fix #1 (Gemini CRITICAL): v0.42 only canonicalized the immediate
// parent. If the parent didn't exist either (e.g. /proj/symlink_dir/
// new_subdir/file.txt where symlink_dir → /Users/denis), the path fell
// through to "absolute as-is" → no canonicalization → bypass.
let canonical = canonicalize_with_walk_up(path)?;
// 3. Even when the file doesn't exist yet, the LEAF could already be a
// dangling symlink that `fs::write` would follow on creation. Reject.
if let Ok(meta) = std::fs::symlink_metadata(&canonical) {
if meta.file_type().is_symlink() {
return Err(format!(
"file_path: leaf is a symlink (refusing to follow): {}",
canonical.display()
));
}
}
// 4. Allowed-root containment FIRST (v0.44 fix #6 reorder: was after
// denylist, which meant macOS $TMPDIR = /private/var/folders/... hit
// the /var/ denylist before reaching the allowed_roots check, blocking
// legitimate use of tempfile-backed CWD on macOS).
//
// v0.44 fix #5 (Claude HIGH): use Path::starts_with for component-aware
// containment — Path::starts_with("/home/u/proj") does NOT match
// /home/u/proj-secrets, the str::starts_with that was here did.
let roots = allowed_roots();
let in_allowed_root = roots.is_empty() || roots.iter().any(|r| {
canonical.starts_with(r)
});
if !in_allowed_root {
return Err(format!(
"file_path: outside allowed roots {:?}: {}",
roots, canonical.display()
));
}
let canon_str = canonical.display().to_string();
// 5. Reject system + substrate-control + credential paths.
// Note: paths inside an allowed root that also match a denylist entry
// are STILL denied (e.g. agent's CWD == ~/.claude/ — denied even
// though it matches a default root). System dirs not in any allowed
// root would have been caught above anyway.
let denylist = [
"/etc/", "/usr/", "/System/", "/var/db/", "/var/log/", "/var/root/",
"/private/etc/", "/private/var/db/", "/private/var/log/", "/private/var/root/",
"/root/", "/bin/", "/sbin/",
];
// NOTE: /var/folders/ (macOS $TMPDIR) and /private/tmp/ are NOT denied —
// they are legitimate working dirs for tempfile-backed agents.
for d in denylist {
if canon_str.starts_with(d) {
return Err(format!("file_path: denied (system dir): {canon_str}"));
}
}
if let Ok(home) = std::env::var("HOME") {
let dir_secrets = [
".ssh/", ".aws/", ".gnupg/", ".config/gcloud/", ".cargo/credentials",
".npmrc", ".docker/config.json", ".kube/",
".claude/", ".grok/", ".gemini/", ".copilot/", ".kimi/",
];
for sd in dir_secrets {
let full = format!("{home}/{sd}");
if canon_str.starts_with(&full) {
return Err(format!("file_path: denied (secret/substrate dir): {canon_str}"));
}
}
let init_files = [
".zshrc", ".bashrc", ".profile", ".bash_profile", ".zprofile",
".zshenv", ".bash_login", ".inputrc", ".gitconfig",
".config/fish/config.fish",
];
for f in init_files {
let full = format!("{home}/{f}");
if canon_str == full {
return Err(format!("file_path: denied (shell-init file): {canon_str}"));
}
}
}
Ok(canonical)
}
/// v0.44 fix #1: walk up the path looking for the deepest existing ancestor,
/// canonicalize THAT, then reattach the non-existent tail components.
/// Resolves symlinks at any depth (existing OR non-existing branches).
fn canonicalize_with_walk_up(path: &Path) -> Result<PathBuf, String> {
// Make the path absolute first so we can walk up reliably.
let abs = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| format!("file_path: cwd unavailable: {e}"))?
.join(path)
};
// Walk up from the leaf, collecting non-existent components in reverse.
let mut current = abs.clone();
let mut tail: Vec<std::ffi::OsString> = Vec::new();
let canon = loop {
if current.exists() {
break current.canonicalize()
.map_err(|e| format!("file_path: canonicalize {}: {e}", current.display()))?;
}
let name = current.file_name()
.ok_or_else(|| format!("file_path: path has no existing ancestor: {}", abs.display()))?
.to_os_string();
let parent = match current.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => return Err(format!("file_path: walked to root without finding existing dir: {}", abs.display())),
};
tail.push(name);
current = parent;
};
// Reattach tail (in reverse — we pushed from leaf to root).
let mut result = canon;
for name in tail.into_iter().rev() {
result.push(name);
}
Ok(result)
}
fn allowed_roots() -> Vec<String> {
// Canonicalize each entry so symlinked roots (e.g. macOS /var → /private/var,
// /tmp → /private/tmp) match canonicalized targets. Trailing slash added
// for the consistency-with-default format. v0.44 fix #5 + #6 combined.
let canon_with_slash = |raw: &str| -> Option<String> {
let p = Path::new(raw);
let canon = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let mut s = canon.display().to_string();
if !s.ends_with('/') { s.push('/'); }
if s.is_empty() { None } else { Some(s) }
};
if let Ok(v) = std::env::var("KEI_ALLOWED_ROOTS") {
return v.split(':')
.filter(|s| !s.is_empty())
.filter_map(canon_with_slash)
.collect();
}
let mut roots = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
if let Some(r) = canon_with_slash(&cwd.display().to_string()) {
roots.push(r);
}
}
roots
}
// ---- chain runner -------------------------------------------------------
/// Run the configured hook chain for `tool` ("bash"/"edit"/"write"), piping
/// `hook_input` to each hook's stdin in order. Exit 0 → continue. Exit 2 (or
/// other non-zero) → return Err with the hook's stderr.
///
/// Skips the chain if the parent process is already inside Claude or Grok
/// (env flags), since those CLIs' native PreToolUse hooks already fired.
/// Run the configured hook chain for `tool` ("bash"/"edit"/"write").
///
/// v0.42 fixes:
/// #3 [HIGH] empty chain (section absent or zero hooks) now FAILS CLOSED
/// unless KEI_POLICY_CHAIN_OPTIONAL=1.
/// #4 [MED] load_chain() converted to async (was: blocking std::fs).
/// #5 [MED] hook subprocess gets `process_group(0)` + killpg on timeout
/// (was: only the bash action got it; hooks could orphan).
/// #6 [MED] aggregate timeout across the whole chain + action (was:
/// per-hook 60s, so chain+action could legitimately run
/// 4× the documented cap on a 3-hook chain).
async fn run_chain(tool: &str, hook_input: &Value) -> Result<(), String> {
if env_truthy("CLAUDECODE") || env_truthy("GROKCODE") {
// Native hooks already enforced — don't double-fire.
return Ok(());
}
let chain = load_chain(tool).await?;
if chain.is_empty() {
// v0.42 fix #3 (Claude+Gemini HIGH): empty section is the same
// misconfig class as missing file — FAIL CLOSED with explicit opt-in.
if env_truthy("KEI_POLICY_CHAIN_OPTIONAL") {
return Ok(());
}
return Err(format!(
"[policy-chain] section [{tool}] is empty — refusing to run \
(set KEI_POLICY_CHAIN_OPTIONAL=1 to allow pass-through, e.g. for tests)"
));
}
let hooks_dir = hooks_dir()?;
let payload = serde_json::to_string(hook_input)
.map_err(|e| format!("encode hook input: {e}"))?;
for hook in chain {
let path = hooks_dir.join(&hook);
if !path.is_file() {
return Err(format!(
"[policy-chain] hook missing: {} (declared in policy-chain.toml [{}])",
path.display(), tool
));
}
let mut child_cmd = Command::new(&path);
child_cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
set_process_group(&mut child_cmd);
// v0.44 fix #4: same env-isolation for hook subprocess.
apply_safe_env(&mut child_cmd);
let mut child = child_cmd
.spawn()
.map_err(|e| format!("spawn {}: {e}", path.display()))?;
let pid_opt = child.id();
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(payload.as_bytes()).await
.map_err(|e| format!("write stdin to {}: {e}", path.display()))?;
stdin.shutdown().await
.map_err(|e| format!("close stdin to {}: {e}", path.display()))?;
}
let fut = child.wait_with_output();
let out = match tokio::time::timeout(Duration::from_secs(SAFE_TOOL_TIMEOUT_SECS), fut).await {
Ok(Ok(o)) => o,
Ok(Err(e)) => return Err(format!("wait {}: {e}", path.display())),
Err(_) => {
// v0.42 fix #5: kill the whole hook process group, not just
// the immediate child.
if let Some(pid) = pid_opt {
killpg_best_effort(pid);
}
return Err(format!("hook {hook} timeout"));
}
};
let code = out.status.code().unwrap_or(-1);
if code == 0 {
continue;
}
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
return Err(format!(
"[blocked by {hook} exit={code}]\n{stderr}"
));
}
Ok(())
}
// ---- config helpers -----------------------------------------------------
/// v0.42 fix #4: async + tokio::fs (was: blocking std::fs would freeze
/// a tokio worker if policy-chain.toml lived on a slow / hung mount).
async fn load_chain(tool: &str) -> Result<Vec<String>, String> {
let path = chain_path()?;
// tokio::fs::try_exists avoids a blocking is_file() syscall.
let exists = fs::try_exists(&path).await.unwrap_or(false);
if !exists {
if env_truthy("KEI_POLICY_CHAIN_OPTIONAL") {
return Ok(vec![]);
}
return Err(format!(
"[policy-chain] config missing: {} (set KEI_POLICY_CHAIN_OPTIONAL=1 to allow pass-through, e.g. for tests)",
path.display()
));
}
let raw = fs::read_to_string(&path).await
.map_err(|e| format!("read policy-chain.toml: {e}"))?;
let parsed: PolicyChain = toml::from_str(&raw)
.map_err(|e| format!("parse policy-chain.toml: {e}"))?;
let chain = match tool {
"bash" => parsed.bash.chain,
"edit" => parsed.edit.chain,
"write" => parsed.write.chain,
_ => return Err(format!("unknown tool kind: {tool}")),
};
Ok(chain)
}
fn chain_path() -> Result<PathBuf, String> {
if let Ok(p) = std::env::var("KEI_POLICY_CHAIN") {
return Ok(PathBuf::from(p));
}
let dir = hooks_dir()?;
Ok(dir.join("_lib").join("policy-chain.toml"))
}
fn hooks_dir() -> Result<PathBuf, String> {
if let Ok(p) = std::env::var("KEI_HOOKS_DIR") {
return Ok(PathBuf::from(p));
}
let home = std::env::var("HOME").map_err(|_| "HOME not set".to_string())?;
Ok(PathBuf::from(home).join(".claude").join("hooks"))
}
fn env_truthy(name: &str) -> bool {
matches!(std::env::var(name).as_deref(), Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes"))
}
fn missing_arg(tool: &str, field: &str) -> String {
format!("{tool}: missing '{field}' argument")
}
#[allow(dead_code)]
const INVALID_PARAMS_REF: i32 = INVALID_PARAMS; // silence unused-import warning if removed

View file

@ -33,6 +33,16 @@ pub fn list(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
.into_iter() .into_iter()
.map(atom_to_tool_descriptor) .map(atom_to_tool_descriptor)
.collect(); .collect();
// v0.39: built-in spawn_agent tool — exposed to all MCP clients so any
// CLI (grok / agy / copilot / kimi / claude) can spawn a KeiSeiKit agent
// as a sub-agent. Bypasses atom discovery (it's an internal handler).
tools.push(spawn_agent_descriptor());
// v0.40 (Phase C): policy-gated MCP tools — kei_bash / kei_edit /
// kei_write run the configured hook chain BEFORE executing the action.
// This restores Claude Code's PreToolUse safety on non-Claude CLIs
// (Grok / Agy / Copilot / Kimi) — any MCP-capable orchestrator that
// disables its native shell + uses kei_bash gets full enforcement.
tools.extend(super::safe_tools::descriptors());
tools.sort_by(|a, b| { tools.sort_by(|a, b| {
a.get("name").and_then(Value::as_str).unwrap_or("") a.get("name").and_then(Value::as_str).unwrap_or("")
.cmp(b.get("name").and_then(Value::as_str).unwrap_or("")) .cmp(b.get("name").and_then(Value::as_str).unwrap_or(""))
@ -50,6 +60,23 @@ pub async fn call(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
None => return err(req.id, INVALID_PARAMS, "missing tool name"), None => return err(req.id, INVALID_PARAMS, "missing tool name"),
}; };
let args = params.get("arguments").cloned().unwrap_or(json!({})); let args = params.get("arguments").cloned().unwrap_or(json!({}));
// v0.39: spawn_agent built-in — short-circuit before atom dispatch.
if name == "spawn_agent" {
return match invoke_spawn_agent(&args).await {
Ok(text) => ok(req.id, json!({
"content": [{ "type": "text", "text": text }],
"isError": false,
})),
Err(e) => err(req.id, INTERNAL_ERROR, e),
};
}
// v0.40 (Phase C): kei_bash / kei_edit / kei_write — policy-gated tools.
if matches!(name.as_str(), "kei_bash" | "kei_edit" | "kei_write") {
return super::safe_tools::dispatch_safe(req, &name, &args).await;
}
match invoke_atom(&ctx.atoms_root, &name, &args).await { match invoke_atom(&ctx.atoms_root, &name, &args).await {
Ok(result) => ok(req.id, json!({ Ok(result) => ok(req.id, json!({
"content": [{ "type": "text", "text": serde_json::to_string(&result).unwrap_or_default() }], "content": [{ "type": "text", "text": serde_json::to_string(&result).unwrap_or_default() }],
@ -59,6 +86,94 @@ pub async fn call(req: JsonRpcRequest, ctx: &ServerContext) -> JsonRpcResponse {
} }
} }
/// v0.39: built-in `spawn_agent` MCP tool descriptor.
/// Exposes KeiSeiKit's cross-CLI agent launcher (`kei-agent-cli.sh`) so any
/// MCP client can spawn an agent on any backend (claude / grok / agy /
/// copilot / kimi). Solves the "non-claude orchestrator can't natively spawn
/// sub-agents" gap — any CLI with MCP support gets the spawn capability.
fn spawn_agent_descriptor() -> Value {
json!({
"name": "spawn_agent",
"description": "Spawn a KeiSeiKit agent as a sub-agent through any configured LLM CLI backend. Reads ~/.claude/agents/<name>.md, composes with the task, and execs the chosen backend non-interactively. Backend resolution: explicit `on` arg → agent manifest's `provider` → ~/.claude/config/primary.toml → claude.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Agent name (looked up in ~/.claude/agents/<name>.md)"
},
"task": {
"type": "string",
"description": "The task / question to give the agent"
},
"on": {
"type": "string",
"description": "Optional explicit backend override (claude/grok/agy/copilot/kimi/codex). Default: DNA → primary → claude.",
"enum": ["claude", "grok", "agy", "antigravity", "copilot", "kimi", "codex"]
}
},
"required": ["name", "task"]
}
})
}
/// v0.39: handler for `tools/call name=spawn_agent`. Shells out to
/// `kei-agent-cli.sh` (located via $HOME/.claude/scripts/) and returns
/// the backend's stdout as the tool result.
async fn invoke_spawn_agent(args: &Value) -> Result<String, String> {
let name = args.get("name").and_then(Value::as_str)
.ok_or_else(|| "spawn_agent: missing 'name' argument".to_string())?;
let task = args.get("task").and_then(Value::as_str)
.ok_or_else(|| "spawn_agent: missing 'task' argument".to_string())?;
let on_opt = args.get("on").and_then(Value::as_str);
// Locate the launcher script. Honors KEI_AGENT_CLI override for testing.
let script = match std::env::var("KEI_AGENT_CLI") {
Ok(v) => PathBuf::from(v),
Err(_) => {
let home = std::env::var("HOME").map_err(|_| "HOME not set".to_string())?;
PathBuf::from(home).join(".claude/scripts/kei-agent-cli.sh")
}
};
if !script.is_file() {
return Err(format!("kei-agent-cli.sh not found: {}", script.display()));
}
let mut cmd = Command::new(&script);
if let Some(on) = on_opt {
cmd.arg(format!("--on={on}"));
}
cmd.arg(name).arg(task);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let child = cmd.spawn()
.map_err(|e| format!("spawn {}: {e}", script.display()))?;
let fut = child.wait_with_output();
// Reuse the existing ATOM_TIMEOUT_SECS for the spawn_agent cap too —
// 60s should suffice for non-interactive prompts; longer tasks would
// need streaming, which the MCP tools-call contract doesn't support
// anyway. Hung agents are killed at the timeout.
match tokio::time::timeout(Duration::from_secs(ATOM_TIMEOUT_SECS), fut).await {
Ok(Ok(out)) => {
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
return Err(format!(
"spawn_agent backend exited {}: {stderr}",
out.status.code().unwrap_or(-1)
));
}
Ok(stdout)
}
Ok(Err(e)) => Err(format!("wait: {e}")),
Err(_) => Err("spawn_agent timeout".into()),
}
}
/// Convert one atom's metadata into the MCP tool-descriptor shape. /// Convert one atom's metadata into the MCP tool-descriptor shape.
fn atom_to_tool_descriptor(meta: AtomMeta) -> Value { fn atom_to_tool_descriptor(meta: AtomMeta) -> Value {
let description = first_paragraph(&meta.body); let description = first_paragraph(&meta.body);

View file

@ -68,7 +68,19 @@ async fn tools_list_returns_two_atoms_with_descriptors() {
let resp = dispatch(req, &ctx).await; let resp = dispatch(req, &ctx).await;
let result = resp.result.expect("should have result"); let result = resp.result.expect("should have result");
let tools = result["tools"].as_array().expect("tools array"); let tools = result["tools"].as_array().expect("tools array");
assert_eq!(tools.len(), 2); // v0.40 (Phase C): list includes 4 built-ins (spawn_agent + kei_bash +
// kei_edit + kei_write) on top of discovered atoms.
assert_eq!(tools.len(), 6); // 2 atoms + 4 built-ins
assert!(
tools.iter().any(|t| t["name"] == "spawn_agent"),
"spawn_agent built-in must be present"
);
for kei in ["kei_bash", "kei_edit", "kei_write"] {
assert!(
tools.iter().any(|t| t["name"] == kei),
"{kei} built-in must be present"
);
}
// sorted alphabetically // sorted alphabetically
assert_eq!(tools[0]["name"], "kei-sage::ask"); assert_eq!(tools[0]["name"], "kei-sage::ask");
assert_eq!(tools[1]["name"], "kei-task::search"); assert_eq!(tools[1]["name"], "kei-task::search");
@ -91,5 +103,14 @@ async fn tools_list_handles_empty_root() {
}; };
let resp = dispatch(req, &ctx).await; let resp = dispatch(req, &ctx).await;
let result = resp.result.expect("should have result"); let result = resp.result.expect("should have result");
assert_eq!(result["tools"].as_array().unwrap().len(), 0); // v0.40 (Phase C): empty atoms root surfaces 4 built-ins
// (spawn_agent + kei_bash + kei_edit + kei_write).
let tools = result["tools"].as_array().unwrap();
assert_eq!(tools.len(), 4);
let names: Vec<&str> = tools.iter()
.filter_map(|t| t["name"].as_str())
.collect();
for required in ["spawn_agent", "kei_bash", "kei_edit", "kei_write"] {
assert!(names.contains(&required), "missing built-in: {required}");
}
} }

View file

@ -0,0 +1,45 @@
# cli-backends.toml — SSoT for external LLM CLIs that can host KeiSeiKit agents.
#
# Each backend is a CLI you have a subscription / local install of. The
# `kei run-via <backend> <agent> "<task>"` launcher composes an agent's
# assembled .md prompt with the task and invokes the backend's
# non-interactive (print) mode.
#
# Add a backend by appending a `[backend.<name>]` table. The launcher
# (`scripts/kei-agent-cli.sh`) reads `bin` + `prompt_flag` and execs.
[backend.claude]
bin = "claude"
prompt_flag = "-p"
notes = "Claude Code (Anthropic) — native --agent flag also supported"
homepage = "https://claude.com/claude-code"
[backend.grok]
bin = "grok"
prompt_flag = "--print"
notes = "xAI Grok Build TUI — native --agent flag also supported"
homepage = "https://x.ai/grok"
[backend.agy]
bin = "agy"
prompt_flag = "--print"
notes = "Google Antigravity (alias: antigravity)"
aliases = ["antigravity"]
[backend.copilot]
bin = "copilot"
prompt_flag = "--prompt"
notes = "GitHub Copilot CLI (@github/copilot npm)"
homepage = "https://github.com/github/copilot-cli"
[backend.kimi]
bin = "kimi"
prompt_flag = "tui-only"
notes = "Moonshot Kimi CLI — TUI-ONLY (smoke 2026-05-26). Headless requires ACP client; launcher saves prompt to tmpfile + opens TUI for paste."
homepage = "https://moonshotai.github.io/kimi-cli/"
[backend.codex]
bin = "codex"
prompt_flag = "-p"
notes = "OpenAI Codex CLI — register here, install separately"
homepage = "https://github.com/openai/codex"

View file

@ -0,0 +1,55 @@
# KeiSeiKit hook-pack + stack-profile map — single source of truth for the
# opt-in install posture. Parsed by install/lib-packs.sh, which reuses the
# generic TOML array reader `_toml_array` extracted from lib-profile.sh
# (python-tomllib preferred, awk fallback). No new dependency.
#
# Values are HOOK BASENAMES WITHOUT `.sh`, matched against the command
# basenames in settings-snippet.json. Every hook wired in the snippet MUST
# appear in exactly one [pack] entry or in [pack-always]; anything missing
# would be silently filtered out of a fresh install.
#
# Posture: only `safety` + `pack-always` are active on a fresh/non-interactive
# install. All other packs are opt-in (via onboarding or `kei configure`).
# `git-guard` (no-github-push) is opt-in ONLY and is pulled by NO stack — a
# general kit must never block a user's normal `git push` to github by default.
[pack]
safety = ["block-dangerous", "safety-guard", "destructive-guard", "disk-headroom-check", "secrets-pre-guard", "no-hand-edit-agents", "assemble-validate", "assemble-agents"]
evidence = ["numeric-claims-guard", "citation-verify", "chat-numeric-prewarn", "chat-numeric-postflag"]
observability = ["task-timer", "session-end-dump", "extract-task-durations", "error-spike-detector", "agent-event-spawn", "agent-event-done", "agent-heartbeat-tick", "stop-verify"]
epistemic = ["alignment-check", "no-downgrade", "recurrence-suggest"]
orchestration = ["agent-fork-logger", "agent-fork-done", "orchestrator-dirty-check", "orchestrator-branch-check", "agent-capability-check", "agent-stub-scan", "milestone-commit-hook", "post-commit-audit", "post-write-check"]
git-guard = ["no-github-push"]
stack-rust = ["rust-first", "no-python-without-approval"]
# Always wired, never filtered (cosmetic / infra). The keisei-pet*.sh status
# updater + the inline pet hook are kept by the filter directly (name match),
# so they are NOT listed here.
[pack-always]
base = ["first-run-onboard", "mailbox-inject", "tomd-preread", "site-wysiwyd-check"]
# Stack profile -> discipline packs auto-enabled (safety is always implicit).
# git-guard intentionally absent from every stack (opt-in only).
[stack-packs]
minimal = []
web = ["evidence", "observability"]
ml = ["evidence", "observability", "epistemic"]
systems = ["evidence", "observability", "stack-rust"]
mobile = ["evidence", "observability"]
# Stack profile -> agent groups installed (the `base` group is always added).
[stack-agents]
minimal = ["base"]
web = ["base", "web"]
ml = ["base", "ml"]
systems = ["base", "systems"]
mobile = ["base", "mobile"]
# Agent group -> manifest basenames (without `.toml`). When no stack is chosen
# (power user / --profile=full / non-interactive), ALL manifests install.
[agent-set]
base = ["architect", "critic", "validator", "researcher", "code-implementer", "security-auditor"]
web = ["code-implementer-typescript", "frontend-validator", "validator-api", "validator-doc", "researcher-web", "researcher-code"]
ml = ["ml-implementer", "ml-researcher", "modal-runner", "cost-guardian", "fal-ai-runner", "code-implementer-python", "validator-benchmark"]
systems = ["code-implementer-rust", "code-implementer-go", "infra-implementer", "infra-implementer-cicd", "infra-implementer-container", "infra-implementer-iac", "infra-implementer-secrets", "validator-version"]
mobile = ["code-implementer-swift", "code-implementer-flutter", "frontend-validator"]

162
bin/kei
View file

@ -8,7 +8,23 @@
# kei # splash → claude (interactive REPL) # kei # splash → claude (interactive REPL)
# kei --no-splash # skip splash → exec claude # kei --no-splash # skip splash → exec claude
# kei --status # status only, don't launch claude # kei --status # status only, don't launch claude
# kei [args...] # splash → claude args... (forwarded verbatim) # kei message ... # inter-session mailbox (send/inbox/list) — see kei-message.sh
# kei configure # re-pick stack profile + opt-in hook packs
# kei pick # interactive picker → set primary → launch it
# kei agent <name> "<task>" # invoke agent, backend from DNA → primary
# kei agent --on=<backend> <name> "<task>" # override backend
# kei run-via <backend> <name> "<task>" # invoke agent on explicit backend
# # backends: claude grok agy copilot kimi codex
# # `kei run-via list` shows install status + agents
# kei primary [<backend>] # get/set primary LLM provider (DNA fallback)
# kei mcp-wire [<cli>] # wire kei-mcp into a CLI's MCP config + hook setup
# # (Phase C cross-CLI policy enforcement)
# kei mcp-wire --list # show enforcement tier per CLI
# kei limits # probe each CLI's subscription quota (best-effort)
# # (4 of 5 CLIs have no public API — honest report)
# kei onboard # post-install wizard (pick primary + mcp-wire + check)
# kei --on=<backend> # one-shot launch of <backend> (does not change primary)
# kei [args...] # splash → exec primary CLI (default: claude)
# #
# The splash shows: substrate version, agent count, last sleep run, # The splash shows: substrate version, agent count, last sleep run,
# active sessions (kei-ping). Press any key to skip the dwell. # active sessions (kei-ping). Press any key to skip the dwell.
@ -17,26 +33,128 @@
set -e set -e
# --- subcommand dispatch (before splash) ---------------------------------
# `kei message ...` → mailbox CLI
# `kei configure` → hook/stack re-picker
# `kei agent ...` → DNA-resolved agent (manifest provider → primary → claude)
# `kei run-via ...` → explicit-backend agent invocation
# `kei primary ...` → get/set primary LLM provider
# rest = splash + launch claude (legacy primary).
case "${1:-}" in
message|msg|m)
shift
exec "$HOME/.claude/scripts/kei-message.sh" "$@"
;;
configure|config|reconfigure)
shift
exec "$HOME/.claude/scripts/kei-configure.sh" "$@"
;;
agent)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" "$@"
;;
run-via|via|agent-via)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" "$@"
;;
primary)
shift
exec "$HOME/.claude/scripts/kei-agent-cli.sh" primary "$@"
;;
pick)
shift
exec "$HOME/.claude/scripts/kei-pick.sh" "$@"
;;
mcp-wire|wire)
shift
exec "$HOME/.claude/scripts/kei-mcp-wire.sh" "$@"
;;
limits|quota|usage)
shift
exec "$HOME/.claude/scripts/kei-limits.sh" "$@"
;;
onboard|setup|wizard)
shift
exec "$HOME/.claude/scripts/kei-onboard.sh" "$@"
;;
esac
# --- one-shot --on=<backend> override (does not write primary.toml) -------
ONESHOT_BACKEND=""
for arg in "$@"; do
case "$arg" in
--on=*) ONESHOT_BACKEND="${arg#--on=}" ;;
esac
done
# --- args ---------------------------------------------------------------- # --- args ----------------------------------------------------------------
SPLASH=1 SPLASH=1
STATUS_ONLY=0 STATUS_ONLY=0
PASSTHROUGH=() PASSTHROUGH=()
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--on=*) ;; # already captured in ONESHOT_BACKEND; don't forward
--no-splash) SPLASH=0 ;; --no-splash) SPLASH=0 ;;
--status) STATUS_ONLY=1; SPLASH=1 ;; --status) STATUS_ONLY=1; SPLASH=1 ;;
*) PASSTHROUGH+=("$arg") ;; *) PASSTHROUGH+=("$arg") ;;
esac esac
done done
# --- locate claude on PATH ----------------------------------------------- # --- resolve primary backend ---------------------------------------------
CLAUDE_BIN="$(command -v claude 2>/dev/null || true)" # Order: --on=<backend> override → ~/.claude/config/primary.toml → claude.
if [ -z "$CLAUDE_BIN" ] && [ "$STATUS_ONLY" = "0" ]; then resolve_primary() {
echo "error: 'claude' not on PATH. Install Claude Code first:" >&2 if [ -n "$ONESHOT_BACKEND" ]; then printf '%s\n' "$ONESHOT_BACKEND"; return; fi
echo " curl -fsSL https://claude.ai/install.sh | sh" >&2 if [ -n "${KEI_PRIMARY:-}" ]; then printf '%s\n' "$KEI_PRIMARY"; return; fi
local cfg="$HOME/.claude/config/primary.toml"
if [ -f "$cfg" ]; then
awk -F'=' '/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}' "$cfg"
return
fi
printf 'claude\n'
}
# Map backend name → executable. Mirrors scripts/kei-agent-cli.sh::backend_bin.
backend_bin_for() {
case "$1" in
claude) echo "claude" ;;
grok) echo "grok" ;;
agy|antigravity) echo "agy" ;;
copilot) echo "copilot" ;;
kimi) echo "kimi" ;;
codex) echo "codex" ;;
*) return 1 ;;
esac
}
PRIMARY="$(resolve_primary)"
PRIMARY_CLI="$(backend_bin_for "$PRIMARY")" || {
echo "error: unknown primary backend: $PRIMARY" >&2
exit 2
}
PRIMARY_BIN="$(command -v "$PRIMARY_CLI" 2>/dev/null || true)"
if [ -z "$PRIMARY_BIN" ] && [ "$STATUS_ONLY" = "0" ]; then
echo "error: primary backend '$PRIMARY' → '$PRIMARY_CLI' not on PATH." >&2
case "$PRIMARY" in
claude) echo " install: curl -fsSL https://claude.ai/install.sh | sh" >&2 ;;
grok) echo " install: see https://x.ai/grok" >&2 ;;
copilot) echo " install: npm i -g @github/copilot" >&2 ;;
kimi) echo " install: uv tool install kimi-cli" >&2 ;;
agy) echo " install: see https://antigravity.dev" >&2 ;;
codex) echo " install: see https://github.com/openai/codex" >&2 ;;
esac
echo " or: kei pick (interactive picker to choose + set primary)" >&2
echo " or: kei primary <backend> (set a different default)" >&2
exit 127 exit 127
fi fi
# Legacy var name for splash code below.
CLAUDE_BIN="$PRIMARY_BIN"
# --- read state ---------------------------------------------------------- # --- read state ----------------------------------------------------------
AGENTS_DIR="${HOME}/.claude/agents" AGENTS_DIR="${HOME}/.claude/agents"
SYNC_DIR="${HOME}/.claude/memory/sync-repo" SYNC_DIR="${HOME}/.claude/memory/sync-repo"
@ -52,12 +170,10 @@ agent_count() {
# Profile from .installed marker file # Profile from .installed marker file
profile_name() { profile_name() {
local f="${AGENTS_DIR}/_primitives/.installed" # install.sh stamps the chosen profile here; .installed only holds primitive
if [ -f "$f" ]; then # names (so the old `grep ^profile .installed` always returned "?").
grep -E "^profile" "$f" 2>/dev/null | head -1 | awk -F= '{gsub(/[ "]/,"",$2); print $2}' || echo "?" local f="$HOME/.claude/.kei-profile"
else if [ -f "$f" ]; then head -1 "$f"; else echo "?"; fi
echo "?"
fi
} }
# Last Phase B sleep timestamp # Last Phase B sleep timestamp
@ -100,13 +216,14 @@ splash() {
sl="$(last_sleep_run)" sl="$(last_sleep_run)"
as="$(active_sessions)" as="$(active_sessions)"
# Only color if stdout is a tty # Only color if stdout is a tty. Brand palette: голубой (sky-blue) + жёлтый (gold).
local C0= C1= C2= C3= local C0= C1= C2= C3= CV=
if [ -t 1 ]; then if [ -t 1 ]; then
C0=$'\033[0m' C0=$'\033[0m'
C1=$'\033[1;36m' # cyan-bold C1=$'\033[1;38;5;39m' # голубой (sky-blue) — logo
C2=$'\033[0;36m' # cyan C2=$'\033[1;38;5;220m' # жёлтый (gold) — brand line
C3=$'\033[2m' # dim C3=$'\033[2;38;5;39m' # dim blue — separators
CV=$'\033[1;38;5;220m' # жёлтый — field values
fi fi
cat <<EOF cat <<EOF
@ -118,12 +235,13 @@ ${C1} ██╔═██╗ ██╔══╝ ██║╚════█
${C1} ██║ ██╗███████╗██║███████║███████╗██║${C0} ${C1} ██║ ██╗███████╗██║███████║███████╗██║${C0}
${C1} ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝${C0} ${C1} ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝${C0}
${C2} KeiSeiKit · substrate v0.16${C0} ${C2} KeiSeiKit · substrate v0.45${C0}
${C3} ─────────────────────────────────────${C0} ${C3} ─────────────────────────────────────${C0}
profile : ${p} primary CLI : ${CV}${PRIMARY}${C0}
agents : ${ac} profile : ${CV}${p}${C0}
last sleep run : ${sl} agents : ${CV}${ac}${C0}
active sessions: ${as} last sleep run : ${CV}${sl}${C0}
active sessions: ${CV}${as}${C0}
${C3} ─────────────────────────────────────${C0} ${C3} ─────────────────────────────────────${C0}
EOF EOF

View file

@ -48,7 +48,14 @@ done
# fallback to cortex for compat with v0.16 default behaviour. # fallback to cortex for compat with v0.16 default behaviour.
prompt_profile() { prompt_profile() {
if [ -n "$PROFILE" ]; then return 0; fi if [ -n "$PROFILE" ]; then return 0; fi
if [ ! -t 0 ] || [ ! -t 1 ]; then PROFILE="cortex"; return 0; fi # Interactive iff stdin is a terminal. NOT stdout: web-install.sh tees stdout
# to a logfile (pipe), so -t 1 is false even in an interactive curl|bash.
# Prompts print to the terminal via tee; the menu reads from stdin.
# Non-interactive (CI / piped, no controlling terminal) → minimal: fast,
# no 105-crate compile, can't half-fail. Matches install.sh's own default
# (was "cortex" here → divergent install vs direct install.sh). Opt up with
# --profile=cortex/full-hub.
if [ ! -t 0 ]; then PROFILE="minimal"; return 0; fi
cat <<'WIZARD' cat <<'WIZARD'
╔═══════════════════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════════════════╗
@ -170,9 +177,14 @@ fi
log "checkout: $KIT_DIR" log "checkout: $KIT_DIR"
# --- 5. run install ------------------------------------------------------ # --- 5. run install ------------------------------------------------------
log "running ./install.sh --profile=$PROFILE $YES_FLAG ${EXTRA_FLAGS[*]:-}" log "running install.sh --profile=$PROFILE $YES_FLAG ${EXTRA_FLAGS[*]:-}"
cd "$KIT_DIR" cd "$KIT_DIR"
./install.sh --profile="$PROFILE" $YES_FLAG "${EXTRA_FLAGS[@]:+${EXTRA_FLAGS[@]}}" # Defensive: invoke via `bash` not `./install.sh` because GitHub's contents
# API does NOT preserve the executable bit on `gh api -X PUT` updates
# (only the git Data API does). Older clones may have install.sh with
# mode 644 even though the source repo has it 755. `bash <file>` works
# regardless of file mode. Verified incident 2026-05-26 prod-curl test.
bash ./install.sh --profile="$PROFILE" $YES_FLAG "${EXTRA_FLAGS[@]:+${EXTRA_FLAGS[@]}}"
# --- 6. post-install verification ---------------------------------------- # --- 6. post-install verification ----------------------------------------
KEI_BIN="$HOME/.claude/agents/_primitives/_rust/target/release" KEI_BIN="$HOME/.claude/agents/_primitives/_rust/target/release"
@ -192,6 +204,25 @@ log ""
log "===========================================================================" log "==========================================================================="
log "DONE — KeiSeiKit installed (profile: $PROFILE)" log "DONE — KeiSeiKit installed (profile: $PROFILE)"
log "===========================================================================" log "==========================================================================="
# v0.45: post-install onboarding wizard.
# Auto-triggers if stdin is a TTY (real terminal). Wizard itself re-checks
# and exits cleanly if non-interactive — so curl|bash one-liner runs work too.
ONBOARD_SH="$HOME/.claude/scripts/kei-onboard.sh"
if [ -x "$ONBOARD_SH" ] && [ -t 0 ] && [ "${KEI_NO_ONBOARD:-0}" != "1" ]; then
log ""
log "Starting post-install onboarding (pick primary CLI + wire MCP)..."
log "Skip with KEI_NO_ONBOARD=1; re-run anytime with 'kei onboard'."
log ""
"$ONBOARD_SH" || log "(onboarding exited non-zero; re-run with 'kei onboard')"
else
log ""
log "Post-install wizard skipped (no TTY or KEI_NO_ONBOARD=1)."
log "Run interactively to configure primary CLI:"
log " kei onboard # full wizard"
log " kei pick # just pick primary"
log " kei mcp-wire # wire MCP into installed CLIs"
fi
log "" log ""
log "Next steps:" log "Next steps:"
log " - Open a new shell so PATH picks up ~/.cargo/bin and the kei-* binaries." log " - Open a new shell so PATH picks up ~/.cargo/bin and the kei-* binaries."

View file

@ -0,0 +1,178 @@
# Cross-CLI policy enforcement
> *Same safety rules. Any LLM CLI. Three honesty tiers.*
KeiSeiKit's safety hooks (`no-github-push`, `safety-guard`, `destructive-guard`,
`citation-verify`, `numeric-claims-guard`) originally fired only inside Claude
Code's `PreToolUse` pipeline. Phase C extends enforcement to other CLIs —
but the strength of enforcement depends on what each CLI permits.
## The 3-tier honesty model
| Tier | What it means | CLIs |
|---|---|---|
| **TIER 1 — full native** | Tool-call enforcement at the CLI's own hook layer. Same as Claude. | claude, **grok** |
| **TIER 2 — MCP-wrapped** | Native shell disabled at launch; agent forced to use our policy-gated `kei_bash`/`kei_edit`/`kei_write` MCP tools. | **copilot** |
| **TIER 3 — advisory** | CLI can't disable native shell; we register kei-mcp and instruct the agent to prefer `kei_*` tools, but enforcement is prompt-level only. | **agy, kimi** |
For patent-sensitive or production-PR work — stick to TIER 1 (claude or grok).
## How to wire
One command sets up enforcement for whichever CLIs you have installed:
```bash
kei mcp-wire # detect + wire all installed CLIs
kei mcp-wire grok # wire one CLI
kei mcp-wire --dry-run # preview config changes without writing
kei mcp-wire --list # show enforcement tier per CLI
```
The orchestrator is idempotent — running twice produces the same config.
## What `kei mcp-wire` writes
### claude (TIER 1 — already enforced)
No-op. Native PreToolUse hooks already gate every tool call. `kei mcp-wire claude`
prints the optional `mcpServers` snippet you can add to
`~/.claude/settings.json` if you want claude to also see `spawn_agent` for
sub-agent dispatch.
### grok (TIER 1 — port our hooks)
Writes `~/.grok/settings.json` `hooks.PreToolUse` block:
- `Bash` matcher → `no-github-push.sh` + `safety-guard.sh` + `destructive-guard.sh`
- `Edit` matcher → `citation-verify.sh` + `numeric-claims-guard.sh`
- `Write` matcher → `citation-verify.sh` + `numeric-claims-guard.sh`
Plus registers kei-mcp with `GROKCODE=1` env (so kei-mcp's policy chain skips
duplicate enforcement when invoked via Grok — your native hooks already fired).
xAI's Grok uses the same JSON input contract as Claude Code's PreToolUse, so
our hook scripts run unchanged. Identical enforcement to claude.
### copilot (TIER 2 — disable native shell, force MCP)
Writes `~/.copilot/mcp-config.json` registering kei-mcp. To activate enforcement,
launch copilot with `--excluded-tools='shell'`:
```bash
alias copilot='copilot --excluded-tools=shell'
```
The agent will have NO native shell tool, only kei-mcp's `kei_bash`
which runs the policy chain before execution. `kei_edit` / `kei_write`
similarly gate file mutations.
### agy / kimi (TIER 3 — advisory)
Writes their MCP config (`~/.gemini/config/mcp_config.json` for agy,
`~/.kimi/mcp.json` for kimi) registering kei-mcp.
**The honest part:** these CLIs do NOT have a way to disable their native
shell. The agent CAN reach for native bash regardless of what we tell it.
The system prompt nudges it toward `kei_bash`, but a determined or careless
agent can bypass.
For patent-sensitive work — **don't use agy or kimi as orchestrator**.
Use them for analysis / brainstorming / no-side-effect tasks only.
## Internals
### policy-chain.toml (SSoT)
One file declares which hooks gate which tool, for all CLIs that go through
the MCP layer:
```toml
# ~/.claude/hooks/_lib/policy-chain.toml
[bash]
chain = ["no-github-push.sh", "safety-guard.sh", "destructive-guard.sh"]
[edit]
chain = ["citation-verify.sh", "numeric-claims-guard.sh"]
[write]
chain = ["citation-verify.sh", "numeric-claims-guard.sh"]
```
To add a hook: append its basename. The hook script must already exist in
`~/.claude/hooks/` and follow the standard PreToolUse contract (read JSON
on stdin with `.tool_name` + `.tool_input`, return exit 0 = pass / 2 = block).
### kei-mcp built-in tools
`kei-mcp` (Rust MCP server at `_primitives/_rust/kei-mcp/`) exposes 4
built-in tools across two source files (both bypass the atom-discovery
loop in `handlers/tools.rs`):
In `handlers/tools.rs`:
- `spawn_agent(name, task, on?)` — invokes a KeiSeiKit agent on any backend
In `handlers/safe_tools.rs` (Phase C, v0.40+):
- `kei_bash(command, cwd?)` — runs `[bash]` chain → executes
- `kei_edit(file_path, old_string, new_string)` — runs `[edit]` chain → edits
- `kei_write(file_path, content)` — runs `[write]` chain → writes
The chain runs against the same hook scripts Claude uses; identical input
shape, identical decisions. On block, the hook's stderr surfaces as the MCP
error message so the calling agent sees exactly why.
**v0.42 hardening** (post 4-CLI re-audit, supersedes v0.41):
- **Fail-CLOSED everywhere** — missing config, missing hook, OR empty
section (`[bash]/[edit]/[write]` with no entries) all refuse to run.
Tests / dev can opt in via `KEI_POLICY_CHAIN_OPTIONAL=1`.
- **Symlink-safe path guard**`kei_edit` / `kei_write` canonicalize the
FULL path (resolving any leaf symlink to its real target) and reject
if the leaf itself is a symlink for a not-yet-existent file. Fixes the
v0.41 CRITICAL bypass where `ln -s ~/.ssh/keys ./x; kei_write x` would
follow the link.
- **$PWD-only default root** — `allowed_roots` defaults to current working
directory only. Was: `$PWD` + entire `$HOME` — too permissive, agent
could overwrite `~/.claude/hooks/*` (self-neuter) or `~/.zshrc` (RCE on
next shell). Operators who need broader access set `KEI_ALLOWED_ROOTS`.
- **Denylist extended** — system dirs (`/etc/`, `/usr/`, `/System/`,
`/var/`, `/root/`, `/bin/`, `/sbin/`); credential stores (`~/.ssh/`,
`~/.aws/`, `~/.gnupg/`, `~/.config/gcloud/`, `~/.cargo/credentials`,
`~/.docker/config.json`, `~/.kube/`); substrate dirs (`~/.claude/`,
`~/.grok/`, `~/.gemini/`, `~/.copilot/`, `~/.kimi/`); exact shell-init
files (`.zshrc`, `.bashrc`, `.profile`, `.zshenv`, `.gitconfig`, ...).
- **Async file I/O in load_chain**`policy-chain.toml` now read via
`tokio::fs` (was: blocking `std::fs` froze worker on slow mounts).
- **Process-group kill on hooks too** — hook subprocesses get
`process_group(0)` and `killpg(SIGKILL)` on timeout. Was: only the bash
action got this; hook grandchildren orphaned.
- **CLAUDECODE/GROKCODE design note** — documented as perf/UX
optimization, NOT a security boundary (env-controllable parent → confused
deputy is already-game-over scenario).
### Double-enforcement guard
If kei-mcp is invoked from a process where `$CLAUDECODE=1` or `$GROKCODE=1`,
it SKIPS its hook chain — the CLI's native hooks already fired. This is set
automatically by `kei mcp-wire claude` / `kei mcp-wire grok`. On copilot /
agy / kimi the env is unset → chain runs.
## Verification
```bash
# All 4 built-ins must list:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| kei-mcp | jq -r '.result.capabilities'
# Block test (kei_bash refuses forbidden command):
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"kei_bash","arguments":{"command":"git push https://github.com/x/y.git main"}}}' \
| kei-mcp 2>&1 | grep "RULE 0.1" # expects: BLOCK — RULE 0.1 NO GITHUB PUSH
# Pass test:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"kei_bash","arguments":{"command":"echo OK"}}}' \
| kei-mcp | tail -1 | jq -r '.result.content[0].text' # expects: OK
```
## Related
- [Multi-CLI agent invocation](./multi-cli-agents.md) — DNA-resolved agent dispatch
- `kei-mcp` source: `_primitives/_rust/kei-mcp/src/handlers/safe_tools.rs`
- Policy SSoT: `hooks/_lib/policy-chain.toml`
- Wire scripts: `scripts/kei-mcp-wire*.sh`

View file

@ -28,12 +28,23 @@ All hooks live under `hooks/` directory. Format: `| Hook Name | Event | Severity
- **remind (exit 0 + stderr on trigger)** — passive reminder - **remind (exit 0 + stderr on trigger)** — passive reminder
- **advisory** — informational, never blocks - **advisory** — informational, never blocks
### Hook packs (opt-in posture)
A fresh install activates **only the `safety` pack** (plus cosmetic/infra hooks).
Discipline packs are opt-in, chosen during onboarding (step 6) or later via
`kei configure`. SSoT for pack membership + stack profiles is
`_primitives/hook-packs.toml`. Packs: `safety` (always on), `evidence`,
`observability`, `epistemic`, `orchestration`, `git-guard` (opt-in only),
`stack-rust` (only under the `systems` stack profile). Discipline hooks also
respect runtime toggling via `KEI_DISABLED_HOOKS` / `KEI_HOOK_PROFILE` (see the
`hooks-control` skill).
### Core Safety Hooks ### Core Safety Hooks
| Hook | Event | Severity | Purpose | Bypass Env | | Hook | Event | Severity | Purpose | Bypass Env |
|------|-------|----------|---------|-----------| |------|-------|----------|---------|-----------|
| no-github-push.sh | PreToolUse:Bash | block | Prevent pushing KeiTech patent IP to github.com — destroys priority date | KEI_NO_GITHUB_PUSH_BYPASS | | no-github-push.sh | PreToolUse:Bash | block | Block accidental push / repo-create to github.com (opt-in; for code kept on a private remote) | KEI_NO_GITHUB_PUSH_BYPASS |
| no-python-without-approval.sh | PreToolUse:Bash | block | Enforce RULE 0.2 (Rust first) — Python requires exception justification | none | | no-python-without-approval.sh | PreToolUse:Bash | block | Optional Rust-first policy — Python requires explicit justification (opt-in, stack-gated) | none |
| rust-first.sh | UserPromptSubmit | remind | Remind about Rust-first default for new work | none | | rust-first.sh | UserPromptSubmit | remind | Remind about Rust-first default for new work | none |
| secrets-pre-guard.sh | PreToolUse:Edit\|Write | block | Detect hardcoded API keys, tokens, private keys before commit | KEI_SECRETS_GUARD_BYPASS | | secrets-pre-guard.sh | PreToolUse:Edit\|Write | block | Detect hardcoded API keys, tokens, private keys before commit | KEI_SECRETS_GUARD_BYPASS |
| destructive-guard.sh | PreToolUse:Bash | block | Block dangerous commands (rm -rf /, git reset --hard main, truncate) | none | | destructive-guard.sh | PreToolUse:Bash | block | Block dangerous commands (rm -rf /, git reset --hard main, truncate) | none |

View file

@ -0,0 +1,225 @@
# Multi-CLI agent invocation
> *Cross-LLM agent execution. Same agent definition, different backend.*
> *Same DNA, swap the brain. KeiSeiKit is no longer Claude-Code-only.*
KeiSeiKit agents are markdown files. Any LLM CLI that takes a prompt can
host them. Three call shapes:
```bash
kei agent <name> "<task>" # DNA-resolved (manifest → primary → claude)
kei agent --on=<backend> <name> "<task>" # override DNA
kei run-via <backend> <name> "<task>" # explicit backend (no DNA lookup)
```
## Backends — smoke-tested 2026-05-26
| Backend | CLI | Flag | Smoke | Notes |
|----------|-----------|--------------|-------|-------|
| claude | `claude` | `-p` | ✅ | Claude Code, native `--agent` flag |
| grok | `grok` | `--print` | ✅ | xAI Grok Build TUI, native `--agent` flag |
| agy | `agy` | `--print` | ✅ | Google Antigravity (Gemini models). Alias: `antigravity` |
| copilot | `copilot` | `--prompt` | ✅ | GitHub Copilot CLI (`@github/copilot`) |
| kimi | `kimi` | TUI-only | ⚠ | No print mode — launcher saves prompt to tmpfile + opens TUI for paste. `kimi acp` JSON-RPC integration is future work. |
| codex | `codex` | `-p` | — | OpenAI Codex (register-only; not installed locally) |
Run `kei run-via list` to see installed backends, current primary, and agent names.
## DNA — agent prefers a provider
Add `provider` to the agent manifest:
```toml
# _manifests/my-agent.toml
name = "my-agent"
provider = "grok" # preferred backend; optional
model = "grok-2" # advisory; informs choice but not yet sent through
```
The assembler emits it into frontmatter:
```yaml
---
name: my-agent
provider: grok
---
```
Resolution order (each falls through if previous returns nothing):
1. `--on=<backend>` flag on the command line
2. `provider:` field in agent manifest
3. `~/.claude/config/primary.toml` (set via `kei primary <backend>`)
4. Default: `claude`
## Primary — your default LLM
```bash
kei primary # show current primary (and fallback)
kei primary grok # set default to Grok
kei primary claude # back to Claude Code
```
`kei primary` writes `~/.claude/config/primary.toml`. Any agent without
its own `provider:` field will resolve to this. This is the lever to
"swap out Claude Code as the primary shell" — set primary to grok, and
every `kei agent <name>` runs on Grok.
## Usage examples
```bash
# DNA mode (manifest's provider, or primary, or claude):
kei agent critic "review src/auth.rs"
# Override DNA — try the same agent on a different model for a second opinion:
kei agent --on=grok critic "review src/auth.rs"
kei agent --on=agy critic "review src/auth.rs"
kei agent --on=copilot critic "review src/auth.rs"
# Explicit backend, no DNA lookup (legacy):
kei run-via grok critic "review src/auth.rs"
# Point at an arbitrary agent file:
kei agent --on=grok --file=/tmp/my-agent.md "do the thing"
# Native --agent flag (grok/claude only):
KEI_NATIVE_AGENT=1 kei agent critic "review src/auth.rs"
```
## How it works
1. Resolves backend from DNA (see above).
2. Reads `~/.claude/agents/<agent-name>.md` (assembler-generated prompt).
3. Strips YAML frontmatter.
4. Composes with task: `<agent prompt>\n\n---\n\nTASK FOR THIS RUN:\n<task>`.
5. Execs the backend's non-interactive CLI with the composed prompt.
No agent file is modified. No new tokens are issued — subscription
authentication is whatever each CLI uses (its own login / config dir).
## When to use each
This is a tool, not a recommendation. Each backend has different
strengths; the substrate is agnostic about which you pick. Pick by:
- **Familiarity** — the CLI you already use day-to-day.
- **Subscription cost** — burn the one with cheaper marginal cost first.
- **Specific feature** — e.g. `grok --agent` for native sub-agent
switching mid-conversation; `agy --sandbox` for terminal restriction.
- **Independent second opinion** — same agent, different model, see if
conclusions diverge.
## Orchestrator picker — `kei` no longer hardcodes claude
Without args, `kei` reads `~/.claude/config/primary.toml` and execs that CLI.
The picker lets you change it interactively:
```bash
kei pick # interactive menu → set primary → launch it
kei # splash → exec the configured primary
kei --on=grok # one-shot launch of grok (does NOT change primary)
kei primary grok # set default to grok (no launch)
kei primary # show current primary
```
The splash shows `primary CLI: <backend>` so you always know which orchestrator
will start. If the chosen primary isn't installed, `kei` prints the install
command and offers `kei pick` as recovery.
## Cross-CLI sub-agent spawn via MCP — `spawn_agent`
`kei-mcp` exposes a built-in `spawn_agent` MCP tool. Any CLI that connects
to it as an MCP client can invoke KeiSeiKit agents on any backend, no matter
what the orchestrator is:
```jsonrpc
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "spawn_agent",
"arguments": {
"name": "critic",
"task": "review src/auth.rs for race conditions",
"on": "grok"
}
}
}
```
Internally `spawn_agent` shells out to `kei-agent-cli.sh` with the same DNA
resolution as `kei agent`. The `on` argument is optional — without it, the
backend is picked from the agent's manifest, then `primary.toml`, then claude.
**Why this matters:** Claude Code has a native `Agent` tool for sub-agent
spawning. Grok / Antigravity / Copilot / Kimi do NOT have that surface
natively — but they all support MCP. With `spawn_agent` exposed via kei-mcp,
**every backend that speaks MCP gets KeiSeiKit's sub-agent capability**. So
when Grok is your orchestrator, it can still spawn `critic` on Claude (or
`code-implementer` on Antigravity, or anything else) — the orchestrator
choice no longer caps your sub-agent surface.
Wire kei-mcp into the orchestrator's MCP config (each CLI has its own):
| CLI | MCP config |
|---|---|
| claude | `~/.claude/settings.json` `mcpServers` block |
| grok | `~/.grok/config.json` (or check `grok --help`) |
| agy | `~/.antigravity/mcp.json` (check `agy plugin list`) |
| copilot | `~/.copilot/mcp.json` (check `copilot --help`) |
| kimi | `kimi mcp add` subcommand |
Point each at `<kit>/_primitives/_rust/target/release/kei-mcp` (built via
`cargo build -p kei-mcp --release`).
## Rule enforcement — see also: cross-CLI policy
**Phase C delivered**: KeiSeiKit's safety hooks now have a 3-tier enforcement
model across CLIs. See [cross-cli-policy.md](./cross-cli-policy.md) for the
full matrix and `kei mcp-wire` setup. Short version: TIER 1 (full native)
on claude+grok, TIER 2 (MCP-wrapped) on copilot, TIER 3 (advisory) on agy+kimi.
## Rule enforcement caveat (READ THIS — pre-Phase-C view)
KeiSeiKit hooks (`numeric-claims-guard`, `citation-verify`, `no-github-push`,
`safety-guard`, `push-to-main`, etc.) are **Claude Code-side**:
`PreToolUse:Bash` / `:Edit` / `:Write` events that fire inside Claude Code's
process. They do **not** propagate to grok / agy / copilot / kimi.
That means:
- **Prompt-level rules** (the agent's instructions inside the `.md`) DO
carry through — the agent reads Constructor Pattern, Evidence Grading,
No Hallucination, etc. as part of its system prompt on any backend.
- **Tool-level enforcement** (hard-deny on `git push github.com`,
citation guard, etc.) only applies on the **claude** backend. Other
backends' tool surfaces are governed by THEIR own hooks/policies.
If you need true rule-enforcement on a non-claude backend, the path is
the **MCP server** (`_primitives/_rust/kei-mcp/`): registers KeiSeiKit
primitives as MCP tools that the other CLI invokes. Tool-side policies
travel with the MCP wrapper, not with the CLI.
## Adding a new backend
1. Add a `[backend.<name>]` table to `_primitives/cli-backends.toml`.
2. Add a case arm in `scripts/kei-agent-cli.sh` `backend_bin()` and
`backend_invoke()` for the new CLI's print-flag.
3. Add a row to the smoke-test table above (state PASS/FAIL/PARTIAL).
## What it is NOT
- Not a router — picks no backend for you; you (or DNA) ask, it dispatches.
- Not a federation — each backend runs independently with its own
context; there is no cross-backend state.
- Not a rule-enforcement layer — hooks only fire on the claude backend
(see caveat above). For non-claude rule enforcement use MCP server.
- Not a wrapper around the backend's tool surface — what the CLI can
do (Bash, file edits, MCP, etc.) is determined by that CLI, not
KeiSeiKit. The substrate only ships the prompt.
## Related
- `_primitives/_rust/kei-llm-router/` — Beta-posterior router for
*programmatic* model selection inside Rust code (a different layer).
- `_primitives/_rust/kei-mcp/` — MCP server that exposes KeiSeiKit
primitives to ANY MCP-compatible client (Cursor / Continue / Zed /
Aider / Cline / Windsurf / OpenClaw).

View file

@ -0,0 +1,32 @@
# policy-chain.toml — SSoT for which hooks gate which MCP tool.
#
# Consumed by `kei-mcp::handlers::safe_tools` to enforce KeiSeiKit's safety
# rules on non-Claude CLIs (Grok / Agy / Copilot / Kimi) via the
# `kei_bash` / `kei_edit` / `kei_write` MCP tools.
#
# Hooks live in ~/.claude/hooks/ (overridable via $KEI_HOOKS_DIR).
# Exit codes: 0 = pass, 2 = block, other non-zero = treat as block + log.
# The dispatcher iterates `chain` IN ORDER and aborts on first non-zero.
#
# Constructor Pattern: ONE chain for all CLIs. Per-CLI override deferred
# until proven necessary. To extend, append a hook basename (no .sh) to
# the relevant chain — the hook script must already exist in ~/.claude/hooks/.
[bash]
chain = [
"no-github-push.sh",
"safety-guard.sh",
"destructive-guard.sh",
]
[edit]
chain = [
"citation-verify.sh",
"numeric-claims-guard.sh",
]
[write]
chain = [
"citation-verify.sh",
"numeric-claims-guard.sh",
]

View file

@ -65,4 +65,14 @@ if [ -n "$TOOL_USE_ID" ] && [ -f "$ACTIVE_FILE" ]; then
mv "$ACTIVE_FILE.tmp" "$ACTIVE_FILE" 2>/dev/null || true mv "$ACTIVE_FILE.tmp" "$ACTIVE_FILE" 2>/dev/null || true
fi fi
# v0.40 root-cause fix: remove the .task-${id}.start marker that task-timer.sh
# wrote on agent_spawn. Without this, completed sub-agents leave stale markers
# in ~/.claude/memory/time-metrics/ which inflate the pet's running-agent
# counter (🤖N). Previously task-timer was the only writer + the 2h stale
# filter in keisei-pet.sh was the only cleanup; that left up-to-2h dead
# markers visible on every status refresh.
if [ -n "$TOOL_USE_ID" ] && [ "$TOOL_USE_ID" != "unknown" ]; then
rm -f "$HOME/.claude/memory/time-metrics/.task-${TOOL_USE_ID}.start" 2>/dev/null || true
fi
exit 0 exit 0

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "alignment-check" || exit 0; fi
# ALIGNMENT CHECK HOOK # ALIGNMENT CHECK HOOK
# Fires on UserPromptSubmit when comparison/experiment keywords detected. # Fires on UserPromptSubmit when comparison/experiment keywords detected.
# THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment. # THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment.

View file

@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "chat-numeric-postflag" || exit 0; fi
# chat-numeric-postflag.sh — Stop warn (RULE 0.18 chat-output) # chat-numeric-postflag.sh — Stop warn (RULE 0.18 chat-output)
# #
# Reads the session transcript, extracts the last assistant message, # Reads the session transcript, extracts the last assistant message,

View file

@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "chat-numeric-prewarn" || exit 0; fi
# chat-numeric-prewarn.sh — UserPromptSubmit remind (RULE 0.18 chat-output) # chat-numeric-prewarn.sh — UserPromptSubmit remind (RULE 0.18 chat-output)
# #
# Detects time/cost/effort keywords in the user's prompt and injects an # Detects time/cost/effort keywords in the user's prompt and injects an

View file

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# DELETED — 2026-05-02 # DELETED — 2026-05-02
# Reasons: # Reasons:
# 1. Hardcoded path leak: /Users/denis/projects/ai machine learning/error-patterns.json # 1. Hardcoded absolute path leak (machine-specific, author-local)
# 2. RULE 0.2 violation: used python3 for JSON parsing # 2. Language-policy violation: used python3 for JSON parsing
# 3. No-op on every machine except original author's # 3. No-op on every machine except the original author's
# Removed from settings-snippet.json PostToolUse matcher "*" block. # Removed from settings-snippet.json PostToolUse matcher "*" block.
exit 0 exit 0

View file

@ -1,4 +1,7 @@
#!/bin/bash #!/bin/bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "citation-verify" || exit 0; fi
# PreToolUse(Edit|Write) — block unverified academic citations # PreToolUse(Edit|Write) — block unverified academic citations
# #
# Rule 0.5 NO HALLUCINATION enforcer. # Rule 0.5 NO HALLUCINATION enforcer.

41
hooks/first-run-onboard.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/sh
# first-run-onboard — on the FIRST Claude Code session after a KeiSeiKit install,
# inject a one-time POST-INSTALL ONBOARDING checklist so Claude walks the user
# through ALL setup in order (agents → sleep → cortex), instead of leaving each
# as a separate thing the user has to discover. Event: SessionStart (stdout is
# injected into session context). Fires ONCE (marker), then silent forever.
# Reset / re-run: rm ~/.claude/.kei-firstrun-shown
MARKER="$HOME/.claude/.kei-firstrun-shown"
[ -f "$MARKER" ] && exit 0
[ -d "$HOME/.claude/agents" ] || exit 0
PROJ="$HOME/Projects"
[ -d "$PROJ" ] || PROJ="$HOME/projects"
[ -d "$PROJ" ] || PROJ="$HOME/Projects"
# Cortex step only if the cortex daemon primitive landed (cortex / full* profiles).
CORTEX_STEP=""
if [ -d "$HOME/.claude/agents/_primitives/_rust/kei-cortex" ] \
|| [ -x "$HOME/.claude/agents/_primitives/_rust/target/release/kei-cortex" ]; then
CORTEX_STEP=" 3. /cortex-setup — cortex daemon + UI (token, whisper, model, bundle)
"
fi
cat <<EOF
[KeiSeiKit · FIRST-RUN ONBOARDING] The substrate is installed. Before other
work, proactively walk the user through this one-time setup, step by step, in
order — confirm each step with the user, then run it:
1. /onboard $PROJ/* — scan every project, detect stack, create a
project-specialist agent per project (delegates to /new-agent).
2. /sleep-setup — nightly memory (REM) consolidation. Recommend
local-only mode (runs on this Mac, no remote/cloud git needed) unless the
user wants the cloud agent (needs a cloud-reachable private git repo).
$CORTEX_STEP
Start with step 1. Offer to do them one after another as a guided flow; the user
can skip any. Do NOT make the user discover these on their own.
EOF
: > "$MARKER"
exit 0

View file

@ -3,7 +3,7 @@
# Bypass: GRAPH_EXPORT_BYPASS=1 # Bypass: GRAPH_EXPORT_BYPASS=1
INTERVAL="${KEI_GRAPH_EXPORT_INTERVAL_S:-5}" INTERVAL="${KEI_GRAPH_EXPORT_INTERVAL_S:-5}"
OUT="${KEI_GRAPH_VIZ_DIR:-$HOME/Projects/lbm-graph-viz}/data-runtime.js" OUT="${KEI_GRAPH_VIZ_DIR:-$HOME/.local/share/kei/graph-viz}/data-runtime.js"
BIN="$(command -v kei-graph-export 2>/dev/null || echo "$HOME/.cargo/bin/kei-graph-export")" BIN="$(command -v kei-graph-export 2>/dev/null || echo "$HOME/.cargo/bin/kei-graph-export")"
[ -x "$BIN" ] || exit 0 [ -x "$BIN" ] || exit 0

57
hooks/mailbox-inject.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/sh
# mailbox-inject — pull-inbox for kei-message. On every UserPromptSubmit, inject
# any messages addressed to THIS session (by cwd-basename or the broadcast
# channel "all") that arrived since last turn, into the session context, so
# Claude sees what other sessions sent. Per-session read cursor dedups; first
# turn starts fresh (no history dump). Never blocks (always exit 0).
# Event: UserPromptSubmit. Bypass: KEI_MAILBOX_BYPASS=1.
[ "${KEI_MAILBOX_BYPASS:-}" = "1" ] && exit 0
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
SID=$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
[ -n "$CWD" ] || CWD="$PWD"
me="$(basename "$CWD")"
MBOX="$HOME/.claude/mailbox"
LOG="$MBOX/messages.jsonl"
mkdir -p "$MBOX"
CUR="$MBOX/.cursor-${SID:-$me}"
# Highest id currently in the bus (0 if the log doesn't exist yet / is empty).
if [ -f "$LOG" ]; then
maxid=$(jq -s 'map(.id) | max // 0' "$LOG" 2>/dev/null || echo 0)
else
maxid=0
fi
[ -n "$maxid" ] || maxid=0
# First fire for this session: record baseline cursor, show nothing. Done even
# when the bus is still empty — so messages that arrive AFTER this point (but
# before the session's next turn) are not missed.
if [ ! -f "$CUR" ]; then
echo "$maxid" > "$CUR"
exit 0
fi
# Nothing to read yet.
[ -f "$LOG" ] || { echo "$maxid" > "$CUR"; exit 0; }
last=$(cat "$CUR" 2>/dev/null || echo 0)
case "$last" in ''|*[!0-9]*) last=0 ;; esac
new=$(jq -r --argjson last "$last" --arg me "$me" '
select(.id > $last)
| select(.to == $me or .to == "all")
| select(.from != $me)
| " • \(.from) -> \(.to): \(.body)"' "$LOG" 2>/dev/null)
# Advance cursor past everything seen this turn.
echo "$maxid" > "$CUR"
if [ -n "$new" ]; then
printf '[kei mailbox] new message(s) for this session (%s):\n%s\n (reply: kei message send --to <name> "...")\n' "$me" "$new"
fi
exit 0

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "no-downgrade" || exit 0; fi
# RULE -1 NO DOWNGRADE / CONSTRUCTIVE ONLY (2026-04-15 LOCK) enforcement. # RULE -1 NO DOWNGRADE / CONSTRUCTIVE ONLY (2026-04-15 LOCK) enforcement.
# #
# Detects downgrade-style phrases in Write/Edit content without accompanying # Detects downgrade-style phrases in Write/Edit content without accompanying

View file

@ -1,9 +1,10 @@
#!/bin/sh #!/bin/sh
# no-github-push.sh — PreToolUse:Bash hard deny (RULE 0.1 NO GITHUB PUSH) # no-github-push.sh — PreToolUse:Bash hard deny.
# #
# Blocks any Bash command that would push code to github.com. # Blocks any Bash command that would push code or create a repo on github.com.
# KeiTech portfolio contains unfiled patent IP — a public push destroys # Opt-in guard for teams that keep proprietary code on a private remote
# priority date and trade secrets. Irrecoverable. # (Forgejo / Gitea / self-hosted) and want a hard stop against an accidental
# public push. Off by default in the public kit — enable it in onboarding.
# #
# Exit codes: # Exit codes:
# 0 = pass (command is safe) # 0 = pass (command is safe)
@ -69,18 +70,16 @@ fi
# --- Block ------------------------------------------------------------------ # --- Block ------------------------------------------------------------------
cat >&2 <<'EOF' cat >&2 <<'EOF'
[no-github-push] BLOCK — RULE 0.1 NO GITHUB PUSH [no-github-push] BLOCK — push to github.com is disabled by this guard.
KeiTech portfolio contains unfiled patent IP. Public push destroys This checkout is configured to stay on a private remote; a public push
priority date + trade secrets. Irrecoverable. could expose code you intend to keep private.
Use a private remote instead (Forgejo, Gitea, self-hosted): Use your private remote instead (Forgejo, Gitea, self-hosted):
git remote set-url origin ssh://git@<private-host>/<user>/<repo>.git git remote set-url origin ssh://git@<private-host>/<user>/<repo>.git
git push origin <branch> git push origin <branch>
Bypass (visible, per-call): Bypass (visible, per-call):
Set env KEI_NO_GITHUB_PUSH_BYPASS=1 before the command. Set env KEI_NO_GITHUB_PUSH_BYPASS=1 before the command.
You must also add confirmation phrase: "yes, push patent code to github"
+ "confirm publication" in the session turn.
EOF EOF
exit 2 exit 2

View file

@ -1,4 +1,7 @@
#!/bin/bash #!/bin/bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "no-python-without-approval" || exit 0; fi
# Hard block on python/python3/python2 invocations in Bash tool. # Hard block on python/python3/python2 invocations in Bash tool.
# RULE 0.2 (Rust First) — Python requires explicit architectural reason. # RULE 0.2 (Rust First) — Python requires explicit architectural reason.
# Claude кroнически нарушает RULE 0.2 inline-вызовами python3 для мелких расчётов. # Claude кroнически нарушает RULE 0.2 inline-вызовами python3 для мелких расчётов.

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "numeric-claims-guard" || exit 0; fi
# RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims # RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims
# without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat). # without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat).
# #
@ -26,7 +29,7 @@ fi
# - "N MB/GB/LOC/tests/crates/atomars" # - "N MB/GB/LOC/tests/crates/atomars"
# - "~$N", "$N/mo" # - "~$N", "$N/mo"
# - "Nm Ns", "займёт N", "should take N" # - "Nm Ns", "займёт N", "should take N"
NUMERIC_PATTERN='(~\s*[0-9]+(\.[0-9]+)?\s*(min|minute|hour|hr|day|week|month|sec|second|MB|GB|KB|LOC|line|test|crate|atomar|%|µs|ms|ns|TPS|req/s)|[0-9]+m\s*[0-9]+s|\$[0-9]+(\.[0-9]+)?(/(mo|hr|day|run))?|~\s*\$[0-9]+|should take|will take|takes about|займёт|за ~|estimated at|ETA[: ]|approximately\s+[0-9])' NUMERIC_PATTERN='(~\s*[0-9]+(\.[0-9]+)?\s*(min|minute|hour|hr|day|week|month|sec|second|MB|GB|KB|LOC|line|test|crate|atomar|%|µs|ms|ns|TPS|req/s)|[0-9]+m\s*[0-9]+s|\$[0-9]+\.[0-9]+|\$[0-9]+/(mo|hr|day|run)|\$[0-9]{2,}|~\s*\$[0-9]+|should take|will take|takes about|займёт|за ~|estimated at|ETA[: ]|approximately\s+[0-9])'
# Markers that satisfy the rule # Markers that satisfy the rule
EVIDENCE_PATTERN='\[(REAL|FROM-JOURNAL|ESTIMATE-HTC)[: ]' EVIDENCE_PATTERN='\[(REAL|FROM-JOURNAL|ESTIMATE-HTC)[: ]'

View file

@ -1,4 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Runtime gate (hooks-control skill / KEI_DISABLED_HOOKS / KEI_HOOK_PROFILE).
_KEI_LIB="$(dirname "$0")/_lib/gate.sh"; if [ -r "$_KEI_LIB" ]; then . "$_KEI_LIB"; kei_hook_gate "rust-first" || exit 0; fi
# RULE 0.2 — RUST FIRST reminder hook. # RULE 0.2 — RUST FIRST reminder hook.
# #
# Fires on UserPromptSubmit. Detects keywords indicating language choice # Fires on UserPromptSubmit. Detects keywords indicating language choice

View file

@ -36,36 +36,67 @@ if [ -n "$transcript" ] && [ -f "$transcript" ]; then
cp -f "$transcript" "$dest" 2>/dev/null || true cp -f "$transcript" "$dest" 2>/dev/null || true
fi fi
# Best-effort ingest — advisory only; never blocks the session from ending. # RECURRENCE FIX 2026-05-26: 18MB+ transcripts caused 4-minute "Recombobulating…"
# hangs at session end. The three heavy ops below now run async-detached:
# hook returns immediately, ingest / scan / sync grind in background.
# Raw JSONL is already saved sync (line 36) — no data loss; only the
# index/embedding step is deferred. kei-memory ingest is idempotent on
# session_id so partial runs are safe.
bg_log="${HOME}/.claude/memory/traces/session-end.bg.log"
mkdir -p "$(dirname "$bg_log")" 2>/dev/null || true
# Portable timeout (macOS has no `timeout` / `gtimeout` by default).
# Fallback: perl alarm. Final fallback: no timeout (rely on detach).
kei_with_timeout() {
secs="$1"; shift
if command -v timeout >/dev/null 2>&1; then
timeout "$secs" "$@"
elif command -v gtimeout >/dev/null 2>&1; then
gtimeout "$secs" "$@"
elif command -v perl >/dev/null 2>&1; then
perl -e 'alarm shift @ARGV; exec @ARGV' "$secs" "$@"
else
"$@"
fi
}
# Best-effort ingest — async-detached.
if command -v kei-memory >/dev/null 2>&1 && [ -f "$dest" ]; then if command -v kei-memory >/dev/null 2>&1 && [ -f "$dest" ]; then
kei-memory ingest \ (
--session-id "$session_id" \ kei_with_timeout 90 kei-memory ingest \
--transcript "$dest" \ --session-id "$session_id" \
>/dev/null 2>&1 || true --transcript "$dest" \
>>"$bg_log" 2>&1 \
|| printf '[%s] kei-memory ingest timeout/fail for %s\n' \
"$(date +%H:%M:%S)" "$session_id" >>"$bg_log"
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi fi
# Wave 25 — frustration-matrix scan: regex+firmware classifier produces a # Wave 25 — frustration-matrix scan.
# JSONL of per-line affect hits per session, much smaller than the full
# transcript. Cloud REM agent reads the affect file instead of 80MB JSONL.
# Silent no-op when the primitive is absent.
if command -v frustration-matrix >/dev/null 2>&1; then if command -v frustration-matrix >/dev/null 2>&1; then
affect_dir="${HOME}/.claude/memory/affect" affect_dir="${HOME}/.claude/memory/affect"
mkdir -p "$affect_dir" 2>/dev/null || true mkdir -p "$affect_dir" 2>/dev/null || true
affect_out="${affect_dir}/${session_id}.jsonl" affect_out="${affect_dir}/${session_id}.jsonl"
frustration-matrix scan \ (
--root "$traces_dir" \ kei_with_timeout 60 frustration-matrix scan \
--since 1d \ --root "$traces_dir" \
--format jsonl \ --since 1d \
--output "$affect_out" \ --format jsonl \
>/dev/null 2>&1 || true --output "$affect_out" \
>>"$bg_log" 2>&1 || true
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi fi
# v0.11 sleep-sync (RULE 0.15) — push traces to the user's memory-repo so a # v0.11 sleep-sync (RULE 0.15) — push traces to memory-repo.
# cloud agent can consolidate them overnight. Silent no-op when the primitive
# is absent or the user hasn't opted in via /sleep-setup.
sleep_sync="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh" sleep_sync="${HOME}/.claude/agents/_primitives/kei-sleep-sync.sh"
if [ -x "$sleep_sync" ]; then if [ -x "$sleep_sync" ]; then
"$sleep_sync" >/dev/null 2>&1 || true (
kei_with_timeout 120 "$sleep_sync" >>"$bg_log" 2>&1 || true
) </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi fi
exit 0 exit 0

View file

@ -37,6 +37,8 @@ source "$LIB_DIR/lib-log.sh"
source "$LIB_DIR/lib-backup.sh" source "$LIB_DIR/lib-backup.sh"
# shellcheck source=install/lib-profile.sh # shellcheck source=install/lib-profile.sh
source "$LIB_DIR/lib-profile.sh" source "$LIB_DIR/lib-profile.sh"
# shellcheck source=install/lib-packs.sh
source "$LIB_DIR/lib-packs.sh"
# shellcheck source=install/lib-args.sh # shellcheck source=install/lib-args.sh
source "$LIB_DIR/lib-args.sh" source "$LIB_DIR/lib-args.sh"
# shellcheck source=install/lib-menu.sh # shellcheck source=install/lib-menu.sh
@ -147,6 +149,11 @@ case "$PROFILE" in
;; ;;
esac esac
say "profile: $PROFILE" say "profile: $PROFILE"
# Stamp the chosen profile so `kei` splash + tools can show it (bin/kei reads this).
mkdir -p "$HOME_DIR/.claude" 2>/dev/null || true
printf '%s\n' "$PROFILE" > "$HOME_DIR/.claude/.kei-profile" 2>/dev/null || true
# Stamp the kit checkout dir so `kei configure` can re-source the libs later.
printf '%s\n' "$KIT_DIR" > "$HOME_DIR/.claude/.kei-kit-dir" 2>/dev/null || true
# --- resolve profile -> primitive list (UNCONDITIONAL, SSoT) ------------- # --- resolve profile -> primitive list (UNCONDITIONAL, SSoT) -------------
# Must run BEFORE any reader of PROFILE_PRIMS: the --no-execute plan block # Must run BEFORE any reader of PROFILE_PRIMS: the --no-execute plan block
@ -203,6 +210,7 @@ if ! printf '%s\n' "$CONFIRM_INPUT" | show_confirm_screen "$CONFIRM_LABEL"; then
fi fi
# --- execute install phases ---------------------------------------------- # --- execute install phases ----------------------------------------------
kei_banner
setup_target_dirs setup_target_dirs
scaffold_memory_index scaffold_memory_index
install_blocks install_blocks
@ -233,7 +241,11 @@ fi
# target/release/ regardless of profile (lib-substrate.sh), so PATH wiring # target/release/ regardless of profile (lib-substrate.sh), so PATH wiring
# is meaningful for every profile except minimal-without-prebuilt. # is meaningful for every profile except minimal-without-prebuilt.
if [ "$NO_PATHWAY" != "1" ]; then if [ "$NO_PATHWAY" != "1" ]; then
if [ "$WITH_PATHWAY" = "1" ] || { [ -t 0 ] && [ -t 1 ]; }; then # Gate on interactive stdin only — NOT -t 1: curl|bash tees stdout to a
# logfile, so -t 1 is false even interactively. Requiring it skipped PATH
# wiring (~/.claude/bin), so the `kei` entry-point was not found after a
# curl|bash install. (Same tee/-t1 trap as the onboarding gates.)
if [ "$WITH_PATHWAY" = "1" ] || [ -t 0 ]; then
pathway_install pathway_install
fi fi
fi fi

View file

@ -6,7 +6,7 @@ STR_WELCOME_TITLE="KeiSeiKit · Exobrain installer"
STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools" STR_WELCOME_TAGLINE="Portable Rust agent substrate for AI coding tools"
# Onboarding wizard steps # Onboarding wizard steps
STR_ONBOARDING_INTRO="Onboarding wizard (5 steps)" STR_ONBOARDING_INTRO="Onboarding wizard (6 steps)"
STR_PICK_LANGUAGE="Choose interface language:" STR_PICK_LANGUAGE="Choose interface language:"
STR_PICK_TRANSPORT="Choose connection transport:" STR_PICK_TRANSPORT="Choose connection transport:"
STR_PICK_PROVIDER="Choose provider within" STR_PICK_PROVIDER="Choose provider within"
@ -19,7 +19,7 @@ STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+key)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)" STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)" STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)" STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth subscription (ChatGPT)" STR_TR_SUBSCRIPTION="Subscription login (Claude Code / ChatGPT — no API key)"
# Auth collection # Auth collection
STR_AUTH_INTRO="Auth for" STR_AUTH_INTRO="Auth for"
@ -40,3 +40,25 @@ STR_MENU_CONFIRM="Confirm selection?"
# Preflight warnings # Preflight warnings
STR_PREFLIGHT_FAILED="Preflight failed — provider may not work." STR_PREFLIGHT_FAILED="Preflight failed — provider may not work."
STR_PREFLIGHT_CONTINUE="Continue anyway? [y/N]" STR_PREFLIGHT_CONTINUE="Continue anyway? [y/N]"
# Wizard explanations + input validation
STR_PICK_INVALID="please type one of the numbers shown"
STR_EXPLAIN_TRANSPORT="How the agents reach the AI. subscription = log in with your plan, no API key (Claude Code is option 1); direct-api = your own API key. Press Enter for the default."
STR_EXPLAIN_PROVIDER="Which AI service. Option 1 is the recommended default — press Enter."
STR_EXPLAIN_MODEL="Default model the agents use. Option 1 is the recommended default — press Enter."
# Stack profile + hook-pack picker (step 6)
STR_PICK_STACK="Pick your stack profile (selects which hooks + agents install):"
STR_PICK_STACK_PROMPT="[1-5, default 1=minimal]: "
STR_STACK_MINIMAL="safety hooks + core agents only"
STR_STACK_WEB="TS/frontend agents + evidence, observability"
STR_STACK_ML="ML/data agents + evidence, observability, epistemic"
STR_STACK_SYSTEMS="Rust/Go agents + Rust-first + evidence, observability"
STR_STACK_MOBILE="Swift/Flutter agents + evidence, observability"
STR_PACK_INTRO="Optional discipline packs (safety is always on):"
STR_PACK_EVIDENCE="force evidence markers on numeric/cost claims"
STR_PACK_OBS="task timing, session dumps, agent telemetry"
STR_PACK_EPI="no-downgrade + alignment + recurrence reminders"
STR_PACK_ORCH="multi-agent fork logging + orchestrator git checks"
STR_PACK_GIT="block git push to github (for private-remote teams)"
STR_PACK_ENABLE="enable? [y/N]: "

View file

@ -19,7 +19,7 @@ STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+ключ)"
STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)" STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)"
STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)" STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)"
STR_TR_PROXY="Прокси (LiteLLM/OpenRouter)" STR_TR_PROXY="Прокси (LiteLLM/OpenRouter)"
STR_TR_SUBSCRIPTION="OAuth-подписка (ChatGPT)" STR_TR_SUBSCRIPTION="Вход по подписке (Claude Code / ChatGPT — без API-ключа)"
# Сбор ключей # Сбор ключей
STR_AUTH_INTRO="Аутентификация для" STR_AUTH_INTRO="Аутентификация для"
@ -40,3 +40,9 @@ STR_MENU_CONFIRM="Подтвердить выбор?"
# Preflight-предупреждения # Preflight-предупреждения
STR_PREFLIGHT_FAILED="Preflight упал — провайдер может не работать." STR_PREFLIGHT_FAILED="Preflight упал — провайдер может не работать."
STR_PREFLIGHT_CONTINUE="Продолжить всё равно? [y/N]" STR_PREFLIGHT_CONTINUE="Продолжить всё равно? [y/N]"
# Пояснения мастера + валидация ввода
STR_PICK_INVALID="введите один из показанных номеров"
STR_EXPLAIN_TRANSPORT="Как агенты обращаются к ИИ. subscription = вход по подписке, без API-ключа (Claude Code — вариант 1); direct-api = свой API-ключ. Нажми Enter для варианта по умолчанию."
STR_EXPLAIN_PROVIDER="Какой ИИ-сервис. Вариант 1 — рекомендуемый по умолчанию, нажми Enter."
STR_EXPLAIN_MODEL="Модель, которую используют агенты. Вариант 1 — рекомендуемый по умолчанию, нажми Enter."

View file

@ -14,9 +14,18 @@
# when present. # when present.
install_manifests() { install_manifests() {
say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)" say "copying generic manifests -> $AGENTS_DIR/_manifests/ (skip if exists)"
local copied=0 skipped=0 f name t has_templates=0 # Stack filter: when a stack profile is chosen, install only its agent set.
# Empty allowlist (no stack / non-interactive) => install ALL (back-compat).
local allow=""
if command -v resolve_selected_agent_manifests >/dev/null 2>&1; then
allow="$(resolve_selected_agent_manifests)"
fi
local copied=0 skipped=0 filtered=0 f name t has_templates=0
for f in "$KIT_DIR/_manifests/"*.toml; do for f in "$KIT_DIR/_manifests/"*.toml; do
name="$(basename "$f")" name="$(basename "$f")"
if [ -n "$allow" ] && ! printf '%s\n' "$allow" | grep -qx "${name%.toml}"; then
filtered=$((filtered+1)); continue
fi
if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then if [[ -f "$AGENTS_DIR/_manifests/$name" ]]; then
skipped=$((skipped+1)) skipped=$((skipped+1))
else else
@ -24,7 +33,11 @@ install_manifests() {
copied=$((copied+1)) copied=$((copied+1))
fi fi
done done
say " copied $copied, skipped $skipped (already present)" if [ -n "$allow" ]; then
say " copied $copied, skipped $skipped, stack-filtered $filtered"
else
say " copied $copied, skipped $skipped (already present)"
fi
for t in "$KIT_DIR/_templates/"*.template; do for t in "$KIT_DIR/_templates/"*.template; do
[ -f "$t" ] && { has_templates=1; break; } [ -f "$t" ] && { has_templates=1; break; }

View file

@ -80,14 +80,29 @@ _mint_runner_token() {
printf '%s' "$token" printf '%s' "$token"
} }
# Internal: register act_runner with the local Forgejo. Writes ${DATA}/.runner. # v0.45 fix: brew installs `gitea-runner` (not `act_runner`); the binary is
# Args: <data_dir> <token>. # named `gitea-runner`. Resolver tries both names so future brew packaging
# changes don't re-break this. act_runner upstream and gitea-runner fork are
# functionally equivalent and both register with Forgejo.
_runner_bin() {
if command -v act_runner >/dev/null 2>&1; then
echo "act_runner"
elif command -v gitea-runner >/dev/null 2>&1; then
echo "gitea-runner"
else
return 1
fi
}
# Internal: register the runner with the local Forgejo. Writes ${DATA}/.runner.
_register_act_runner() { _register_act_runner() {
local data_dir="$1" local data_dir="$1"
local token="$2" local token="$2"
local label="self-hosted,macos-arm64,native" local label="self-hosted,macos-arm64,native"
local name="$(hostname -s)-keisei" local name="$(hostname -s)-keisei"
( cd "$data_dir" && act_runner register \ local runner
runner="$(_runner_bin)" || { err "no runner binary found (looked for act_runner + gitea-runner)"; return 1; }
( cd "$data_dir" && "$runner" register \
--no-interactive \ --no-interactive \
--instance http://127.0.0.1:3001 \ --instance http://127.0.0.1:3001 \
--token "$token" \ --token "$token" \
@ -97,12 +112,19 @@ _register_act_runner() {
# Public entry: install + register + bootstrap the runner. # Public entry: install + register + bootstrap the runner.
install_dev_hub_forgejo_runner() { install_dev_hub_forgejo_runner() {
say "installing dev-hub-forgejo-runner (act_runner)" say "installing dev-hub-forgejo-runner (Forgejo Actions runner)"
_require_forgejo_binary || return 1 _require_forgejo_binary || return 1
_require_forgejo_running || return 1 _require_forgejo_running || return 1
say "brew install act_runner" # Prefer the Forgejo-official runner; fall back to the gitea-runner fork
brew install act_runner # (which is what `brew install gitea-runner` actually provides today).
if ! _runner_bin >/dev/null 2>&1; then
say "brew install gitea-runner (Forgejo-compatible)"
brew install gitea-runner || {
warn "brew install gitea-runner failed — try 'brew tap actions/runner' for act_runner"
return 1
}
fi
local data_dir local data_dir
data_dir="$(_runner_data_dir)" data_dir="$(_runner_data_dir)"
@ -125,7 +147,9 @@ install_dev_hub_forgejo_runner() {
. "$KIT_DIR/install/lib-launchd.sh" . "$KIT_DIR/install/lib-launchd.sh"
install_service forgejo-runner install_service forgejo-runner
say "act_runner registered + running. Polling http://127.0.0.1:3001 for jobs." local runner_name
runner_name="$(_runner_bin 2>/dev/null || echo runner)"
say "$runner_name registered + running. Polling http://127.0.0.1:3001 for jobs."
} }
# Public entry: stop + unload the runner. Keeps ${DATA}/.runner so re-install # Public entry: stop + unload the runner. Keeps ${DATA}/.runner so re-install

View file

@ -97,11 +97,19 @@ _dhf_bootstrap_admin_user() {
local kc_token_svc kc_pass_svc local kc_token_svc kc_pass_svc
config="$(_dhf_app_ini)" config="$(_dhf_app_ini)"
username="${KEI_FORGEJO_ADMIN_USER:-${USER:-denis}}" username="${KEI_FORGEJO_ADMIN_USER:-${USER:-denis}}"
# Single-source Keychain service names (override per-host via env).
# Wizard MUST read identical names — see drive-import-wizard.sh.tmpl.
kc_token_svc="${KEI_FORGEJO_KC_TOKEN_SERVICE:-forgejo-api-token}" kc_token_svc="${KEI_FORGEJO_KC_TOKEN_SERVICE:-forgejo-api-token}"
kc_pass_svc="${KEI_FORGEJO_KC_PASS_SERVICE:-forgejo-admin-password}" kc_pass_svc="${KEI_FORGEJO_KC_PASS_SERVICE:-forgejo-admin-password}"
# Detection: any rows beyond header in `admin user list`?
# v0.45 fix: Forgejo on first install needs `migrate` to create the sqlite
# schema. Without it, `admin user create` fails with "no such table: user"
# (verified bug 2026-05-26 in prod curl|bash test). `migrate` is idempotent
# — safe to re-run.
if ! forgejo --config "$config" migrate 2>/dev/null; then
warn " → forgejo migrate failed; daemon may need restart before admin create"
fi
# Detection: any rows beyond header in `admin user list`? Now safe to
# parse since migrate has ensured the user table exists.
user_count="$(forgejo --config "$config" admin user list 2>/dev/null \ user_count="$(forgejo --config "$config" admin user list 2>/dev/null \
| tail -n +2 | grep -cv '^$' || echo 0)" | tail -n +2 | grep -cv '^$' || echo 0)"
if [ "$user_count" -gt 0 ]; then if [ "$user_count" -gt 0 ]; then
@ -160,6 +168,8 @@ _dhf_bootstrap_admin_user() {
# Public — install entry point. Called from install.sh primitives phase. # Public — install entry point. Called from install.sh primitives phase.
install_dev_hub_forgejo() { install_dev_hub_forgejo() {
say "[dev-hub-forgejo] install starting" say "[dev-hub-forgejo] install starting"
# shellcheck source=./lib-launchd.sh
. "$KIT_DIR/install/lib-launchd.sh" # install_service / detect_brew_prefix (was unsourced → command not found)
_dhf_check_brew || return 1 _dhf_check_brew || return 1
_dhf_brew_install || return 1 _dhf_brew_install || return 1
_dhf_ensure_data_dir || return 1 _dhf_ensure_data_dir || return 1

View file

@ -41,13 +41,38 @@ _dhz_check_go_runtime() {
fi fi
} }
# Step b — brew install zoekt (idempotent). # Step b — install zoekt. Zoekt is NOT in homebrew/core — try tap first,
# then fall back to building from source via Go (if installed). On total
# failure, skip cleanly rather than aborting the whole install.
# v0.45 fix: prior version errored hard ("No formula") and bailed the entire
# dev-hub install. Now degrades gracefully.
_dhz_brew_install() { _dhz_brew_install() {
say "installing zoekt via brew (idempotent)" say "installing zoekt (idempotent)"
if ! brew install zoekt; then if command -v zoekt-webserver >/dev/null 2>&1 && command -v zoekt-index >/dev/null 2>&1; then
err "brew install zoekt failed — see brew log above" say " → zoekt already installed; skipping"
return 1 return 0
fi fi
if brew install zoekt 2>/dev/null; then
say " → installed via brew core"
return 0
fi
if brew install sourcegraph/zoekt/zoekt 2>/dev/null \
|| brew install hyperdiscovery/zoekt/zoekt 2>/dev/null; then
say " → installed via tap"
return 0
fi
if command -v go >/dev/null 2>&1; then
say " → falling back to 'go install' from sourcegraph/zoekt"
if go install github.com/sourcegraph/zoekt/cmd/zoekt-webserver@latest \
&& go install github.com/sourcegraph/zoekt/cmd/zoekt-index@latest; then
say " → installed via go"
return 0
fi
fi
warn "zoekt unavailable: not in brew core/taps + no go fallback."
warn "Skipping zoekt service install. Other dev-hub services continue."
warn "To install later: brew install --HEAD sourcegraph/zoekt/zoekt"
return 2 # signal partial — caller treats as skip, not fatal
} }
# Step c — ensure data dir tree (+ index dir). # Step c — ensure data dir tree (+ index dir).
@ -128,6 +153,8 @@ _dhz_print_banner() {
# Public — install entry point. Called from install.sh primitives phase. # Public — install entry point. Called from install.sh primitives phase.
install_dev_hub_zoekt() { install_dev_hub_zoekt() {
say "[dev-hub-zoekt] install starting" say "[dev-hub-zoekt] install starting"
# shellcheck source=./lib-launchd.sh
. "$KIT_DIR/install/lib-launchd.sh" # install_service / detect_brew_prefix (was unsourced → command not found)
_dhz_check_brew || return 1 _dhz_check_brew || return 1
_dhz_check_go_runtime _dhz_check_go_runtime
_dhz_brew_install || return 1 _dhz_brew_install || return 1

View file

@ -27,14 +27,16 @@ install_hooks() {
say " installed $hook_count hook(s)" say " installed $hook_count hook(s)"
# v0.17 — shared hook library (gate.sh + test-gate.sh) # v0.17 — shared hook library (gate.sh + test-gate.sh)
# v0.40 — also copy *.toml files from _lib/ (policy-chain.toml for safe_tools).
if [ -d "$KIT_DIR/hooks/_lib" ]; then if [ -d "$KIT_DIR/hooks/_lib" ]; then
mkdir -p "$HOOKS_DIR/_lib" mkdir -p "$HOOKS_DIR/_lib"
local lib_count=0 lib_src lib_name local lib_count=0 lib_src lib_name
for lib_src in "$KIT_DIR/hooks/_lib/"*.sh; do for lib_src in "$KIT_DIR/hooks/_lib/"*.sh "$KIT_DIR/hooks/_lib/"*.toml; do
[ -f "$lib_src" ] || continue [ -f "$lib_src" ] || continue
lib_name="$(basename "$lib_src")" lib_name="$(basename "$lib_src")"
cp -f "$lib_src" "$HOOKS_DIR/_lib/$lib_name" cp -f "$lib_src" "$HOOKS_DIR/_lib/$lib_name"
chmod +x "$HOOKS_DIR/_lib/$lib_name" # chmod +x only for shell scripts; .toml stays read-only.
case "$lib_name" in *.sh) chmod +x "$HOOKS_DIR/_lib/$lib_name" ;; esac
lib_count=$((lib_count+1)) lib_count=$((lib_count+1))
done done
say " installed $lib_count hook library file(s) -> $HOOKS_DIR/_lib/" say " installed $lib_count hook library file(s) -> $HOOKS_DIR/_lib/"
@ -60,6 +62,10 @@ _jq_merge_hooks() {
| reduce ($add.hooks | keys[]) as $phase ($orig; | reduce ($add.hooks | keys[]) as $phase ($orig;
.hooks[$phase] = ( .hooks[$phase] = (
((.hooks[$phase] // []) + ($add.hooks[$phase] // [])) ((.hooks[$phase] // []) + ($add.hooks[$phase] // []))
# Normalize null/absent matcher to "" (Claude Code /doctor rejects null;
# pre-kit user hooks often have no matcher field) before group_by so
# null and "" collapse into one group.
| map(.matcher //= "")
| group_by(.matcher) | group_by(.matcher)
| map( | map(
.[0].matcher as $m .[0].matcher as $m
@ -98,20 +104,64 @@ _jq_merge_hooks() {
fi fi
} }
# Write a filtered copy of the snippet keeping only hook entries whose command
# basename is in the newline allowlist (plus the cosmetic pet hooks, always
# kept). Drops emptied matcher groups. Echoes the temp path. Arg: $1 = allowlist.
filter_snippet_by_packs() {
local allow="$1" snippet="$KIT_DIR/settings-snippet.json" tmp
tmp="$(mktemp -t kei-snippet.XXXXXX)"
jq --arg allow "$allow" '
def b: sub("^.*/"; "") | sub("\\.sh$"; "");
def keep($ok; $c): (($c | b) as $x | ($ok | index($x)) != null)
or ($c | test("keisei-pet")) or ($c | test("^CMD="));
($allow | split("\n") | map(select(length > 0))) as $ok
| .hooks |= with_entries(
.value |= ( map(.hooks |= map(select(keep($ok; .command))))
| map(select((.hooks | length) > 0)) )
)
' "$snippet" > "$tmp" || { err "snippet filter failed"; rm -f "$tmp"; return 1; }
printf '%s' "$tmp"
}
# Remove every kit-owned hook entry from an existing settings.json (ownership =
# basename in the full pack universe, plus pet hooks). Foreign hooks survive.
# Lets reconfigure REMOVE deselected hooks (the merge alone is additive-only).
# Args: $1 = target settings.json, $2 = newline list of all kit hook basenames.
prune_kit_hooks() {
local target="$1" universe="$2" tmp
tmp="$(mktemp "$target.XXXXXX")"
jq --arg universe "$universe" '
def b: sub("^.*/"; "") | sub("\\.sh$"; "");
def owned($kit; $c): (($c | b) as $x | ($kit | index($x)) != null)
or ($c | test("keisei-pet")) or ($c | test("^CMD="));
($universe | split("\n") | map(select(length > 0))) as $kit
| .hooks |= with_entries(
.value |= ( map(.hooks |= map(select(owned($kit; .command) | not)))
| map(select((.hooks | length) > 0)) )
)
' "$target" > "$tmp" && mv "$tmp" "$target" || { err "prune failed"; rm -f "$tmp"; return 1; }
}
activate_hooks() { activate_hooks() {
local snippet="$KIT_DIR/settings-snippet.json" local snippet="$KIT_DIR/settings-snippet.json"
local target="$HOME_DIR/.claude/settings.json" local target="$HOME_DIR/.claude/settings.json"
[ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; } [ -f "$snippet" ] || { warn "no snippet at $snippet"; return 0; }
local allow filtered
allow="$(resolve_selected_hook_basenames)"
filtered="$(filter_snippet_by_packs "$allow")" || return 1
if [ ! -f "$target" ]; then if [ ! -f "$target" ]; then
local tmp local tmp
tmp="$(mktemp "$target.XXXXXX")" tmp="$(mktemp "$target.XXXXXX")"
jq 'del(._comment)' "$snippet" > "$tmp" jq 'del(._comment)' "$filtered" > "$tmp"
mv "$tmp" "$target" mv "$tmp" "$target"
say "created $target from snippet (no prior settings.json)" rm -f "$filtered"
say "created $target from filtered snippet"
return 0 return 0
fi fi
backup_file "$target" backup_file "$target"
_jq_merge_hooks "$snippet" "$target" prune_kit_hooks "$target" "$(all_pack_basenames)"
_jq_merge_hooks "$filtered" "$target"
rm -f "$filtered"
} }
# Flag-or-prompt dispatcher, mirroring the v0.15 behavior: # Flag-or-prompt dispatcher, mirroring the v0.15 behavior:
@ -129,7 +179,7 @@ maybe_activate_hooks() {
elif [ ! -f "$settings_file" ]; then elif [ ! -f "$settings_file" ]; then
say "no existing settings.json; installing snippet" say "no existing settings.json; installing snippet"
activate_hooks && DID_ACTIVATE=1 activate_hooks && DID_ACTIVATE=1
elif [ -t 0 ] && [ -t 1 ]; then elif [ -t 0 ]; then # stdin-only: stdout may be tee'd in curl|bash
if [ "$COLOR" = "1" ]; then if [ "$COLOR" = "1" ]; then
printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] ' printf '\033[1;36m[install]\033[0m activate hooks now? [y/N] '
else else

View file

@ -1,21 +1,43 @@
# shellcheck shell=bash # shellcheck shell=bash
# lib-log.sh — say / warn / err with optional ANSI color. # lib-log.sh — say / warn / err with optional ANSI color + KeiSei banner.
# Honors NO_COLOR (no-color.org) and TTY detection on fd 1. # Honors NO_COLOR (no-color.org). Color is ON when stdout is a TTY OR a
# controlling terminal is reachable via /dev/tty — the latter matters under
# `curl|bash`, where web-install.sh tees stdout (so `-t 1` is false even in an
# interactive session, but the terminal is still there via /dev/tty).
# This `-t 1`-with-/dev/tty test is COLOR detection, NOT an interactivity gate
# (see ~/.claude/rules/tty-interactivity-gate.md) — it pairs with no `-t 0`.
# Sourced by install.sh; no top-level execution. # Sourced by install.sh; no top-level execution.
# ANSI on iff stdout is a TTY and NO_COLOR is unset. if { [ -t 1 ] || { : < /dev/tty; } 2>/dev/null; } && [ "${NO_COLOR:-}" = "" ]; then
if [ -t 1 ] && [ "${NO_COLOR:-}" = "" ]; then
COLOR=1 COLOR=1
else else
COLOR=0 COLOR=0
fi fi
# Brand palette: тёмно-жёлтый (gold) for [install], голубой (sky-blue) for KeiSei.
if [ "$COLOR" = "1" ]; then if [ "$COLOR" = "1" ]; then
say() { printf '\033[1;36m[install]\033[0m %s\n' "$*"; } KEI_GOLD=$'\033[38;5;178m' # тёмно-жёлтый — install prefix
KEI_BLUE=$'\033[38;5;39m' # голубой — logo / primitives
KEI_DIM=$'\033[2m'
KEI_RST=$'\033[0m'
say() { printf '%s[install]%s %s\n' "$KEI_GOLD" "$KEI_RST" "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; } err() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; }
else else
KEI_GOLD= KEI_BLUE= KEI_DIM= KEI_RST=
say() { printf '[install] %s\n' "$*"; } say() { printf '[install] %s\n' "$*"; }
warn() { printf '[warn] %s\n' "$*"; } warn() { printf '[warn] %s\n' "$*"; }
err() { printf '[error] %s\n' "$*" >&2; } err() { printf '[error] %s\n' "$*" >&2; }
fi fi
# KeiSei ASCII banner — голубой logo, shown once at install start.
kei_banner() {
printf '\n'
printf '%s ██╗ ██╗███████╗██╗███████╗███████╗██╗%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s ██║ ██╔╝██╔════╝██║██╔════╝██╔════╝██║%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s █████╔╝ █████╗ ██║███████╗█████╗ ██║%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s ██╔═██╗ ██╔══╝ ██║╚════██║██╔══╝ ██║%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s ██║ ██╗███████╗██║███████║███████╗██║%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚══════╝╚═╝%s\n' "$KEI_BLUE" "$KEI_RST"
printf '%s KeiSeiKit · installing%s\n\n' "$KEI_GOLD" "$KEI_RST"
}

View file

@ -17,8 +17,7 @@ menu_should_skip() {
[ -n "$ADD_LIST" ] && return 0 [ -n "$ADD_LIST" ] && return 0
[ -n "$REMOVE_NAME" ] && return 0 [ -n "$REMOVE_NAME" ] && return 0
[ "$LIST_MODE" = "1" ] && return 0 [ "$LIST_MODE" = "1" ] && return 0
[ ! -t 0 ] && return 0 [ ! -t 0 ] && return 0 # interactive stdin only; not -t 1 (curl|bash tees stdout)
[ ! -t 1 ] && return 0
return 1 return 1
} }
@ -38,7 +37,7 @@ menu_whiptail_profile() {
"ops" "+ 9 infra tools — provision, ssh-check, firewall-diff" OFF \ "ops" "+ 9 infra tools — provision, ssh-check, firewall-diff" OFF \
"dev" "+ 17 dev tools — kei-migrate, kei-memory, deep-sleep" OFF \ "dev" "+ 17 dev tools — kei-migrate, kei-memory, deep-sleep" OFF \
"mcp" "+ 10 MCP tools — kei-router, kei-sage, kei-auth, kei-pet" OFF \ "mcp" "+ 10 MCP tools — kei-router, kei-sage, kei-auth, kei-pet" OFF \
"cortex" "+ 11 cortex stack — kei-cortex daemon + cortex-ui" OFF \ "cortex" "+ 11 cortex stack — kei-cortex daemon + UI primitives" OFF \
"full" "+ all 62 primitives (~5 min, 380 MB)" OFF \ "full" "+ all 62 primitives (~5 min, 380 MB)" OFF \
"local-mirror" "dev hub: cortex + Forgejo + CI runner (+ 13 prims)" OFF \ "local-mirror" "dev hub: cortex + Forgejo + CI runner (+ 13 prims)" OFF \
"dashboard" "local-mirror + projects-index + Datasette (+ 16 prims)" OFF \ "dashboard" "local-mirror + projects-index + Datasette (+ 16 prims)" OFF \

View file

@ -45,12 +45,20 @@ onboarding_fallback_providers() {
printf "lmstudio-local\tlocal\tLM Studio (local)\t_\n" printf "lmstudio-local\tlocal\tLM Studio (local)\t_\n"
printf "litellm-proxy\tproxy\tLiteLLM proxy (keisei.app)\tKEI_LITELLM_KEY\n" printf "litellm-proxy\tproxy\tLiteLLM proxy (keisei.app)\tKEI_LITELLM_KEY\n"
printf "openrouter\tproxy\tOpenRouter\tOPENROUTER_API_KEY\n" printf "openrouter\tproxy\tOpenRouter\tOPENROUTER_API_KEY\n"
printf "claude-code\tsubscription\tClaude Code (subscription — your claude CLI, no API key)\t_\n"
printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n" printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n"
} }
# Уникальные транспорты — для первого экрана выбора. # Уникальные транспорты — для первого экрана выбора.
# Claude-Code-native kit → выводим subscription + direct-api ПЕРВЫМИ, чтобы
# рекомендованный путь (Claude Code, опция 1) был дефолтом. Остальные следом.
onboarding_list_transports() { onboarding_list_transports() {
onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u local all; all="$(onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u)"
local t
for t in subscription direct-api; do
printf '%s\n' "$all" | grep -qx "$t" && echo "$t"
done
printf '%s\n' "$all" | grep -vxE 'subscription|direct-api' || true
} }
# Провайдеры внутри транспорта. # Провайдеры внутри транспорта.

View file

@ -39,6 +39,8 @@ language = "$ONBOARDING_LANG"
transport = "$ONBOARDING_TRANSPORT" transport = "$ONBOARDING_TRANSPORT"
provider = "$ONBOARDING_PROVIDER" provider = "$ONBOARDING_PROVIDER"
default_model = "$ONBOARDING_MODEL" default_model = "$ONBOARDING_MODEL"
stack_profile = "$ONBOARDING_STACK"
enabled_packs = "$ONBOARDING_PACKS"
EOF EOF
# Override для kei-model-router (HIGH аудит-1). # Override для kei-model-router (HIGH аудит-1).

View file

@ -12,6 +12,70 @@
# - lib-i18n.sh: STR_* словарь + i18n_available_languages + i18n_load_lang # - lib-i18n.sh: STR_* словарь + i18n_available_languages + i18n_load_lang
# - lib-onboarding-registry.sh: списки провайдеров/моделей # - lib-onboarding-registry.sh: списки провайдеров/моделей
# Read a validated 1-based menu choice. Non-numeric or out-of-range input is
# rejected with a re-prompt instead of crashing: bash arithmetic $((ans-1))
# treats a non-numeric "ans" (e.g. the user typing "claude") as a variable name
# → "unbound variable" under `set -u`. $1=option count, $2=prompt.
# Echoes a number in [1,$1] on stdout; prompts/warnings go to stderr.
_onb_read_choice() {
local max="$1" prompt="$2" ans
while true; do
read -r -p "$prompt" ans
ans="${ans:-1}"
if [[ "$ans" =~ ^[0-9]+$ ]] && [ "$ans" -ge 1 ] && [ "$ans" -le "$max" ]; then
printf '%s' "$ans"; return 0
fi
printf ' ⚠ %s\n' "${STR_PICK_INVALID:-please enter a number from 1 to $max}" >&2
done
}
# Step 6 — pick a stack profile (selects which discipline hooks + agents
# install) then optionally toggle discipline packs the stack does not pull.
# Sets ONBOARDING_STACK + ONBOARDING_PACKS. Reuses _onb_read_choice + stack_packs
# (lib-packs.sh). Default = minimal (safety hooks + core agents only).
onboarding_pick_stack() {
echo "" >&2
printf '%s\n' "${STR_PICK_STACK:-Pick your stack profile (selects hooks + agents):}" >&2
local opts="minimal web ml systems mobile" i=1 o d ans
for o in $opts; do
case "$o" in
minimal) d="${STR_STACK_MINIMAL:-safety hooks + core agents only}" ;;
web) d="${STR_STACK_WEB:-TS/frontend agents + evidence, observability}" ;;
ml) d="${STR_STACK_ML:-ML/data agents + evidence, observability, epistemic}" ;;
systems) d="${STR_STACK_SYSTEMS:-Rust/Go agents + Rust-first + evidence, observability}" ;;
mobile) d="${STR_STACK_MOBILE:-Swift/Flutter agents + evidence, observability}" ;;
esac
printf ' %d) %-8s — %s\n' "$i" "$o" "$d" >&2
i=$((i+1))
done
ans="$(_onb_read_choice 5 "${STR_PICK_STACK_PROMPT:-[1-5, default 1=minimal]: }")"
ONBOARDING_STACK="$(echo "$opts" | cut -d' ' -f"$ans")"
[ -n "$ONBOARDING_STACK" ] || ONBOARDING_STACK="minimal"
# Offer discipline packs the chosen stack does not already enable.
local stackpacks p pd reply
stackpacks=" $(command -v stack_packs >/dev/null 2>&1 && stack_packs "$ONBOARDING_STACK") "
ONBOARDING_PACKS=""
printf '%s\n' "${STR_PACK_INTRO:-Optional discipline packs (safety is always on):}" >&2
for p in evidence observability epistemic orchestration git-guard; do
case "$stackpacks" in *" $p "*) continue ;; esac
case "$p" in
evidence) pd="${STR_PACK_EVIDENCE:-force evidence markers on numeric/cost claims}" ;;
observability) pd="${STR_PACK_OBS:-task timing, session dumps, agent telemetry}" ;;
epistemic) pd="${STR_PACK_EPI:-no-downgrade + alignment + recurrence reminders}" ;;
orchestration) pd="${STR_PACK_ORCH:-multi-agent fork logging + orchestrator git checks}" ;;
git-guard) pd="${STR_PACK_GIT:-block git push to github (for private-remote teams)}" ;;
esac
printf ' + %-13s — %s\n' "$p" "$pd" >&2
read -r -p " ${STR_PACK_ENABLE:-enable? [y/N]: }" reply
case "$reply" in y|Y|yes|YES) ONBOARDING_PACKS="$ONBOARDING_PACKS $p" ;; esac
done
ONBOARDING_PACKS="$(echo "$ONBOARDING_PACKS" | sed 's/^ *//;s/ *$//')"
if command -v say >/dev/null 2>&1; then
say "stack: $ONBOARDING_STACK packs: ${ONBOARDING_PACKS:-(stack defaults only)}"
fi
}
onboarding_pick_language() { onboarding_pick_language() {
local langs local langs
langs="$(i18n_available_languages 2>/dev/null)" langs="$(i18n_available_languages 2>/dev/null)"
@ -40,11 +104,10 @@ onboarding_pick_language() {
while IFS=$'\t' read -r code name; do while IFS=$'\t' read -r code name; do
[ -z "$code" ] && continue [ -z "$code" ] && continue
codes+=("$code") codes+=("$code")
printf " %2d) %s — %s\n" "$i" "$code" "$name" >&2 printf " %2d) ${KEI_BLUE:-}%s${KEI_RST:-}${KEI_GOLD:-}%s${KEI_RST:-}\n" "$i" "$code" "$name" >&2
i=$((i+1)) i=$((i+1))
done <<< "$langs" done <<< "$langs"
read -r -p "[1-${#codes[@]}, default 1=en]: " ans ans="$(_onb_read_choice "${#codes[@]}" "[1-${#codes[@]}, default 1=en]: ")"
ans="${ans:-1}"
ONBOARDING_LANG="${codes[$((ans-1))]:-en}" ONBOARDING_LANG="${codes[$((ans-1))]:-en}"
fi fi
command -v i18n_load_lang >/dev/null 2>&1 && i18n_load_lang "$ONBOARDING_LANG" command -v i18n_load_lang >/dev/null 2>&1 && i18n_load_lang "$ONBOARDING_LANG"
@ -76,6 +139,7 @@ onboarding_pick_transport() {
else else
echo "" >&2 echo "" >&2
echo "$prompt" >&2 echo "$prompt" >&2
echo " ${STR_EXPLAIN_TRANSPORT:-How the agents reach the AI. subscription = log in with your plan (no API key); direct-api = your own API key. Default is fine for most.}" >&2
local i=1 local i=1
declare -a opts=() declare -a opts=()
while IFS= read -r tr; do while IFS= read -r tr; do
@ -83,8 +147,7 @@ onboarding_pick_transport() {
echo " $i) $tr" >&2 echo " $i) $tr" >&2
i=$((i+1)) i=$((i+1))
done <<< "$transports" done <<< "$transports"
read -r -p "[1-${#opts[@]}, default 1]: " ans ans="$(_onb_read_choice "${#opts[@]}" "[1-${#opts[@]}, default 1]: ")"
ans="${ans:-1}"
ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}" ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}"
fi fi
} }
@ -111,6 +174,7 @@ onboarding_pick_provider() {
else else
echo "" >&2 echo "" >&2
echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2 echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2
echo " ${STR_EXPLAIN_PROVIDER:-Which AI service. Option 1 is the recommended default.}" >&2
declare -a ids=() declare -a ids=()
local i=1 local i=1
while IFS=$'\t' read -r id dn ae; do while IFS=$'\t' read -r id dn ae; do
@ -118,8 +182,7 @@ onboarding_pick_provider() {
echo " $i) $id$dn" >&2 echo " $i) $id$dn" >&2
i=$((i+1)) i=$((i+1))
done <<< "$rows" done <<< "$rows"
read -r -p "[1-${#ids[@]}, default 1]: " ans ans="$(_onb_read_choice "${#ids[@]}" "[1-${#ids[@]}, default 1]: ")"
ans="${ans:-1}"
ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}" ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}"
fi fi
} }
@ -135,6 +198,12 @@ onboarding_pick_model() {
local rows; rows=$(onboarding_models_for_provider "$lookup") local rows; rows=$(onboarding_models_for_provider "$lookup")
[ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n") [ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n")
# Single model → auto-select, no dead-end prompt (mirrors provider count==1).
if [ "$(printf '%s\n' "$rows" | grep -c .)" = "1" ]; then
ONBOARDING_MODEL=$(printf '%s\n' "$rows" | head -1 | awk -F'\t' '{print $1}')
return
fi
if command -v whiptail >/dev/null 2>&1; then if command -v whiptail >/dev/null 2>&1; then
local args=() local args=()
while IFS=$'\t' read -r id dn; do while IFS=$'\t' read -r id dn; do
@ -146,6 +215,7 @@ onboarding_pick_model() {
else else
echo "" >&2 echo "" >&2
echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2 echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2
echo " ${STR_EXPLAIN_MODEL:-Default model the agents use. Option 1 is the recommended default.}" >&2
declare -a ids=() declare -a ids=()
local i=1 local i=1
while IFS=$'\t' read -r id dn; do while IFS=$'\t' read -r id dn; do
@ -153,8 +223,7 @@ onboarding_pick_model() {
echo " $i) $id$dn" >&2 echo " $i) $id$dn" >&2
i=$((i+1)) i=$((i+1))
done <<< "$rows" done <<< "$rows"
read -r -p "[1-${#ids[@]}, default 1]: " ans ans="$(_onb_read_choice "${#ids[@]}" "[1-${#ids[@]}, default 1]: ")"
ans="${ans:-1}"
ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}" ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}"
fi fi
} }

View file

@ -20,6 +20,8 @@ ONBOARDING_LANG=""
ONBOARDING_TRANSPORT="" ONBOARDING_TRANSPORT=""
ONBOARDING_PROVIDER="" ONBOARDING_PROVIDER=""
ONBOARDING_MODEL="" ONBOARDING_MODEL=""
ONBOARDING_STACK=""
ONBOARDING_PACKS=""
declare -a ONBOARDING_AUTH_ENV_KEYS=() declare -a ONBOARDING_AUTH_ENV_KEYS=()
declare -a ONBOARDING_AUTH_ENV_VALUES=() declare -a ONBOARDING_AUTH_ENV_VALUES=()
@ -41,32 +43,36 @@ REGISTRY_MODELS="$KIT_DIR/_blocks/registries/models.toml"
onboarding_should_run() { onboarding_should_run() {
[ -f "$ONBOARDED_FLAG" ] && return 1 [ -f "$ONBOARDED_FLAG" ] && return 1
[ "${KEISEI_SKIP_ONBOARD:-}" = "1" ] && return 1 [ "${KEISEI_SKIP_ONBOARD:-}" = "1" ] && return 1
# Interactive iff stdin is a terminal. We deliberately do NOT require -t 1:
# the curl|bash bootstrapper (web-install.sh) tees stdout to a logfile, so
# -t 1 is false even in an interactive session. Prompts go to stderr, input
# reads from stdin — an interactive stdin is the only real requirement.
[ ! -t 0 ] && return 1 [ ! -t 0 ] && return 1
[ ! -t 1 ] && return 1
return 0 return 0
} }
# Оркестратор: 5 шагов + preflight + запись. # Оркестратор: 6 шагов + preflight + запись.
onboarding_run() { onboarding_run() {
onboarding_should_run || return 0 onboarding_should_run || return 0
if command -v say >/dev/null 2>&1; then if command -v say >/dev/null 2>&1; then
say "${STR_ONBOARDING_INTRO:-Onboarding wizard (5 steps)}" say "${STR_ONBOARDING_INTRO:-Onboarding wizard (6 steps)}"
else else
echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (5 steps)} ──" >&2 echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (6 steps)} ──" >&2
fi fi
onboarding_pick_language onboarding_pick_language
onboarding_pick_transport onboarding_pick_transport
onboarding_pick_provider onboarding_pick_provider
onboarding_pick_model onboarding_pick_model
onboarding_pick_stack
# Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей. # Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей.
if command -v preflight_run >/dev/null 2>&1; then if command -v preflight_run >/dev/null 2>&1; then
if ! preflight_run "$ONBOARDING_PROVIDER"; then if ! preflight_run "$ONBOARDING_PROVIDER"; then
echo "" >&2 echo "" >&2
echo "${STR_PREFLIGHT_FAILED:-Preflight failed — provider may not work.}" >&2 echo "${STR_PREFLIGHT_FAILED:-Preflight failed — provider may not work.}" >&2
if [ -t 0 ] && [ -t 1 ]; then if [ -t 0 ]; then # stdin-only: stdout may be tee'd in curl|bash
read -r -p " ${STR_PREFLIGHT_CONTINUE:-Continue anyway? [y/N]} " _ans read -r -p " ${STR_PREFLIGHT_CONTINUE:-Continue anyway? [y/N]} " _ans
case "$_ans" in case "$_ans" in
y|Y|yes|да|Да) y|Y|yes|да|Да)
@ -92,4 +98,9 @@ onboarding_run() {
say " ${STR_DONE_CONFIG:-config:} $ONBOARDING_CONFIG" say " ${STR_DONE_CONFIG:-config:} $ONBOARDING_CONFIG"
[ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " ${STR_DONE_SECRETS:-secrets:} $SECRETS_ENV (chmod 600)" [ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " ${STR_DONE_SECRETS:-secrets:} $SECRETS_ENV (chmod 600)"
fi fi
# MUST end on success: the last statement above is a short-circuit `&&` that
# evaluates false when the provider has no auth keys (claude-code / codex /
# local) → function would return 1 → `set -e` aborts the whole install at the
# onboarding_run call. Subscription/local providers are exactly the no-key case.
return 0
} }

74
install/lib-packs.sh Normal file
View file

@ -0,0 +1,74 @@
# shellcheck shell=bash
# lib-packs.sh — hook-pack + stack-profile resolver. Reads _primitives/hook-packs.toml
# via the generic _toml_array reader (from lib-profile.sh). Decides which hooks get
# wired into settings.json and which agent manifests install, based on the user's
# onboarding selection (or the safe minimal default when none was made).
#
# Requires: _toml_array from lib-profile.sh.
# Reads globals: $KIT_DIR (kit checkout), optional $ONBOARDING_STACK / $ONBOARDING_PACKS
# (live onboarding), optional $ONBOARDING_CONFIG (persisted selection).
PACKS_TOML="${PACKS_TOML:-$KIT_DIR/_primitives/hook-packs.toml}"
# --- thin table readers ---------------------------------------------------
pack_hooks() { _toml_array "$PACKS_TOML" "pack" "$1"; }
stack_packs() { _toml_array "$PACKS_TOML" "stack-packs" "$1"; }
stack_agent_groups() { _toml_array "$PACKS_TOML" "stack-agents" "$1"; }
agent_set_members() { _toml_array "$PACKS_TOML" "agent-set" "$1"; }
_packs_always() { _toml_array "$PACKS_TOML" "pack-always" "base"; }
# --- selection (live onboarding globals > persisted toml > none) ----------
# Echo the chosen stack, or empty if the user never chose one.
_packs_chosen_stack() {
if [ -n "${ONBOARDING_STACK:-}" ]; then printf '%s' "$ONBOARDING_STACK"; return; fi
local cfg="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}"
[ -f "$cfg" ] && grep -E '^stack_profile[[:space:]]*=' "$cfg" \
| sed -E 's/.*=[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1
}
# Echo the explicitly enabled packs (space-separated), or empty.
_packs_chosen_packs() {
if [ -n "${ONBOARDING_PACKS:-}" ]; then printf '%s' "$ONBOARDING_PACKS"; return; fi
local cfg="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}"
[ -f "$cfg" ] && grep -E '^enabled_packs[[:space:]]*=' "$cfg" \
| sed -E 's/.*=[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1
}
# --- resolution -----------------------------------------------------------
# Newline list of hook basenames to wire on install: safety + always + every
# pack pulled by the chosen stack + every explicitly enabled pack. Default
# (no choice) = safety + always only.
resolve_selected_hook_basenames() {
local stack packs p out
stack="$(_packs_chosen_stack)"; stack="${stack:-minimal}"
packs="$(_packs_chosen_packs)"
out="$(pack_hooks safety) $(_packs_always)"
for p in $(stack_packs "$stack") $packs; do
out="$out $(pack_hooks "$p")"
done
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}
# Newline list of agent manifest basenames to install for the chosen stack
# (base group always included). EMPTY when no stack was chosen → caller
# installs ALL manifests (power-user / non-interactive default).
resolve_selected_agent_manifests() {
local stack g out
stack="$(_packs_chosen_stack)"
[ -n "$stack" ] || return 0
out=""
for g in $(stack_agent_groups "$stack"); do
out="$out $(agent_set_members "$g")"
done
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}
# Newline list of EVERY kit-owned hook basename (all packs + always). Used by
# prune_kit_hooks to identify which settings.json entries the kit owns.
all_pack_basenames() {
local p out=""
for p in safety evidence observability epistemic orchestration git-guard stack-rust; do
out="$out $(pack_hooks "$p")"
done
out="$out $(_packs_always)"
echo "$out" | tr ' ' '\n' | grep -v '^$' | sort -u
}

View file

@ -100,7 +100,7 @@ _print_primitive_rows() {
[ -z "$name" ] && continue [ -z "$name" ] && continue
kind="$(primitive_field "$name" kind 2>/dev/null || echo '?')" kind="$(primitive_field "$name" kind 2>/dev/null || echo '?')"
extra="$(primitive_time_secs "$name")s, $(( $(primitive_disk_kb "$name") / 1024 )) MB" extra="$(primitive_time_secs "$name")s, $(( $(primitive_disk_kb "$name") / 1024 )) MB"
printf ' + %-22s (%s, ~%s)\n' "$name" "$kind" "$extra" printf ' + %s%-22s%s (%s, ~%s)\n' "${KEI_BLUE:-}" "$name" "${KEI_RST:-}" "$kind" "$extra"
done done
} }

View file

@ -25,7 +25,7 @@ preflight_offer_install() {
echo "$cli не найден." >&2 echo "$cli не найден." >&2
echo " Установить: $install_cmd" >&2 echo " Установить: $install_cmd" >&2
echo "" >&2 echo "" >&2
if [ -t 0 ] && [ -t 1 ]; then if [ -t 0 ]; then # stdin-only: stdout may be tee'd in curl|bash
echo " ⓘ команда: $install_cmd" >&2 echo " ⓘ команда: $install_cmd" >&2
read -r -p " Поставить сейчас? [y/N/skip] " ans read -r -p " Поставить сейчас? [y/N/skip] " ans
case "$ans" in case "$ans" in

View file

@ -28,7 +28,7 @@ copy_shell_primitive() {
mkdir -p "$AGENTS_DIR/_primitives" mkdir -p "$AGENTS_DIR/_primitives"
cp -f "$src" "$dst" cp -f "$src" "$dst"
chmod +x "$dst" chmod +x "$dst"
say " + shell: $name ($file)" say " + shell: ${KEI_BLUE:-}$name${KEI_RST:-} ($file)"
} }
remove_shell_primitive() { remove_shell_primitive() {
@ -65,7 +65,7 @@ copy_rust_primitive() {
cp -rf "$src/$sibling/"* "$dst/$sibling/" 2>/dev/null || true cp -rf "$src/$sibling/"* "$dst/$sibling/" 2>/dev/null || true
fi fi
done done
say " + rust: $name (crate $crate)" say " + rust: ${KEI_BLUE:-}$name${KEI_RST:-} (crate $crate)"
} }
remove_rust_primitive() { remove_rust_primitive() {
@ -103,7 +103,7 @@ copy_node_primitive() {
find "$dst" -depth -name "$ex" -exec rm -rf {} + 2>/dev/null || true find "$dst" -depth -name "$ex" -exec rm -rf {} + 2>/dev/null || true
done < <(_node_excludes) done < <(_node_excludes)
fi fi
say " + node: $name (path $rel)" say " + node: ${KEI_BLUE:-}$name${KEI_RST:-} (path $rel)"
} }
remove_node_primitive() { remove_node_primitive() {
@ -171,7 +171,7 @@ install_external_primitive() {
err "external primitive $name: entry point $slug not found in $file" err "external primitive $name: entry point $slug not found in $file"
return 1 return 1
fi fi
say " + external: $name (via $file)" say " + external: ${KEI_BLUE:-}$name${KEI_RST:-} (via $file)"
"$slug" || warn "$name install failed — re-run after fixing prereqs" "$slug" || warn "$name install failed — re-run after fixing prereqs"
} }

View file

@ -19,13 +19,16 @@ have_python_toml() {
return 1 return 1
} }
# Echo space-separated primitive names for a given profile. # Generic one-line-array TOML reader. Echoes space-separated values of
# Usage: profile_members <profile-name> # [<table>]
profile_members() { # <key> = ["a", "b", ...]
local profile="$1" # python-tomllib preferred; awk fallback handles one-line arrays only.
[ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; } # Usage: _toml_array <file> <table> <key>
_toml_array() {
local file="$1" table="$2" key="$3"
[ -f "$file" ] || return 1
if have_python_toml; then if have_python_toml; then
python3 - "$MANIFEST" "$profile" <<'PY' 2>/dev/null || return 1 python3 - "$file" "$table" "$key" <<'PY' 2>/dev/null || return 1
import sys import sys
try: try:
import tomllib import tomllib
@ -33,20 +36,19 @@ try:
except ImportError: except ImportError:
import toml as tomllib import toml as tomllib
mode = "r" mode = "r"
path, prof = sys.argv[1], sys.argv[2] path, table, key = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path, mode) as f: with open(path, mode) as f:
data = tomllib.load(f) if mode == "rb" else tomllib.load(f) data = tomllib.load(f)
members = data.get("profile", {}).get(prof) vals = data.get(table, {}).get(key)
if members is None: if vals is None:
sys.exit(2) sys.exit(2)
print(" ".join(members)) print(" ".join(vals))
PY PY
else else
# awk fallback — only handles `profile.<name> = [...]` on one line awk -v table="$table" -v key="$key" '
awk -v prof="$profile" ' $0 ~ "^\\[" table "\\]" { in_t=1; next }
/^\[profile\]/ { in_profile=1; next } /^\[/ { in_t=0 }
/^\[/ && !/^\[profile\]/ { in_profile=0 } in_t && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
in_profile && $0 ~ "^[[:space:]]*" prof "[[:space:]]*=" {
line = $0 line = $0
sub(/^[^\[]*\[/, "", line) sub(/^[^\[]*\[/, "", line)
sub(/\].*$/, "", line) sub(/\].*$/, "", line)
@ -55,10 +57,18 @@ PY
print line print line
exit exit
} }
' "$MANIFEST" ' "$file"
fi fi
} }
# Echo space-separated primitive names for a given profile.
# Usage: profile_members <profile-name>
profile_members() {
local profile="$1"
[ -f "$MANIFEST" ] || { err "MANIFEST.toml not found at $MANIFEST"; return 1; }
_toml_array "$MANIFEST" "profile" "$profile"
}
# Echo a field of a primitive. Usage: primitive_field <name> <field> # Echo a field of a primitive. Usage: primitive_field <name> <field>
# field ∈ { kind, file, crate, desc, deps } # field ∈ { kind, file, crate, desc, deps }
primitive_field() { primitive_field() {

View file

@ -66,20 +66,19 @@ copy_sleep_scripts() {
fi fi
} }
# KeiSei tamagotchi statusline — copy the renderer + state updater into # Pure-bash scripts → ~/.claude/scripts/ (tamagotchi renderer + state updater,
# ~/.claude/scripts/. Zero binary deps (pure bash, state under ~/.claude/pet/), # kei-message mailbox CLI, any future scripts). Zero binary deps, always
# always available regardless of profile. The statusLine + pet-update hooks # available regardless of profile. statusLine + pet-update + mailbox-inject
# are wired into settings.json by the settings-snippet merge (lib-hooks.sh). # hooks are wired into settings.json by the settings-snippet merge (lib-hooks.sh).
copy_pet_scripts() { copy_pet_scripts() {
local pet_sh src dst="$HOME_DIR/.claude/scripts" local src dst="$HOME_DIR/.claude/scripts" name
[ -d "$KIT_DIR/scripts" ] || return 0 [ -d "$KIT_DIR/scripts" ] || return 0
mkdir -p "$dst" mkdir -p "$dst"
for pet_sh in keisei-pet.sh keisei-pet-update.sh; do for src in "$KIT_DIR/scripts/"*.sh; do
src="$KIT_DIR/scripts/$pet_sh" [ -f "$src" ] || continue
if [ -f "$src" ]; then name="$(basename "$src")"
cp -f "$src" "$dst/$pet_sh" cp -f "$src" "$dst/$name"
chmod +x "$dst/$pet_sh" chmod +x "$dst/$name"
fi
done done
} }

View file

@ -66,7 +66,9 @@ print_summary() {
$AGENTS_DIR/_assembler/target/release/assemble --validate $AGENTS_DIR/_assembler/target/release/assemble --validate
./install.sh --list # show installed primitives ./install.sh --list # show installed primitives
To create a new project-specialist agent: To set up agents for ALL your projects (scan stack + create one per project):
/onboard ~/Projects/*
Or create a single project-specialist agent:
/new-agent /new-agent
========================================================================== ==========================================================================
@ -93,7 +95,9 @@ EOF
$AGENTS_DIR/_assembler/target/release/assemble --validate $AGENTS_DIR/_assembler/target/release/assemble --validate
./install.sh --list # show installed primitives ./install.sh --list # show installed primitives
To create a new project-specialist agent: To set up agents for ALL your projects (scan stack + create one per project):
/onboard ~/Projects/*
Or create a single project-specialist agent:
/new-agent /new-agent
========================================================================== ==========================================================================

View file

@ -10,7 +10,7 @@
run_sleep_wizard() { run_sleep_wizard() {
local sleep_helper="$AGENTS_DIR/_primitives/kei-sleep-setup.sh" local sleep_helper="$AGENTS_DIR/_primitives/kei-sleep-setup.sh"
if [[ -x "$sleep_helper" ]] && [ -t 0 ] && [ -t 1 ]; then if [[ -x "$sleep_helper" ]] && [ -t 0 ]; then # stdin only; not -t 1 (curl|bash tees stdout)
say "running sleep-sync setup helper" say "running sleep-sync setup helper"
"$sleep_helper" || warn "sleep-sync setup did not complete — re-run via /sleep-setup" "$sleep_helper" || warn "sleep-sync setup did not complete — re-run via /sleep-setup"
else else

View file

@ -2,10 +2,10 @@
"$schema": "https://json.schemastore.org/claude-code-plugin.json", "$schema": "https://json.schemastore.org/claude-code-plugin.json",
"name": "keisei", "name": "keisei",
"displayName": "KeiSei", "displayName": "KeiSei",
"description": "Constructor Pattern agent substrate — 59 agents, 67 skills, 39 hooks, 86 blocks. Rust primitives via classic ./install.sh.", "description": "Constructor Pattern multi-LLM agent substrate — 38 agents, 69 skills, 54 hooks, 86 blocks. Cross-CLI policy enforcement (Claude/Grok/Copilot/Agy/Kimi) via kei-mcp + kei_bash/kei_edit/kei_write. Rust primitives via classic ./install.sh.",
"version": "0.38.0", "version": "0.45.0",
"homepage": "https://keigit.com/keisei/KeiSeiKit-1.0", "homepage": "https://keisei.app",
"repository": "https://keigit.com/keisei/KeiSeiKit-1.0.git", "repository": "https://github.com/KeiSeiLab/KeiSeiKit-1.0.git",
"author": { "author": {
"name": "Denis Parfionovich", "name": "Denis Parfionovich",
"email": "parfionovich@keilab.io" "email": "parfionovich@keilab.io"

281
scripts/kei-agent-cli.sh Executable file
View file

@ -0,0 +1,281 @@
#!/usr/bin/env bash
# kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend.
#
# Two entry points (both route through this script):
#
# kei run-via <backend> <agent> "<task>" # explicit backend
# kei agent <agent> "<task>" # backend resolved from DNA:
# # 1. --on=<backend> flag
# # 2. agent manifest's `provider`
# # 3. ~/.claude/config/primary.toml
# # 4. fallback: claude
#
# Other forms:
# kei run-via list # show backends + agents
# kei agent --on=<backend> <agent> "<task>" # override DNA backend
# kei primary # print current primary
# kei primary <backend> # set primary provider
# kei run-via --help
#
# Backends (SSoT: _primitives/cli-backends.toml):
# claude grok agy copilot kimi codex
#
# Reads assembled prompt from ~/.claude/agents/<agent-name>.md.
# Strips YAML frontmatter, composes with task, execs the CLI.
#
# Env overrides:
# KEI_AGENTS_DIR agent .md dir (default: ~/.claude/agents)
# KEI_MANIFESTS_DIR manifest .toml dir (default: ~/.claude/_manifests)
# KEI_PRIMARY override primary backend (beats config file)
# KEI_NATIVE_AGENT=1 prefer backend's native --agent flag (grok/claude)
set -euo pipefail
KEI_AGENTS_DIR="${KEI_AGENTS_DIR:-$HOME/.claude/agents}"
KEI_MANIFESTS_DIR="${KEI_MANIFESTS_DIR:-$HOME/.claude/_manifests}"
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
KEI_NATIVE_AGENT="${KEI_NATIVE_AGENT:-0}"
usage() { sed -n '2,32p' "$0" | sed 's|^# \{0,1\}||'; }
# ---- backend table (SSoT mirror; kept in sync with cli-backends.toml) -----
backend_bin() {
case "$1" in
claude) echo "claude" ;;
grok) echo "grok" ;;
agy|antigravity) echo "agy" ;;
copilot) echo "copilot" ;;
kimi) echo "kimi" ;;
codex) echo "codex" ;;
*) return 1 ;;
esac
}
backend_supports_native_agent() {
case "$1" in claude|grok) return 0 ;; *) return 1 ;; esac
}
# ---- DNA resolver: agent → preferred backend --------------------------------
# Reads `provider = "..."` line from the manifest TOML if present.
manifest_provider() {
local agent="$1" tomlf="$KEI_MANIFESTS_DIR/$1.toml"
[ -f "$tomlf" ] || return 1
awk -F'=' '
/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}
' "$tomlf"
}
# Reads primary from config file (or KEI_PRIMARY env override).
config_primary() {
if [ -n "${KEI_PRIMARY:-}" ]; then
printf '%s\n' "$KEI_PRIMARY"; return 0
fi
[ -f "$KEI_PRIMARY_CFG" ] || return 1
awk -F'=' '
/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}
' "$KEI_PRIMARY_CFG"
}
# Resolution order: explicit --on= → manifest provider → primary → claude.
resolve_backend() {
local agent="$1" explicit="${2:-}" out=""
if [ -n "$explicit" ]; then printf '%s\n' "$explicit"; return 0; fi
out=$(manifest_provider "$agent" 2>/dev/null) || true
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
out=$(config_primary 2>/dev/null) || true
if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi
printf 'claude\n'
}
# ---- backend invocation ---------------------------------------------------
backend_invoke() {
local backend="$1" prompt="$2" agent_name="${3:-}" bin
bin=$(backend_bin "$backend") || {
printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2
return 2
}
command -v "$bin" >/dev/null 2>&1 || {
printf '[kei-agent-cli] %s not on PATH. Install it or pick another backend.\n' "$bin" >&2
return 127
}
# Native --agent path (grok/claude) — pass agent name + task directly.
if [ "$KEI_NATIVE_AGENT" = "1" ] \
&& [ -n "$agent_name" ] \
&& backend_supports_native_agent "$backend"; then
printf '[kei-agent-cli] %s --agent %s\n' "$bin" "$agent_name" >&2
exec "$bin" --agent "$agent_name" --print "${prompt##*TASK FOR THIS RUN:}"
fi
# v0.41 fix: headless subprocess invocation of claude/grok without
# --dangerously-skip-permissions returns empty (the agent's system prompt
# asks for Read/Grep tools, but those need permission prompts which can't
# be answered in -p mode). Pass the flag so the agent actually executes.
# Override via KEI_AGENT_PERMISSIVE=0 to keep the strict default.
local permissive_claude="" permissive_grok=""
if [ "${KEI_AGENT_PERMISSIVE:-1}" = "1" ]; then
permissive_claude="--permission-mode=bypassPermissions"
permissive_grok="--always-approve"
fi
case "$backend" in
claude) exec "$bin" $permissive_claude -p "$prompt" ;;
grok) exec "$bin" $permissive_grok --print "$prompt" ;;
agy|antigravity) exec "$bin" --dangerously-skip-permissions --print "$prompt" ;;
copilot) exec "$bin" --prompt "$prompt" ;;
kimi)
# Kimi has NO one-shot print mode (smoke-tested 2026-05-26): bare `kimi`
# opens an interactive TUI that ignores piped stdin and exits with "Bye!".
# For headless invocation we'd need an ACP client (`kimi acp` is a JSON-RPC
# stdio server). Until KeiSeiKit ships that client, dump the composed
# prompt to a tmpfile and open the TUI so the user can paste it in.
tmp=$(mktemp -t kei-agent-kimi.XXXX.md)
printf '%s\n' "$prompt" > "$tmp"
printf '[kei-agent-cli] kimi non-interactive is unsupported (TUI only).\n' >&2
printf '[kei-agent-cli] composed prompt saved: %s\n' "$tmp" >&2
printf '[kei-agent-cli] copy-paste it into Kimi after the TUI opens.\n' >&2
printf '[kei-agent-cli] (or pipe via `kimi acp` if you have an ACP client.)\n' >&2
exec "$bin"
;;
codex) exec "$bin" -p "$prompt" ;;
esac
}
# ---- agent loader -------------------------------------------------------
load_agent() {
local name="$1" path
case "$name" in
--file=*) path="${name#--file=}" ;;
/*|./*|*/*) path="$name" ;;
*) path="$KEI_AGENTS_DIR/$name.md" ;;
esac
if [ ! -f "$path" ]; then
printf '[kei-agent-cli] agent not found: %s\n' "$path" >&2
if [ -d "$KEI_AGENTS_DIR" ]; then
printf ' Available (%s): %s\n' "$KEI_AGENTS_DIR" \
"$(find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
| xargs -n1 basename 2>/dev/null | sed 's/\.md$//' \
| sort | head -8 | tr '\n' ' ')..." >&2
fi
return 1
fi
awk '
BEGIN { in_fm=0 }
NR==1 && /^---$/ { in_fm=1; next }
in_fm && /^---$/ { in_fm=0; next }
in_fm { next }
{ print }
' "$path"
}
# ---- primary subcommand ------------------------------------------------
handle_primary() {
local arg="${1:-}"
if [ -z "$arg" ]; then
cur=$(config_primary 2>/dev/null || true)
printf 'primary provider: %s\n' "${cur:-claude (default fallback)}"
[ -f "$KEI_PRIMARY_CFG" ] && printf 'config: %s\n' "$KEI_PRIMARY_CFG"
return 0
fi
backend_bin "$arg" >/dev/null || {
printf '[kei-primary] unknown backend: %s\n' "$arg" >&2
printf 'valid: claude grok agy copilot kimi codex\n' >&2
return 2
}
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
printf '# kei primary — written %s\nprovider = "%s"\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$arg" > "$KEI_PRIMARY_CFG"
printf 'primary provider set: %s → %s\n' "$arg" "$KEI_PRIMARY_CFG"
}
# ---- subcommands --------------------------------------------------------
case "${1:-}" in
""|-h|--help|help) usage; exit 0 ;;
list|--list)
printf 'Backends (✓ installed, ✗ missing):\n'
for b in claude grok agy copilot kimi codex; do
bin=$(backend_bin "$b")
if p=$(command -v "$bin" 2>/dev/null); then
printf ' %-10s ✓ %s\n' "$b" "$p"
else
printf ' %-10s ✗ (not on PATH)\n' "$b"
fi
done
cur=$(config_primary 2>/dev/null || true)
printf '\nprimary: %s\n' "${cur:-claude (default)}"
printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR"
if [ -d "$KEI_AGENTS_DIR" ]; then
find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \
| xargs -n1 basename 2>/dev/null | sed 's/\.md$/ /' | sort | column 2>/dev/null \
|| (find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' \
| xargs -n1 basename | sed 's/\.md$//' | sort | head -20)
fi
exit 0
;;
primary)
shift
handle_primary "${1:-}"
exit $?
;;
agent)
# Direct-invocation passthrough: `kei-agent-cli.sh agent <name> "task"`
# behaves identically to `kei-agent-cli.sh <name> "task"` (DNA mode).
# Lets users call either form without surprise.
shift
;;
esac
# ---- main: DNA mode (no leading backend) OR explicit run-via ------------
# Detect call shape:
# "$1" is a known backend → run-via flow (kei run-via <backend> <agent> "task")
# "$1" starts with --on= → DNA flow with override
# "$1" is anything else → DNA flow (kei agent <agent> "task")
EXPLICIT_BACKEND=""
case "${1:-}" in
--on=*)
EXPLICIT_BACKEND="${1#--on=}"
shift
;;
*)
if [ $# -ge 1 ] && backend_bin "$1" >/dev/null 2>&1; then
EXPLICIT_BACKEND="$1"
shift
fi
;;
esac
if [ $# -lt 2 ]; then
usage
exit 2
fi
AGENT_REF="$1"; shift
TASK="$*"
AGENT_NAME=$(basename "${AGENT_REF#--file=}")
AGENT_NAME="${AGENT_NAME%.md}"
BACKEND=$(resolve_backend "$AGENT_NAME" "$EXPLICIT_BACKEND")
if ! AGENT_PROMPT=$(load_agent "$AGENT_REF"); then
exit 1
fi
COMPOSED=$(printf '%s\n\n---\n\nTASK FOR THIS RUN:\n%s\n' "$AGENT_PROMPT" "$TASK")
printf '[kei-agent-cli] agent=%s backend=%s (via %s)\n' \
"$AGENT_NAME" "$BACKEND" \
"$([ -n "$EXPLICIT_BACKEND" ] && echo explicit \
|| ([ -n "$(manifest_provider "$AGENT_NAME" 2>/dev/null)" ] && echo manifest \
|| ([ -n "$(config_primary 2>/dev/null)" ] && echo primary || echo default)))" >&2
backend_invoke "$BACKEND" "$COMPOSED" "$AGENT_NAME"

62
scripts/kei-configure.sh Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
# kei-configure — re-pick hook packs + stack profile after install, without a
# full reinstall. Updates ~/.claude/config/onboarding.toml and re-applies the
# hook selection to settings.json (adds newly selected hooks, removes deselected
# kit hooks, leaves your own hooks untouched). Agent-set changes apply on the
# next `./install.sh`.
#
# Invoked via `kei configure`. Interactive (needs a terminal).
set -u
set -o pipefail 2>/dev/null || true
HOME_DIR="${HOME:?HOME not set}"
KIT_DIR="$(cat "$HOME_DIR/.claude/.kei-kit-dir" 2>/dev/null || true)"
if [ -z "$KIT_DIR" ] || [ ! -d "$KIT_DIR/install" ]; then
echo "kei configure: KeiSeiKit checkout not found." >&2
echo " (expected its path in ~/.claude/.kei-kit-dir; re-run ./install.sh from your checkout)" >&2
exit 1
fi
if [ ! -t 0 ]; then
echo "kei configure: interactive only — run it from a terminal." >&2
exit 1
fi
LIB_DIR="$KIT_DIR/install"
MANIFEST="$KIT_DIR/_primitives/MANIFEST.toml"
PACKS_TOML="$KIT_DIR/_primitives/hook-packs.toml"
ONBOARDING_CONFIG="$HOME_DIR/.claude/config/onboarding.toml"
export HOME_DIR KIT_DIR LIB_DIR MANIFEST PACKS_TOML ONBOARDING_CONFIG
# shellcheck source=/dev/null
source "$LIB_DIR/lib-log.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-backup.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-profile.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-packs.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-hooks.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/lib-onboarding-ui.sh"
ONBOARDING_STACK=""
ONBOARDING_PACKS=""
onboarding_pick_stack
# Update only stack_profile/enabled_packs in onboarding.toml; preserve the rest.
mkdir -p "$(dirname "$ONBOARDING_CONFIG")"
touch "$ONBOARDING_CONFIG"
_tmp="$(mktemp)"
grep -vE '^(stack_profile|enabled_packs)[[:space:]]*=' "$ONBOARDING_CONFIG" > "$_tmp" 2>/dev/null || true
{
printf 'stack_profile = "%s"\n' "$ONBOARDING_STACK"
printf 'enabled_packs = "%s"\n' "$ONBOARDING_PACKS"
} >> "$_tmp"
mv "$_tmp" "$ONBOARDING_CONFIG"
# Re-apply hooks: prune kit-owned entries, merge the newly selected set.
activate_hooks
say "reconfigured: stack=$ONBOARDING_STACK packs=${ONBOARDING_PACKS:-none}"
say " settings.json hooks updated. Agent-set changes apply on the next ./install.sh."

230
scripts/kei-limits.sh Executable file
View file

@ -0,0 +1,230 @@
#!/usr/bin/env bash
# kei-limits — probe each installed CLI's remaining quota / balance.
#
# Reality (research 2026-05-26):
# • claude — no programmatic API. Headers per-API-call only. Admin API
# exists but needs a separate admin key. See dashboard.
# • grok — same as claude. Headers per-API-call only. No file.
# • agy — interactive /usage slash-cmd is broken (shows 100% always,
# forum-verified bug). No public API.
# • copilot — no public quota API. github.com/settings/billing only.
# Inline output during call shows usage but nothing exposed
# for poll.
# • kimi — Moonshot API /v1/users/me/balance returns $ balance only
# (no session/weekly quota). Requires MOONSHOT_API_KEY.
#
# Output:
# stdout: human summary (default) OR JSON (--json)
# file: ~/.claude/pet/limits-cache.json (always, for pet to read)
#
# Polling: NOT poll-friendly. Run on demand or via launchd at >5 min intervals.
# Pet's job: read the cache; pet does NOT call this script.
set -u
# v0.43-fix #4: jq runtime guard (convention with 40+ sibling scripts).
command -v jq >/dev/null 2>&1 || {
echo "kei-limits: jq required (brew install jq / apt install jq)" >&2
exit 1
}
CACHE="${KEI_LIMITS_CACHE:-$HOME/.claude/pet/limits-cache.json}"
mkdir -p "$(dirname "$CACHE")"
JSON_OUT=0
QUIET=0
for arg in "$@"; do
case "$arg" in
--json) JSON_OUT=1 ;;
--quiet) QUIET=1 ;;
-h|--help) sed -n '2,22p' "$0" | sed 's|^# \{0,1\}||'; exit 0 ;;
esac
done
# --- per-CLI probes (each returns one JSON value to stdout) ----------------
probe_claude() {
# No public API; produce a status marker, no live data.
printf '%s' '{"status":"no-api","note":"see claude.ai/settings/usage","dashboard":"https://claude.ai/settings/usage"}'
}
probe_grok() {
printf '%s' '{"status":"no-api","note":"headers-only per API call; see x.ai dashboard","dashboard":"https://x.ai"}'
}
probe_agy() {
printf '%s' '{"status":"broken-api","note":"interactive /usage shows 100% (forum-verified bug); use Google Cloud Console","dashboard":"https://console.cloud.google.com/apis/api/generativelanguage.googleapis.com/quotas"}'
}
probe_copilot() {
# Try gh CLI graphQL — most variants don't expose Copilot billing publicly.
# If we ever find an endpoint, drop it in here. For now: status marker.
printf '%s' '{"status":"no-api","note":"see github.com/settings/billing → Copilot section","dashboard":"https://github.com/settings/billing"}'
}
probe_kimi() {
if [ -z "${MOONSHOT_API_KEY:-}" ]; then
printf '%s' '{"status":"need-key","note":"set MOONSHOT_API_KEY in env to fetch live balance","dashboard":"https://platform.kimi.ai"}'
return
fi
if ! command -v curl >/dev/null 2>&1; then
printf '%s' '{"status":"no-curl","note":"curl required for live probe"}'
return
fi
# v0.44 fix #3 (Gemini HIGH): sanitize MOONSHOT_API_KEY before formatting.
# Was: token injected into a curl --config line via printf 'header = "...%s..."';
# if the token contained a double-quote + newline + 'url = "attacker"',
# curl would parse the injected config option and redirect the request.
# Now: validate the key matches a known-safe charset; reject otherwise.
case "$MOONSHOT_API_KEY" in
*[!A-Za-z0-9_.\-]*)
printf '%s' '{"status":"probe-failed","note":"MOONSHOT_API_KEY contains unsafe chars; expected [A-Za-z0-9_.-]"}'
return
;;
esac
local resp
resp=$(printf 'header = "Authorization: Bearer %s"\n' "$MOONSHOT_API_KEY" \
| curl -sS --max-time 5 --config - \
"https://api.moonshot.ai/v1/users/me/balance" 2>/dev/null \
|| echo '')
if [ -z "$resp" ]; then
printf '%s' '{"status":"probe-failed","note":"no response (network / wrong key)"}'
return
fi
# v0.43-fix #2: tonumber? swallows parse errors (was: tonumber threw on
# any non-numeric balance, emitted empty JSON, poisoned the assembler
# --argjson → whole cache wiped).
local avail
avail=$(printf '%s' "$resp" | jq -r '.data.available_balance // empty' 2>/dev/null)
if [ -z "$avail" ]; then
printf '%s' '{"status":"probe-failed","note":"API returned non-balance response"}'
return
fi
local cash voucher
cash=$(printf '%s' "$resp" | jq -r '.data.cash_balance // 0' 2>/dev/null)
voucher=$(printf '%s' "$resp" | jq -r '.data.voucher_balance // 0' 2>/dev/null)
jq -n --arg s "live" --arg a "$avail" --arg c "$cash" --arg v "$voucher" \
'{status:$s, available_balance_usd:($a|tonumber? // 0), cash_balance_usd:($c|tonumber? // 0), voucher_balance_usd:($v|tonumber? // 0), dashboard:"https://platform.kimi.ai"}'
}
# --- assemble cache JSON ---------------------------------------------------
# v0.43-fix #1: atomic stage-and-rename. Was: `jq > "$CACHE"` truncated the
# cache BEFORE jq ran — a transient failure permanently wiped the cache.
# Now: build in tmpfile, validate non-empty, then atomic mv. Preserves
# last-known-good across probe failures.
# v0.43-fix #2 (defense-in-depth): if any individual probe returns empty
# string, substitute a status marker so --argjson never sees invalid JSON.
_safe_json() {
local payload="$1"
if [ -z "$payload" ]; then
printf '%s' '{"status":"probe-empty","note":"probe returned empty result"}'
return
fi
# Validate parses.
if ! printf '%s' "$payload" | jq empty 2>/dev/null; then
printf '%s' '{"status":"probe-invalid","note":"probe returned non-JSON"}'
return
fi
printf '%s' "$payload"
}
P_CLAUDE=$(_safe_json "$(probe_claude)")
P_GROK=$(_safe_json "$(probe_grok)")
P_AGY=$(_safe_json "$(probe_agy)")
P_COPILOT=$(_safe_json "$(probe_copilot)")
P_KIMI=$(_safe_json "$(probe_kimi)")
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP=$(mktemp "${CACHE}.XXXXXX")
if jq -n \
--arg ts "$NOW" \
--argjson claude "$P_CLAUDE" \
--argjson grok "$P_GROK" \
--argjson agy "$P_AGY" \
--argjson copilot "$P_COPILOT" \
--argjson kimi "$P_KIMI" \
'{ts:$ts, claude:$claude, grok:$grok, agy:$agy, copilot:$copilot, kimi:$kimi}' \
> "$TMP" 2>/dev/null \
&& [ -s "$TMP" ]; then
mv -f "$TMP" "$CACHE"
else
rm -f "$TMP" 2>/dev/null
echo "kei-limits: cache refresh failed — keeping previous cache" >&2
if [ ! -f "$CACHE" ]; then
# v0.44 fix #9 (Claude MED): failure-fallback must carry the SAME schema
# as the success cache (ts + 5 per-CLI keys). Was: emitted only {ts,
# status} which broke pet's .kimi.available_balance_usd read and the
# script's own per-CLI render loop. Now: full shape, all 5 marked
# status="assembly-failed".
jq -n '{ts:"",
claude:{status:"assembly-failed",note:"see logs"},
grok:{status:"assembly-failed",note:"see logs"},
agy:{status:"assembly-failed",note:"see logs"},
copilot:{status:"assembly-failed",note:"see logs"},
kimi:{status:"assembly-failed",note:"see logs"}}' \
> "$CACHE" 2>/dev/null \
|| printf '%s\n' '{"ts":"","claude":{"status":"assembly-failed"},"grok":{"status":"assembly-failed"},"agy":{"status":"assembly-failed"},"copilot":{"status":"assembly-failed"},"kimi":{"status":"assembly-failed"}}' > "$CACHE"
fi
fi
# --- output ----------------------------------------------------------------
if [ "$JSON_OUT" = "1" ]; then
cat "$CACHE"
exit 0
fi
if [ "$QUIET" = "1" ]; then
exit 0
fi
C0= CB= CG= CY= CR= CD=
if [ -t 1 ]; then
C0=$'\033[0m'
CB=$'\033[1;38;5;39m'
CG=$'\033[32m'
CY=$'\033[33m'
CR=$'\033[31m'
CD=$'\033[2m'
fi
format_one() {
local label="$1" key="$2" data="$3"
local status note
status=$(printf '%s' "$data" | jq -r '.status')
note=$(printf '%s' "$data" | jq -r '.note // ""')
case "$status" in
live)
local avail
avail=$(printf '%s' "$data" | jq -r '.available_balance_usd // empty')
printf " ${CG}${C0} %-8s \$%-8s ${CD}live (Moonshot balance)${C0}\n" "$label" "$avail"
;;
no-api|need-key)
printf " ${CY}?${C0} %-8s ${CD}%s${C0}\n" "$label" "$note"
;;
broken-api)
printf " ${CR}${C0} %-8s ${CD}%s${C0}\n" "$label" "$note"
;;
*)
printf " ${CY}?${C0} %-8s ${CD}%s${C0}\n" "$label" "$note"
;;
esac
}
cat <<EOF
${CB}╔════════════════════════════════════════════════════════════╗
║ KeiSeiKit · CLI subscription limits ║
╚════════════════════════════════════════════════════════════╝${C0}
EOF
CACHE_CONTENT=$(cat "$CACHE")
for cli in claude grok agy copilot kimi; do
data=$(printf '%s' "$CACHE_CONTENT" | jq -c ".$cli")
format_one "$cli" "$cli" "$data"
done
echo
echo "${CD}cached: $CACHE${C0}"
echo "${CD}note: no CLI exposes session/weekly quota in a poll-friendly way.${C0}"
echo "${CD} See dashboards via 'open <url>' from --json output.${C0}"

52
scripts/kei-mcp-wire-agy.sh Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# kei-mcp-wire-agy — TIER 3: advisory enforcement for Google Antigravity.
#
# Antigravity (Gemini-backed) has NO tool allowlist mechanism — only the
# binary --dangerously-skip-permissions flag. We CANNOT disable its native
# shell. Best we can do:
# 1. Register kei-mcp via ~/.gemini/config/mcp_config.json
# 2. Prompt the agent (via its system prompt) to prefer kei_bash
# 3. Document honestly that this is advisory, not hard-enforced.
set -eu
CFG="$HOME/.gemini/config/mcp_config.json"
KEI_MCP_BIN="$HOME/.claude/_primitives/_rust/target/release/kei-mcp"
[ -f "$KEI_MCP_BIN" ] || KEI_MCP_BIN="$(command -v kei-mcp 2>/dev/null || true)"
if [ -z "$KEI_MCP_BIN" ] || [ ! -x "$KEI_MCP_BIN" ]; then
echo " agy: kei-mcp binary missing — build first: cargo build -p kei-mcp --release"
exit 0
fi
mkdir -p "$(dirname "$CFG")"
[ -f "$CFG" ] || echo '{}' > "$CFG"
desired=$(cat <<JSON
{
"mcpServers": {
"kei-mcp": {
"command": "$KEI_MCP_BIN"
}
}
}
JSON
)
if [ "${KEI_WIRE_DRY_RUN:-0}" = "1" ] || [ "${KEI_WIRE_CHECK:-0}" = "1" ]; then
echo " agy: would merge into $CFG:"
printf '%s\n' "$desired"
exit 0
fi
tmp=$(mktemp)
jq -s '.[0] * .[1]' "$CFG" <(printf '%s\n' "$desired") > "$tmp"
mv "$tmp" "$CFG"
cat <<EOF
agy: kei-mcp registered → $CFG
⚠ TIER 3 advisory: Antigravity has no way to disable native shell.
Native bash remains reachable and ungated. The agent reads the
system prompt (which mentions kei_bash) but may still use native.
For patent-sensitive / production-PR work, use Claude or Grok.
EOF

42
scripts/kei-mcp-wire-claude.sh Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# kei-mcp-wire-claude — verify Claude Code MCP wiring (TIER 1: already native).
#
# Claude Code reads MCP servers from ~/.claude/settings.json `mcpServers`
# block. We don't strictly need kei-mcp here (Claude's native PreToolUse
# hooks already enforce policy), but adding it gives Claude access to
# `spawn_agent` for cross-CLI sub-agent dispatch.
set -eu
CFG="$HOME/.claude/settings.json"
BIN="$HOME/.claude/_primitives/_rust/target/release/kei-mcp"
[ -f "$BIN" ] || BIN="$(command -v kei-mcp 2>/dev/null || true)"
if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
echo " kei-mcp binary not found — build first: cargo build -p kei-mcp --release"
exit 0
fi
echo " claude: native PreToolUse hooks already enforce policy chain (TIER 1)"
echo " kei-mcp binary: $BIN"
echo " (spawn_agent + kei_bash MCP tools available if added to"
echo " $CFG mcpServers — optional for Claude.)"
# Optional: dump merge snippet
if [ "${KEI_WIRE_CHECK:-0}" = "1" ] || [ "${KEI_WIRE_DRY_RUN:-0}" = "1" ]; then
cat <<EOF
Suggested merge into $CFG:
{
"mcpServers": {
"kei-mcp": {
"command": "$BIN",
"env": { "CLAUDECODE": "1" }
}
}
}
(CLAUDECODE=1 tells kei-mcp to skip its hook chain — your native hooks
already fire on PreToolUse. Avoids double-enforcement.)
EOF
fi

52
scripts/kei-mcp-wire-copilot.sh Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# kei-mcp-wire-copilot — TIER 2: MCP-wrapped enforcement for GitHub Copilot.
#
# Copilot CLI has NO hook system, BUT:
# 1. Supports --excluded-tools='shell' to disable native shell.
# 2. Has MCP server config at ~/.copilot/mcp-config.json.
# So: register kei-mcp via MCP, and instruct user to launch Copilot with
# --excluded-tools=shell so the agent can't use native bash and must use
# our policy-gated kei_bash.
set -eu
CFG="$HOME/.copilot/mcp-config.json"
KEI_MCP_BIN="$HOME/.claude/_primitives/_rust/target/release/kei-mcp"
[ -f "$KEI_MCP_BIN" ] || KEI_MCP_BIN="$(command -v kei-mcp 2>/dev/null || true)"
if [ -z "$KEI_MCP_BIN" ] || [ ! -x "$KEI_MCP_BIN" ]; then
echo " copilot: kei-mcp binary missing — build first: cargo build -p kei-mcp --release"
exit 0
fi
mkdir -p "$(dirname "$CFG")"
[ -f "$CFG" ] || echo '{}' > "$CFG"
desired=$(cat <<JSON
{
"mcpServers": {
"kei-mcp": {
"type": "stdio",
"command": "$KEI_MCP_BIN"
}
}
}
JSON
)
if [ "${KEI_WIRE_DRY_RUN:-0}" = "1" ] || [ "${KEI_WIRE_CHECK:-0}" = "1" ]; then
echo " copilot: would merge into $CFG:"
printf '%s\n' "$desired"
echo
echo " copilot: launch flag to enforce: --excluded-tools='shell'"
exit 0
fi
tmp=$(mktemp)
jq -s '.[0] * .[1]' "$CFG" <(printf '%s\n' "$desired") > "$tmp"
mv "$tmp" "$CFG"
echo " copilot: kei-mcp registered → $CFG"
echo " copilot: to enforce, launch with: copilot --excluded-tools='shell'"
echo " (this disables native shell; agent must use kei_bash via MCP)"
echo " Consider adding an alias: alias copilot='copilot --excluded-tools=shell'"

77
scripts/kei-mcp-wire-grok.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# kei-mcp-wire-grok — TIER 1: port KeiSeiKit hooks to Grok's PreToolUse pipeline.
#
# Grok CLI supports Claude-Code-compatible PreToolUse hooks via
# ~/.grok/settings.json. Same JSON input contract → our existing
# ~/.claude/hooks/*.sh scripts run unchanged.
#
# We register THREE hook entries (one per Bash-gating safety hook) plus
# the kei-mcp MCP server so Grok can also call spawn_agent.
#
# Idempotent: jq-merge into existing settings.json; foreign entries survive.
set -eu
CFG="$HOME/.grok/settings.json"
HOOKS_DIR="$HOME/.claude/hooks"
KEI_MCP_BIN="$HOME/.claude/_primitives/_rust/target/release/kei-mcp"
[ -f "$KEI_MCP_BIN" ] || KEI_MCP_BIN="$(command -v kei-mcp 2>/dev/null || true)"
mkdir -p "$(dirname "$CFG")"
[ -f "$CFG" ] || echo '{}' > "$CFG"
# Build the hook block — three Bash hooks + two Edit/Write hooks (same as
# Claude's policy-chain.toml).
desired=$(cat <<JSON
{
"hooks": {
"PreToolUse": [
{"matcher": "Bash", "hooks": [{"type": "command", "command": "$HOOKS_DIR/no-github-push.sh"}]},
{"matcher": "Bash", "hooks": [{"type": "command", "command": "$HOOKS_DIR/safety-guard.sh"}]},
{"matcher": "Bash", "hooks": [{"type": "command", "command": "$HOOKS_DIR/destructive-guard.sh"}]},
{"matcher": "Edit", "hooks": [{"type": "command", "command": "$HOOKS_DIR/citation-verify.sh"}]},
{"matcher": "Edit", "hooks": [{"type": "command", "command": "$HOOKS_DIR/numeric-claims-guard.sh"}]},
{"matcher": "Write", "hooks": [{"type": "command", "command": "$HOOKS_DIR/citation-verify.sh"}]},
{"matcher": "Write", "hooks": [{"type": "command", "command": "$HOOKS_DIR/numeric-claims-guard.sh"}]}
]
}
}
JSON
)
mcp_block=""
if [ -n "$KEI_MCP_BIN" ] && [ -x "$KEI_MCP_BIN" ]; then
mcp_block=$(cat <<JSON
{
"mcpServers": {
"kei-mcp": {
"command": "$KEI_MCP_BIN",
"env": { "GROKCODE": "1" }
}
}
}
JSON
)
fi
if [ "${KEI_WIRE_DRY_RUN:-0}" = "1" ] || [ "${KEI_WIRE_CHECK:-0}" = "1" ]; then
echo " grok: would merge into $CFG:"
printf '%s\n' "$desired"
[ -n "$mcp_block" ] && printf '%s\n' "$mcp_block"
exit 0
fi
# Merge: existing | desired (desired wins on key conflict; arrays are
# replaced, not appended — Grok PreToolUse semantics).
tmp=$(mktemp)
if [ -n "$mcp_block" ]; then
jq -s '.[0] * .[1] * .[2]' "$CFG" <(printf '%s\n' "$desired") <(printf '%s\n' "$mcp_block") > "$tmp"
else
jq -s '.[0] * .[1]' "$CFG" <(printf '%s\n' "$desired") > "$tmp"
fi
mv "$tmp" "$CFG"
echo " grok: wired PreToolUse hooks → $CFG"
echo " 5 hook entries (Bash×3 + Edit×2 + Write×2)"
[ -n "$mcp_block" ] && echo " kei-mcp MCP server registered (with GROKCODE=1 guard)"
echo " Same enforcement as Claude Code."

52
scripts/kei-mcp-wire-kimi.sh Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# kei-mcp-wire-kimi — TIER 3: advisory enforcement for Moonshot Kimi.
#
# Kimi uses a confirmation-prompt model — no tool allowlist syntax, no
# --excluded-tools flag. The user is prompted before every native tool
# call (YOLO mode auto-approves). MCP server config at ~/.kimi/mcp.json.
# Best we can do: register kei-mcp + prompt the agent to prefer kei_bash.
set -eu
CFG="$HOME/.kimi/mcp.json"
KEI_MCP_BIN="$HOME/.claude/_primitives/_rust/target/release/kei-mcp"
[ -f "$KEI_MCP_BIN" ] || KEI_MCP_BIN="$(command -v kei-mcp 2>/dev/null || true)"
if [ -z "$KEI_MCP_BIN" ] || [ ! -x "$KEI_MCP_BIN" ]; then
echo " kimi: kei-mcp binary missing — build first: cargo build -p kei-mcp --release"
exit 0
fi
mkdir -p "$(dirname "$CFG")"
[ -f "$CFG" ] || echo '{"mcpServers":{}}' > "$CFG"
desired=$(cat <<JSON
{
"mcpServers": {
"kei-mcp": {
"command": "$KEI_MCP_BIN",
"transport": "stdio"
}
}
}
JSON
)
if [ "${KEI_WIRE_DRY_RUN:-0}" = "1" ] || [ "${KEI_WIRE_CHECK:-0}" = "1" ]; then
echo " kimi: would merge into $CFG:"
printf '%s\n' "$desired"
exit 0
fi
tmp=$(mktemp)
jq -s '.[0] * .[1]' "$CFG" <(printf '%s\n' "$desired") > "$tmp"
mv "$tmp" "$CFG"
cat <<EOF
kimi: kei-mcp registered → $CFG
Alternative via Kimi CLI: kimi mcp add kei-mcp --transport stdio \\
--command "$KEI_MCP_BIN"
⚠ TIER 3 advisory: Kimi has only confirmation prompts, no allowlist.
Native shell remains reachable. Keep YOLO mode OFF for safety.
For patent-sensitive work, use Claude or Grok as orchestrator.
EOF

106
scripts/kei-mcp-wire.sh Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env bash
# kei-mcp-wire — orchestrator for cross-CLI MCP enforcement setup.
#
# Phase C cube — wires kei-mcp (with kei_bash/kei_edit/kei_write tools) into
# each installed LLM CLI's MCP config, plus per-CLI tool-restriction config
# where the CLI supports it.
#
# Usage:
# kei mcp-wire # detect installed CLIs + wire each
# kei mcp-wire <cli> # wire one: claude/grok/copilot/agy/kimi
# kei mcp-wire --check # diff: current vs target (no writes)
# kei mcp-wire --dry-run # preview changes without applying
# kei mcp-wire --list # show enforcement tier per CLI
#
# Enforcement tiers (3-tier honesty model):
# TIER 1 — full native: claude (existing hooks), grok (ports our hooks
# to ~/.grok/settings.json — same JSON shape)
# TIER 2 — MCP-wrapped: copilot (disable native shell + force kei_bash)
# TIER 3 — advisory: agy + kimi (cannot disable native shell;
# MCP available but enforcement is prompt-only)
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DRY_RUN=0
CHECK=0
LIST=0
TARGET=""
usage() { sed -n '2,17p' "$0" | sed 's|^# \{0,1\}||'; }
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--check) CHECK=1 ;;
--list) LIST=1 ;;
--help|-h) usage; exit 0 ;;
*) TARGET="$arg" ;;
esac
done
export KEI_WIRE_DRY_RUN="$DRY_RUN"
export KEI_WIRE_CHECK="$CHECK"
declare -A TIERS=(
[claude]="TIER 1: full native"
[grok]="TIER 1: full native (ports our hooks)"
[copilot]="TIER 2: MCP-wrapped (disable native shell)"
[agy]="TIER 3: advisory (no native-shell disable)"
[kimi]="TIER 3: advisory (confirmation model only)"
)
backend_bin() {
case "$1" in
claude) echo "claude" ;;
grok) echo "grok" ;;
agy|antigravity) echo "agy" ;;
copilot) echo "copilot" ;;
kimi) echo "kimi" ;;
*) return 1 ;;
esac
}
if [ "$LIST" = "1" ]; then
echo "Cross-CLI enforcement tiers:"
for cli in claude grok copilot agy kimi; do
bin=$(backend_bin "$cli")
if command -v "$bin" >/dev/null 2>&1; then
mark="✓"
else
mark="✗"
fi
printf " %s %-8s %s\n" "$mark" "$cli" "${TIERS[$cli]}"
done
exit 0
fi
wire_one() {
local cli="$1" wire_script="$SCRIPT_DIR/kei-mcp-wire-$cli.sh"
if [ ! -x "$wire_script" ]; then
echo "[kei-mcp-wire] no wire script for: $cli (expected $wire_script)" >&2
return 2
fi
local bin
bin=$(backend_bin "$cli") || { echo "unknown cli: $cli" >&2; return 2; }
if ! command -v "$bin" >/dev/null 2>&1; then
echo "[kei-mcp-wire] $cli not installed (skipping)"
return 0
fi
echo
echo "──── $cli (${TIERS[$cli]}) ────"
"$wire_script"
}
if [ -n "$TARGET" ]; then
wire_one "$TARGET"
exit $?
fi
# No target → wire all installed CLIs.
echo "kei-mcp-wire: detecting installed CLIs..."
for cli in claude grok copilot agy kimi; do
wire_one "$cli"
done
echo
echo "done. See \`kei mcp-wire --list\` for per-CLI enforcement tier."

71
scripts/kei-message.sh Executable file
View file

@ -0,0 +1,71 @@
#!/bin/sh
# kei-message — minimal persistent mailbox so ANY Claude Code session can message
# ANY other (not just Agent-Teams teammates). Append-only jsonl bus; the
# mailbox-inject.sh UserPromptSubmit hook pulls unread into each session's
# context per turn. Identity = basename of the session's cwd (or --from/--to a
# name), plus the broadcast channel "all".
#
# kei message send [--to <name|all>] [--from <name>] <text...>
# kei message inbox # messages addressed to me (cwd) or all
# kei message list # whole bus (recent)
# kei message channels # known recipient names
#
# Store: ~/.claude/mailbox/messages.jsonl (one JSON object per line)
set -eu
command -v jq >/dev/null 2>&1 || { echo "kei message: jq required" >&2; exit 1; }
MBOX="$HOME/.claude/mailbox"
LOG="$MBOX/messages.jsonl"
mkdir -p "$MBOX"
[ -f "$LOG" ] || : > "$LOG"
me="$(basename "$PWD")"
cmd="${1:-inbox}"
[ $# -gt 0 ] && shift || true
case "$cmd" in
send)
to="all"; body=""
while [ $# -gt 0 ]; do
case "$1" in
--to) to="$2"; shift; shift ;;
--from) me="$2"; shift; shift ;;
--) shift; body="$body $*"; break ;;
# Leading @name = recipient (e.g. `send @frontend hi`). First token only;
# a later @x stays literal text.
@?*) if [ "$to" = "all" ] && [ -z "$body" ]; then to="${1#@}"; else body="$body $1"; fi; shift ;;
*) body="$body $1"; shift ;;
esac
done
body="${body# }"
[ -n "$body" ] || { echo "usage: kei message send [--to <name|all>] <text>" >&2; exit 1; }
# Sub-second component: GNU `date +%N` where available; on stock macOS (BSD
# date) %N is unsupported and prints literal "N" → fall back to /dev/urandom.
# Result id = epoch(10) + 6 digits = 16-digit integer, safely < 2^53.
ns="$(date +%N 2>/dev/null)"
case "$ns" in *[!0-9]*|'') ns="$(od -An -N4 -tu4 /dev/urandom 2>/dev/null | tr -dc 0-9)" ;; esac
sub="$(printf '%s000000' "$ns" | cut -c1-6)"
id="$(date +%s)${sub}"
jq -cn --argjson id "$id" --arg ts "$(date -u +%FT%TZ)" \
--arg from "$me" --arg to "$to" --arg body "$body" \
'{id:$id, ts:$ts, from:$from, to:$to, body:$body}' >> "$LOG"
echo "-> sent to '$to' (from '$me')"
;;
inbox|read)
while [ $# -gt 0 ]; do case "$1" in --me) me="$2"; shift; shift ;; *) shift ;; esac; done
jq -r --arg me "$me" '
select(.to==$me or .to=="all")
| "[\(.ts|sub("T";" ")|sub("Z";""))] \(.from) -> \(.to): \(.body)"' "$LOG" | tail -20
;;
list|all)
jq -r '"[\(.ts|sub("T";" ")|sub("Z";""))] \(.from) -> \(.to): \(.body)"' "$LOG" | tail -40
;;
channels|names|who)
jq -r '.to, .from' "$LOG" 2>/dev/null | sort -u | grep -v '^$' || true
;;
*)
echo "kei message: send [--to <name|all>] <text> | inbox | list | channels" >&2
exit 1
;;
esac

191
scripts/kei-onboard.sh Executable file
View file

@ -0,0 +1,191 @@
#!/usr/bin/env bash
# kei-onboard — post-install wizard.
#
# Runs after install.sh / bootstrap.sh to guide the user through:
# Step 1: pick the primary LLM orchestrator (default for `kei` no-args)
# Step 2: wire kei-mcp into the chosen CLI (cross-CLI policy + spawn_agent)
# Step 3: optional MOONSHOT_API_KEY hint for kei limits
# Step 4: quick health check
#
# Idempotent — safe to re-run anytime via `kei onboard`.
# Honors TTY gate: non-interactive runs print summary + exit, no prompts.
set -eu
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
PICK_SH="$HOME/.claude/scripts/kei-pick.sh"
WIRE_SH="$HOME/.claude/scripts/kei-mcp-wire.sh"
# Colors only if stdout is a TTY (TTY-INTERACTIVITY-GATE: -t 1 for color is OK).
C0= CB= CC= CG= CD= CR=
if [ -t 1 ]; then
C0=$'\033[0m'
CB=$'\033[1;38;5;39m' # blue
CC=$'\033[1;38;5;220m' # gold
CG=$'\033[32m' # green
CR=$'\033[31m' # red
CD=$'\033[2m' # dim
fi
# Non-interactive (no stdin TTY): print summary + exit.
# Per tty-interactivity-gate.md: -t 0 not -t 1.
if [ ! -t 0 ]; then
cat <<EOF
${CB}KeiSeiKit · onboarding${C0} (non-interactive — wizard skipped)
Next manual steps:
${CC}kei onboard${C0} run this wizard interactively
${CC}kei pick${C0} pick primary LLM CLI
${CC}kei mcp-wire${C0} wire kei-mcp into your CLIs
${CC}kei limits${C0} check subscription quotas (honest report)
${CC}kei-doctor${C0} substrate health diagnostic
EOF
exit 0
fi
# Banner
cat <<EOF
${CB}╔═══════════════════════════════════════════════════════════════════╗
║ KeiSeiKit · post-install onboarding ║
╚═══════════════════════════════════════════════════════════════════╝${C0}
The install put 38 agents, 54 hooks, and 60+ Rust primitives in place.
Now let's wire up the LLM CLIs you'll actually use.
EOF
# ── Step 1: pick primary ───────────────────────────────────────────
echo "${CB}── Step 1/4 — Pick your primary LLM orchestrator ──${C0}"
echo
echo "When you run ${CC}kei${C0} (no args) it launches your primary CLI."
echo "Each agent's manifest can also declare a preferred provider (DNA)."
echo
declare -a BACKENDS=(claude grok agy copilot kimi)
declare -A LABELS=(
[claude]="Claude Code (Anthropic, full hook enforcement)"
[grok]="Grok (xAI, native --agent flag)"
[agy]="Antigravity (Google Gemini)"
[copilot]="GitHub Copilot (Microsoft, MCP-wrapped)"
[kimi]="Kimi (Moonshot, TUI-primary)"
)
i=1
for b in "${BACKENDS[@]}"; do
if command -v "$b" >/dev/null 2>&1; then
mark="${CG}${C0}"
else
mark="${CR}${C0} ${CD}(not installed)${C0}"
fi
printf " ${CB}%d${C0}) %s %-20s %s\n" "$i" "$mark" "$b" "${LABELS[$b]}"
i=$((i+1))
done
echo " ${CB}s${C0}) skip — keep current primary (claude default)"
echo
current=""
[ -f "$KEI_PRIMARY_CFG" ] && current=$(awk -F'=' '/^provider/ {gsub(/[" ]/, "", $2); print $2; exit}' "$KEI_PRIMARY_CFG")
printf "Current primary: ${CC}%s${C0}\n" "${current:-claude (default)}"
printf "Pick [1-${#BACKENDS[@]}/s, default=s]: "
read -r choice
choice="${choice:-s}"
primary_set=""
case "$choice" in
s|S|"")
echo " ${CD}— keeping ${current:-claude}${C0}"
primary_set="${current:-claude}"
;;
[1-9])
idx=$((choice-1))
if [ $idx -ge ${#BACKENDS[@]} ] || [ $idx -lt 0 ]; then
echo " ${CR}invalid; keeping ${current:-claude}${C0}"
primary_set="${current:-claude}"
else
new="${BACKENDS[$idx]}"
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
printf '# kei primary — written %s by onboarding\nprovider = "%s"\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$new" > "$KEI_PRIMARY_CFG"
echo " ${CG}${C0} primary set: ${CC}${new}${C0}$KEI_PRIMARY_CFG"
primary_set="$new"
fi
;;
*)
echo " ${CR}invalid; keeping ${current:-claude}${C0}"
primary_set="${current:-claude}"
;;
esac
# ── Step 2: mcp-wire ───────────────────────────────────────────────
echo
echo "${CB}── Step 2/4 — Wire kei-mcp into installed CLIs ──${C0}"
echo
echo "kei-mcp exposes ${CC}spawn_agent${C0} + ${CC}kei_bash/kei_edit/kei_write${C0} (with"
echo "policy chain) to any MCP-capable CLI. Enables cross-CLI agent invocation"
echo "AND hook enforcement on non-Claude backends."
echo
printf "Run ${CC}kei mcp-wire${C0} now (writes to ~/.grok/, ~/.copilot/, etc.)? [Y/n]: "
read -r wire_ans
wire_ans="${wire_ans:-Y}"
case "$wire_ans" in
y|Y|yes)
if [ -x "$WIRE_SH" ]; then
"$WIRE_SH"
else
echo " ${CR}$WIRE_SH not found; skip${C0}"
fi
;;
*)
echo " ${CD}— skipped. Run later: ${CC}kei mcp-wire${C0}${CD}${C0}"
;;
esac
# ── Step 3: MOONSHOT key hint ──────────────────────────────────────
echo
echo "${CB}── Step 3/4 — Live subscription limits (optional) ──${C0}"
echo
echo "${CC}kei limits${C0} probes each CLI's subscription quota. Research found that"
echo "only Kimi exposes a public API; the others are dashboard-only."
echo
if [ -n "${MOONSHOT_API_KEY:-}" ]; then
echo " ${CG}${C0} MOONSHOT_API_KEY is set — Kimi balance probing enabled"
else
cat <<EOF
${CD}Optional: set ${CC}MOONSHOT_API_KEY${CD} in ${CC}~/.claude/secrets/.env${CD} to enable
Kimi balance polling. Other CLIs: see dashboards via ${CC}kei limits${CD}.${C0}
EOF
fi
# ── Step 4: health check ───────────────────────────────────────────
echo
echo "${CB}── Step 4/4 — Health check ──${C0}"
echo
if command -v kei-doctor >/dev/null 2>&1; then
kei-doctor 2>&1 | head -20 || true
else
echo " ${CD}— kei-doctor not on PATH yet. Open new shell + run: ${CC}kei-doctor${C0}"
fi
# ── Done ───────────────────────────────────────────────────────────
cat <<EOF
${CB}╔═══════════════════════════════════════════════════════════════════╗
║ Onboarding complete. ║
╚═══════════════════════════════════════════════════════════════════╝${C0}
Quick-start:
${CC}kei${C0} launch ${primary_set} (your primary)
${CC}kei agent critic "..."${C0} invoke an agent (DNA → primary)
${CC}kei agent --on=grok critic "..."${C0} invoke on a specific backend
${CC}kei mcp-wire --list${C0} show enforcement tiers per CLI
${CC}kei limits${C0} quota report (where APIs exist)
${CC}kei pick${C0} re-pick primary anytime
${CC}kei configure${C0} re-pick hook packs / stack profile
Docs: ${CD}~/.local/share/keisei/docs/encyclopedia/${C0}
Logs: ${CD}~/.keisei-install.log${C0}
EOF

153
scripts/kei-pick.sh Executable file
View file

@ -0,0 +1,153 @@
#!/usr/bin/env bash
# kei-pick — interactive orchestrator picker.
#
# Shows installed LLM CLIs, lets the user choose one, writes it to
# ~/.claude/config/primary.toml, then exec's it (so the shell becomes
# the picked orchestrator). Designed for `kei pick`.
#
# Non-interactive (no TTY): just shows status and exits 0.
set -eu
KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}"
# Mirrors scripts/kei-agent-cli.sh::backend_bin and bin/kei::backend_bin_for.
backend_bin() {
case "$1" in
claude) echo "claude" ;;
grok) echo "grok" ;;
agy|antigravity) echo "agy" ;;
copilot) echo "copilot" ;;
kimi) echo "kimi" ;;
codex) echo "codex" ;;
*) return 1 ;;
esac
}
backend_label() {
case "$1" in
claude) echo "Claude Code (Anthropic)" ;;
grok) echo "Grok Build TUI (xAI)" ;;
agy) echo "Antigravity / Gemini (Google)" ;;
copilot) echo "GitHub Copilot CLI (Microsoft/GitHub)" ;;
kimi) echo "Kimi Code CLI (Moonshot) — TUI-only for agents" ;;
codex) echo "Codex CLI (OpenAI)" ;;
esac
}
current_primary() {
[ -f "$KEI_PRIMARY_CFG" ] || { echo "claude"; return; }
awk -F'=' '/^provider[[:space:]]*=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
gsub(/^"|"$/, "", $2)
print $2; exit
}' "$KEI_PRIMARY_CFG"
}
# --- list installed backends ------------------------------------------
BACKENDS=(claude grok agy copilot kimi codex)
INSTALLED=()
for b in "${BACKENDS[@]}"; do
bin=$(backend_bin "$b")
if command -v "$bin" >/dev/null 2>&1; then
INSTALLED+=("$b")
fi
done
cur=$(current_primary)
# --- non-interactive: just show status --------------------------------
# Gate on stdin (RULE TTY-INTERACTIVITY-GATE): -t 0, not -t 1.
# curl|bash tees stdout, so -t 1 false ≠ non-interactive.
if [ ! -t 0 ]; then
echo "kei pick — non-interactive mode"
echo "current primary: $cur"
echo "installed backends: ${INSTALLED[*]:-none}"
echo "(run \`kei pick\` from a real terminal for the picker)"
exit 0
fi
# --- interactive picker -----------------------------------------------
C0="" CB="" CC="" CD=""
if [ -t 1 ]; then
C0=$'\033[0m'
CB=$'\033[1;38;5;39m' # blue
CC=$'\033[1;38;5;220m' # gold
CD=$'\033[2m' # dim
fi
cat <<EOF
${CB}╔════════════════════════════════════════════╗
║ KeiSeiKit · orchestrator picker ║
╚════════════════════════════════════════════╝${C0}
Pick the LLM CLI that becomes your primary shell.
Any agent invocation (\`kei agent <name>\`) routes here unless DNA overrides.
EOF
i=1
for b in "${BACKENDS[@]}"; do
bin=$(backend_bin "$b")
label=$(backend_label "$b")
if command -v "$bin" >/dev/null 2>&1; then
mark="${CC}${C0}"
else
mark="${CD}${C0}"
label="$label ${CD}(not installed)${C0}"
fi
cur_mark=""
[ "$b" = "$cur" ] && cur_mark="${CC} ← current${C0}"
printf " ${CB}%d${C0}) %s %-10s %s%s\n" "$i" "$mark" "$b" "$label" "$cur_mark"
i=$((i+1))
done
echo
printf " ${CB}q${C0}) cancel (keep current: ${CC}%s${C0})\n\n" "$cur"
printf "Pick [1-${#BACKENDS[@]}/q]: "
read -r choice
choice="${choice:-q}"
case "$choice" in
q|Q|"") echo "cancelled."; exit 0 ;;
[1-9])
idx=$((choice-1))
if [ $idx -ge ${#BACKENDS[@]} ] || [ $idx -lt 0 ]; then
echo "invalid choice: $choice" >&2; exit 2
fi
new="${BACKENDS[$idx]}"
;;
*) echo "invalid choice: $choice" >&2; exit 2 ;;
esac
bin=$(backend_bin "$new")
if ! command -v "$bin" >/dev/null 2>&1; then
echo
echo "${CC}'$new' is not installed.${C0}"
echo "Set as primary anyway (you'll need to install it before \`kei\` will work)? [y/N]: "
read -r confirm
case "$confirm" in y|Y|yes) ;; *) echo "cancelled."; exit 0 ;; esac
fi
mkdir -p "$(dirname "$KEI_PRIMARY_CFG")"
printf '# kei primary — written %s\nprovider = "%s"\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$new" > "$KEI_PRIMARY_CFG"
echo
echo "${CC}${C0} primary set: $cur${CC}$new${C0}"
echo " config: $KEI_PRIMARY_CFG"
echo
if [ -n "${KEI_NO_LAUNCH:-}" ]; then
echo "(skipping launch — KEI_NO_LAUNCH set)"
exit 0
fi
if ! command -v "$bin" >/dev/null 2>&1; then
echo "${CD}skipping launch — $bin not on PATH; install it then run \`kei\`.${C0}"
exit 0
fi
echo "launching $new..."
exec "$bin"

View file

@ -1,12 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# KeiSei pet state updater — called by hooks to change the pet's mood. # KeiSei pet state updater — called by hooks to change the pet's mood and to
# track running sub-agents, current language, and plan completion.
# Usage: keisei-pet-update.sh <event> # Usage: keisei-pet-update.sh <event>
# Events: prompt | rust_write | github_block | python_no_reason | # Mood events: prompt | rust_write | github_block | python_no_reason |
# modal_cost | patent_filed | concept_saved | secret_leak | # modal_cost | patent_filed | concept_saved | secret_leak |
# test_pass | test_fail | sleep | rule_violation | idle # test_pass | test_fail | sleep | rule_violation | idle
# Agent events: agent_start | agent_done (read tool JSON on stdin)
# Plan event: plan (ExitPlanMode finished)
# Language: lang (reads .tool_input.file_path)
# #
# The hook may also pipe JSON tool-context on stdin; we ignore it for now # State lives under ~/.claude/pet/:
# (future: parse tool_input to make reactions smarter). # state — sourced shell vars (mood/message/since/day/counters/lang/plan)
# agents/<id> — one file per running sub-agent: "emoji|name|start_epoch"
# agent_tokens — cumulative tokens spent by sub-agents this session
set -u set -u
@ -15,95 +21,123 @@ STATE="${STATE_DIR}/state"
HISTORY="${STATE_DIR}/history.log" HISTORY="${STATE_DIR}/history.log"
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
# Load current state # Slurp stdin once (hook JSON). Non-blocking; never hang.
mood="neutral" INPUT=""
message="" if [ ! -t 0 ]; then INPUT="$(cat 2>/dev/null || true)"; fi
since=$(date +%s)
rust_today=0
patents_today=0
violations=0
# shellcheck source=/dev/null
[ -f "$STATE" ] && source "$STATE" 2>/dev/null || true
# Daily counter reset (if state last updated yesterday)
last_day=${day:-}
today=$(date +%Y-%m-%d)
if [ "$last_day" != "$today" ]; then
rust_today=0
patents_today=0
violations=0
fi
event="${1:-}" event="${1:-}"
now=$(date +%s) now=$(date +%s)
# Discard stdin quickly so hook doesn't block # ── language emoji map (agent emojis live in the renderer keisei-pet.sh) ─────
if [ ! -t 0 ]; then _lang_emoji() {
cat >/dev/null 2>&1 || true case "$1" in
fi rs) echo "🦀" ;;
py|pyi|pyw|ipynb) echo "🐍" ;;
go) echo "🐹" ;;
ts|tsx|mts|cts) echo "📘" ;;
js|jsx|mjs|cjs) echo "🟨" ;;
swift) echo "🦅" ;;
c|h) echo "🔧" ;;
cc|cpp|cxx|hpp|hh|hxx) echo "" ;;
java) echo "☕" ;;
kt|kts) echo "🟪" ;;
rb|erb|gemspec) echo "💎" ;;
sh|bash|zsh|fish) echo "🐚" ;;
md|mdx|markdown) echo "📝" ;;
toml|ini|cfg|conf|properties) echo "🧾" ;;
json|jsonc|json5) echo "📐" ;;
yaml|yml) echo "📋" ;;
html|htm|xhtml) echo "🌐" ;;
css|scss|sass|less) echo "🎨" ;;
sql) echo "🗄️" ;;
lua) echo "🌙" ;;
php) echo "🐘" ;;
zig) echo "⚡" ;;
dart) echo "🎯" ;;
scala|sc) echo "🔺" ;;
clj|cljs|cljc|edn) echo "🍃" ;;
ex|exs|eex|heex) echo "💧" ;;
erl|hrl) echo "📡" ;;
hs|lhs) echo "🎓" ;;
ml|mli|ocaml) echo "🐫" ;;
nim) echo "👑" ;;
cr) echo "🔮" ;;
r|rmd) echo "📊" ;;
jl) echo "🔢" ;;
v|vsh) echo "🅥" ;;
vala) echo "🏛️" ;;
groovy|gradle) echo "🍀" ;;
dockerfile) echo "🐳" ;;
mk|makefile|cmake) echo "🔨" ;;
proto) echo "🔌" ;;
graphql|gql) echo "◈" ;;
vue) echo "💚" ;;
svelte) echo "🧡" ;;
astro) echo "🚀" ;;
tf|tfvars|hcl) echo "🌍" ;;
pl|pm|perl) echo "🐪" ;;
ps1|psm1) echo "🔵" ;;
nix) echo "❄️" ;;
wasm|wat) echo "🕸️" ;;
xml) echo "📰" ;;
svg) echo "🖼️" ;;
csv|tsv) echo "📊" ;;
pdf) echo "📕" ;;
lock) echo "🔒" ;;
env) echo "🔑" ;;
txt|text) echo "📄" ;;
asm|s) echo "🛠️" ;;
f|f90|f95|fortran) echo "🧮" ;;
cs) echo "🟩" ;;
fs|fsx) echo "🔷" ;;
el|lisp|scm) echo "λ" ;;
*) echo "📄" ;;
esac
}
# ── load current state ──────────────────────────────────────────────────────
mood="neutral"; message=""; since="$now"; day=""
rust_today=0; patents_today=0; violations=0; lang=""; plan=""
# shellcheck source=/dev/null
[ -f "$STATE" ] && source "$STATE" 2>/dev/null || true
# Daily counter reset
today=$(date +%Y-%m-%d)
if [ "${day:-}" != "$today" ]; then rust_today=0; patents_today=0; violations=0; fi
# ── agent / plan / language events (do not change mood face) ─────────────────
case "$event" in case "$event" in
prompt) # NOTE: running-agent tracking + token/cost accounting are NOT done here —
mood="thinking" # the kit already does it (hooks/task-timer.sh → time-metrics/.task-*.start,
message="考えてる..." # hooks/agent-event-done.sh → agent-events.jsonl). keisei-pet.sh READS those
# (SSoT). This updater only owns mood / language / plan / counters.
plan)
plan="📋"; mood="proud"; message="план готов"
;; ;;
rust_write) lang)
rust_today=$((rust_today + 1)) fp="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)"
mood="happy" if [ -n "$fp" ]; then
message="構造式 ✓ Rust" ext="${fp##*.}"; ext="$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')"
;; lang="$(_lang_emoji "$ext")"
github_block) if [ "$ext" = "rs" ]; then rust_today=$((rust_today + 1)); mood="happy"; message="構造式 ✓ Rust"; fi
mood="angry" fi
message="RULE 0.1! no github"
violations=$((violations + 1))
;;
python_no_reason)
mood="alert"
message="Python? 理由は? (RULE 0.2)"
;;
modal_cost)
mood="alert"
message="\$\$ compute check"
;;
patent_filed)
mood="proud"
patents_today=$((patents_today + 1))
message="特許 filed!"
;;
concept_saved)
mood="happy"
message="💡 concept saved"
;;
secret_leak)
mood="angry"
message="SECRET! RULE 0.8"
violations=$((violations + 1))
;;
test_pass)
mood="happy"
message="テスト ✓"
;;
test_fail)
mood="sad"
message="テスト ✗"
;;
rule_violation)
mood="angry"
message="rule violation ⚠"
violations=$((violations + 1))
;;
sleep)
mood="sleep"
message="zzz"
;;
*)
# unknown event — no-op, keep current state
:
;; ;;
prompt) mood="thinking"; message="考えてる..." ;;
rust_write) rust_today=$((rust_today + 1)); mood="happy"; message="構造式 ✓ Rust"; lang="🦀" ;;
github_block) mood="angry"; message="RULE 0.1! no github"; violations=$((violations + 1)) ;;
python_no_reason) mood="alert"; message="Python? 理由は? (RULE 0.2)" ;;
modal_cost) mood="alert"; message="\$\$ compute check" ;;
patent_filed) mood="proud"; patents_today=$((patents_today + 1)); message="特許 filed!" ;;
concept_saved) mood="happy"; message="💡 concept saved" ;;
secret_leak) mood="angry"; message="SECRET! RULE 0.8"; violations=$((violations + 1)) ;;
test_pass) mood="happy"; message="テスト ✓" ;;
test_fail) mood="sad"; message="テスト ✗" ;;
rule_violation) mood="angry"; message="rule violation ⚠"; violations=$((violations + 1)) ;;
sleep) mood="sleep"; message="zzz"; plan="" ;;
*) : ;;
esac esac
# Write state atomically # ── write state atomically ──────────────────────────────────────────────────
tmp="${STATE}.tmp.$$" tmp="${STATE}.tmp.$$"
cat > "$tmp" <<EOF cat > "$tmp" <<EOF
mood="$mood" mood="$mood"
@ -113,14 +147,13 @@ day="$today"
rust_today=$rust_today rust_today=$rust_today
patents_today=$patents_today patents_today=$patents_today
violations=$violations violations=$violations
lang="$lang"
plan="$plan"
EOF EOF
mv "$tmp" "$STATE" mv "$tmp" "$STATE" 2>/dev/null || true
# Rolling history (last 50 events) printf "%s %s\n" "$(date -u +%FT%TZ)" "$event" >> "$HISTORY" 2>/dev/null || true
printf "%s %s\n" "$(date -u +%FT%TZ)" "$event" >> "$HISTORY" if [ -f "$HISTORY" ] && [ "$(wc -l < "$HISTORY" 2>/dev/null || echo 0)" -gt 50 ]; then
if [ -f "$HISTORY" ] && [ "$(wc -l < "$HISTORY")" -gt 50 ]; then tail -50 "$HISTORY" > "${HISTORY}.tmp" 2>/dev/null && mv "${HISTORY}.tmp" "$HISTORY" 2>/dev/null || true
tail -50 "$HISTORY" > "${HISTORY}.tmp" && mv "${HISTORY}.tmp" "$HISTORY"
fi fi
# Hooks in Claude Code expect exit 0 to pass through
exit 0 exit 0

View file

@ -1,67 +1,213 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# KeiSei tamagotchi — statusline renderer. # KeiSei tamagotchi — statusline renderer. Outputs ONE line.
# Called by Claude Code on every prompt render. Outputs ONE line. # SSoT: reads the kit's OWN tracking, does not maintain a parallel one.
# Reads state from ~/.claude/pet/state (written by keisei-pet-update.sh). # - running sub-agents ← ~/.claude/memory/time-metrics/.task-<id>.start
# (written by hooks/task-timer.sh: {id,desc,type,start_epoch})
# - agent token / cost ← ~/.claude/memory/agent-events.jsonl
# (written by hooks/agent-event-done.sh: {tokens,cost_usd,...})
# - mood / lang / plan ← ~/.claude/pet/state (keisei-pet-update.sh)
set -u set -u
# Claude Code pipes the live session JSON to the statusLine on stdin. Capture
# Discard any stdin (Claude Code may pipe session JSON to statusLine) # it (don't discard) — it carries this session's token/context/cost, which is
if [ ! -t 0 ]; then # what replaced the default statusline when the pet took over.
cat >/dev/null 2>&1 || true SLINE=""
fi if [ ! -t 0 ]; then SLINE="$(cat 2>/dev/null || true)"; fi
STATE="${HOME}/.claude/pet/state" STATE="${HOME}/.claude/pet/state"
TM_DIR="${HOME}/.claude/memory/time-metrics"
EVENTS="${HOME}/.claude/memory/agent-events.jsonl"
# Defaults (if state file missing/stale) mood="neutral"; message=""; since=$(date +%s)
mood="neutral" rust_today=0; patents_today=0; violations=0; lang=""; plan=""
message=""
since=$(date +%s)
rust_today=0
patents_today=0
violations=0
# shellcheck source=/dev/null # shellcheck source=/dev/null
[ -f "$STATE" ] && source "$STATE" 2>/dev/null || true [ -f "$STATE" ] && source "$STATE" 2>/dev/null || true
now=$(date +%s) now=$(date +%s)
idle=$((now - since)) dim=$'\033[2m'; reset=$'\033[0m'
# Idle >5 min → pet sleeps (unless it's angry/alert about something) _agent_emoji() {
if [ "$idle" -gt 300 ] && [ "$mood" != "angry" ] && [ "$mood" != "alert" ]; then case "$1" in
mood="sleep" # ── project specialists (match before generic families) ──
message="zzz" *cartoon*) echo "🎬" ;;
*cloudsync*) echo "🔄" ;;
*vortex*) echo "🌀" ;;
*recruiter*) echo "🧑‍💼" ;;
*leadgen*) echo "🎯" ;;
*surf*) echo "🏄" ;;
*neuralcloak*) echo "🕶️" ;;
*openclaw*) echo "🦞" ;;
*keit0*|*keisense*) echo "🖐️" ;;
*wave*) echo "🌊" ;;
*cortex*) echo "🧬" ;;
*keimd*) echo "🕸️" ;;
*keisei-os*|*keiseios*) echo "🧩" ;;
*sa-specialist*|*sa_specialist*) echo "🏝️" ;;
# ── kit agent families ──
*researcher*) echo "🔬" ;;
*architect*) echo "🏗️" ;;
*critic*) echo "🔪" ;;
*security*) echo "🛡️" ;;
*validator*) echo "✅" ;;
*cost*) echo "💰" ;;
*modal*) echo "☁️" ;;
*fal*) echo "🎨" ;;
*ml-implementer*|*ml_implementer*) echo "🧠" ;;
*ml-researcher*|*ml_researcher*) echo "📚" ;;
*infra*) echo "🔧" ;;
*implementer*) echo "⚙️" ;;
*patent*) echo "📜" ;;
*frontend*) echo "🎨" ;;
*debug*) echo "🐞" ;;
*guide*) echo "📖" ;;
Explore|*explore*) echo "🔭" ;;
Plan|*plan*) echo "📐" ;;
*general*) echo "🤖" ;;
*) echo "🤖" ;;
esac
}
_elapsed() {
local s=$1
if [ "$s" -lt 60 ]; then printf '%ds' "$s"
elif [ "$s" -lt 3600 ]; then printf '%dm' $(( s / 60 ))
else printf '%dh%dm' $(( s / 3600 )) $(( (s % 3600) / 60 )); fi
}
# ── running sub-agents (count only — compact view, no per-agent list) ────────
# Counts younger-than-2h .task-*.start markers across ALL parallel sessions.
# v0.40: dropped per-agent emoji+name list to keep status line readable when
# many parallel sessions/agents fire. Per-agent detail moved to `kei status`
# (see TODO) — pet stays as a single counter.
n_agents=0
if [ -d "$TM_DIR" ]; then
for f in "$TM_DIR"/.task-*.start; do
[ -f "$f" ] || continue
st="$(jq -r '.start_epoch // empty' "$f" 2>/dev/null)"
[ -z "$st" ] && continue
age=$(( now - st ))
[ "$age" -gt 7200 ] && continue
n_agents=$((n_agents+1))
done
fi fi
# Face + color by mood # ── today's aggregates (across ALL sessions) ─────────────────────────────────
# Tokens + cost from agent-events.jsonl; sessions from distinct parent_id of
# today's agent_spawn events.
today_tok=0; today_cost=0; today_sess=0
if [ -f "$EVENTS" ]; then
today="$(date -u +%Y-%m-%d)"
# Single awk pass: count tokens, cost, distinct parent_id.
read -r today_tok today_cost today_sess < <(awk -v d="$today" '
index($0,d)>0 {
if (match($0,/total_tokens[^0-9]*[0-9]+/)) { s=substr($0,RSTART,RLENGTH); gsub(/[^0-9]/,"",s); T+=s }
if (match($0,/"cost_usd"[: ]*[0-9.]+/)) { s=substr($0,RSTART,RLENGTH); gsub(/[^0-9.]/,"",s); C+=s }
if (match($0,/"parent_id"[: ]*"[^"]+"/)) { s=substr($0,RSTART,RLENGTH); gsub(/.*"parent_id"[: ]*"|".*/,"",s); seen[s]=1 }
} END {
n=0; for (k in seen) n++
printf "%d %.4f %d", T+0, C+0, n
}' "$EVENTS" 2>/dev/null)
fi
# Format tokens compactly: 1234567 → 1.2M / 5400 → 5k / 999 → 999
_short_tok() {
local n=${1:-0}
if [ "$n" -ge 1000000 ]; then awk -v n="$n" 'BEGIN{printf "%.1fM", n/1000000}'
elif [ "$n" -ge 1000 ]; then awk -v n="$n" 'BEGIN{printf "%dk", n/1000}'
else printf '%d' "$n"
fi
}
global=""
[ "${today_sess:-0}" -gt 0 ] 2>/dev/null && global+="💬${today_sess} "
[ "${today_tok:-0}" -gt 0 ] 2>/dev/null && global+="🌍$(_short_tok "$today_tok") "
[ "${n_agents:-0}" -gt 0 ] 2>/dev/null && global+="🤖${n_agents} "
spend=""
if [ "${today_cost:-0}" != "0.0000" ] && [ -n "${today_cost:-}" ]; then
spend="💰\$$(printf '%.2f' "$today_cost" 2>/dev/null)"
fi
[ -n "$spend" ] && global+="${spend} "
global="${global% }"
# v0.43: CLI subscription limits (best-effort).
# Pet does NOT poll — reads cache only. Cache populated by `kei limits`.
# Reality: 4 of 5 CLIs have no programmatic limit API (see research). Pet
# shows only what's actually available + how stale the cache is.
limits_cache="${HOME}/.claude/pet/limits-cache.json"
limits=""
if [ -f "$limits_cache" ]; then
# Cache age in seconds.
cache_ts=$(jq -r '.ts // empty' "$limits_cache" 2>/dev/null)
if [ -n "$cache_ts" ]; then
# Convert ISO8601 to epoch (macOS + Linux compatible).
cache_epoch=$(
date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$cache_ts" "+%s" 2>/dev/null \
|| date -u -d "$cache_ts" "+%s" 2>/dev/null \
|| echo 0
)
cache_age=$(( now - cache_epoch ))
# Kimi balance (only CLI with live API). Show $X.XX if available.
kimi_avail=$(jq -r '.kimi | select(.status=="live") | .available_balance_usd' "$limits_cache" 2>/dev/null)
if [ -n "$kimi_avail" ] && [ "$kimi_avail" != "null" ]; then
limits+="K:\$$(printf '%.2f' "$kimi_avail" 2>/dev/null) "
fi
# Stale marker if older than 1h.
if [ "$cache_age" -gt 3600 ] 2>/dev/null && [ -n "$limits" ]; then
stale_min=$((cache_age / 60))
limits="${limits% }${dim}(${stale_min}m old)${reset} "
fi
fi
fi
limits="${limits% }"
# ── THIS session: tokens + context% (from statusLine stdin) ─────────────────
sess=""
if [ -n "$SLINE" ]; then
read -r s_in s_out s_pct < <(printf '%s' "$SLINE" | jq -r '[
(.context_window.total_input_tokens // 0),
(.context_window.total_output_tokens // 0),
(.context_window.used_percentage // 0)] | @tsv' 2>/dev/null)
st=$(( ${s_in:-0} + ${s_out:-0} ))
if [ "$st" -gt 0 ] 2>/dev/null; then
if [ "$st" -ge 1000000 ]; then tk="$(awk "BEGIN{printf \"%.1fM\",$st/1000000}")"
elif [ "$st" -ge 1000 ]; then tk="$(( st / 1000 ))k"
else tk="$st"; fi
pct="${s_pct%%.*}"; pcol=$'\033[32m'
[ "${pct:-0}" -ge 70 ] 2>/dev/null && pcol=$'\033[33m'
[ "${pct:-0}" -ge 90 ] 2>/dev/null && pcol=$'\033[31m'
sess="🪙${tk} ${pcol}${pct}%${reset}"
fi
fi
# ── mood face ───────────────────────────────────────────────────────────────
idle=$(( now - since ))
if [ "$idle" -gt 300 ] && [ "$mood" != "angry" ] && [ "$mood" != "alert" ] && [ "$n_agents" -eq 0 ]; then
mood="sleep"; message="zzz"
fi
case "$mood" in case "$mood" in
happy) face="(ᵔᴥᵔ)"; color=$'\033[32m' ;; # green happy) face="(ᵔᴥᵔ)"; color=$'\033[32m';; proud) face="(•̀ᴗ•́)و"; color=$'\033[1;32m';;
proud) face="(•̀ᴗ•́)و"; color=$'\033[1;32m';; # bright green thinking) face="(⊙.⊙)"; color=$'\033[36m';; alert) face="(ʘᴗʘ)"; color=$'\033[33m';;
thinking) face="(⊙.⊙)"; color=$'\033[36m' ;; # cyan angry) face="(ò_ó)"; color=$'\033[31m';; sad) face="(╥﹏╥)"; color=$'\033[34m';;
alert) face="(ʘᴗʘ)"; color=$'\033[33m' ;; # yellow sleep) face="(-.-)"; color=$'\033[2;37m';; *) face="(•ᴗ•)"; color=$'\033[37m';;
angry) face="(ò_ó)"; color=$'\033[31m' ;; # red
sad) face="(╥﹏╥)"; color=$'\033[34m' ;; # blue
sleep) face="(-.-)"; color=$'\033[2;37m';; # dim gray
*) face="(•ᴗ•)"; color=$'\033[37m' ;; # white (neutral)
esac esac
dim=$'\033[2m'
reset=$'\033[0m'
# stats line (compact)
stats="" stats=""
[ "$rust_today" -gt 0 ] && stats+=" 🦀${rust_today}" [ "${rust_today:-0}" -gt 0 ] 2>/dev/null && stats+=" 🦀${rust_today}"
[ "$patents_today" -gt 0 ] && stats+=" 📜${patents_today}" [ "${patents_today:-0}" -gt 0 ] 2>/dev/null && stats+=" 📜${patents_today}"
[ "$violations" -gt 0 ] && stats+="${violations}" # recent errors — from the kit's error-spike-detector rolling window (SSoT)
errn=0
EWIN="${HOME}/.claude/memory/error-window.txt"
[ -f "$EWIN" ] && errn="$(awk '$2==1' "$EWIN" 2>/dev/null | wc -l | tr -d ' ')"
[ "${errn:-0}" -gt 0 ] 2>/dev/null && stats+=" $(printf '\033[31m')${errn}${reset}"
[ "${violations:-0}" -gt 0 ] 2>/dev/null && stats+="${violations}"
proj="${PWD##*/}"; [ -z "$proj" ] && proj="~"
# Project name from PWD out=""
proj="${PWD##*/}" [ -n "$sess" ] && out+="${sess} "
[ -z "$proj" ] && proj="~" [ -n "$global" ] && out+="${dim}${global}${reset} "
[ -n "$limits" ] && out+="${dim}${limits}${reset} "
# Render: face | message | stats | project [ -n "$plan" ] && out+="${plan} "
# Keep it ≤ one line out+="${color}${face}${reset}"
printf "%s%s%s %s%s%s%s%s %s%s%s" \ [ -n "$message" ] && out+=" ${dim}${message}${reset}"
"$color" "$face" "$reset" \ out+="${stats}"
"$dim" "$message" "$reset" \ [ -n "$lang" ] && out+=" ${lang}"
"$stats" \ out+=" ${dim}📁 ${proj}${reset}"
"" \ printf '%s' "$out"
"$dim" "📁 $proj" "$reset"

View file

@ -26,7 +26,7 @@
}, },
{ {
"type": "command", "type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path // empty'); [ -n \"$FILE\" ] && [ \"${FILE##*.}\" = 'rs' ] && ~/.claude/scripts/keisei-pet-update.sh rust_write; exit 0" "command": "~/.claude/scripts/keisei-pet-update.sh lang"
} }
] ]
}, },
@ -62,6 +62,19 @@
"type": "command", "type": "command",
"command": "~/.claude/hooks/agent-stub-scan.sh", "command": "~/.claude/hooks/agent-stub-scan.sh",
"statusMessage": "STATUS-TRUTH marker scan (RULE 0.16)..." "statusMessage": "STATUS-TRUTH marker scan (RULE 0.16)..."
},
{
"type": "command",
"command": "~/.claude/hooks/agent-event-done.sh"
}
]
},
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/keisei-pet-update.sh plan"
} }
] ]
}, },
@ -189,6 +202,10 @@
{ {
"type": "command", "type": "command",
"command": "~/.claude/hooks/task-timer.sh" "command": "~/.claude/hooks/task-timer.sh"
},
{
"type": "command",
"command": "~/.claude/hooks/agent-event-spawn.sh"
} }
] ]
} }
@ -224,6 +241,11 @@
{ {
"type": "command", "type": "command",
"command": "~/.claude/scripts/keisei-pet-update.sh prompt" "command": "~/.claude/scripts/keisei-pet-update.sh prompt"
},
{
"type": "command",
"command": "~/.claude/hooks/mailbox-inject.sh",
"statusMessage": "kei mailbox pull-inbox..."
} }
] ]
} }
@ -262,6 +284,18 @@
} }
] ]
} }
],
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/first-run-onboard.sh",
"statusMessage": "KeiSeiKit first-run onboard nudge..."
}
]
}
] ]
} }
} }

View file

@ -27,7 +27,7 @@ Store the reply verbatim as `REPO`.
} }
``` ```
Store as `PLATFORM`. If `Both` is selected, emit a one-line confirm: "You understand — only non-patent code ever pushes to GitHub?" and wait for a `y` typed reply before proceeding. Store as `PLATFORM`. If `Both` is selected, emit a one-line confirm: "You understand — only public-safe code ever pushes to GitHub?" and wait for a `y` typed reply before proceeding.
## 1c — Languages click (AskUserQuestion, multi-select) ## 1c — Languages click (AskUserQuestion, multi-select)

62
skills/msg/SKILL.md Normal file
View file

@ -0,0 +1,62 @@
---
name: msg
description: Read or write the cross-session mailbox by @id. Send a message to another Claude Code session (`/msg @name text`), read your own inbox (`/msg` with no args), broadcast to everyone (`/msg all text`), list the whole bus, or discover who is reachable. Thin wrapper over the `kei message` jsonl mailbox — messages land in the recipient's NEXT turn via the mailbox-inject hook (pull, not push). Use whenever the user wants sessions/agents to talk to each other.
argument-hint: "[@name] <message> | (empty = read inbox) | list | who"
---
# /msg — Inter-Session Mailbox
A persistent append-only bus so ANY Claude Code session can message ANY other —
not just Agent-Teams teammates, no tmux, no daemon. Backed by
`~/.claude/scripts/kei-message.sh` writing `~/.claude/mailbox/messages.jsonl`.
The `mailbox-inject.sh` UserPromptSubmit hook pulls each session's unread into
its context once per turn, so delivery is **pull** (arrives on the recipient's
next turn), not instant push.
## Identity model
- **Your address** = the basename of this session's working directory (`$PWD`).
So a session running in `~/Projects/frontend` is reachable as `@frontend`.
- **`all`** is the broadcast channel — every session sees `to:"all"` messages.
- You can override the sender with `--from <name>` and the reader identity with
`--me <name>` if a session's cwd basename isn't the name you want to use.
## Command map
Interpret `$ARGUMENTS` and run the matching command via Bash, then show its
output to the user. The launcher `kei message …` and the script path are
equivalent — prefer the script path (always present after install):
| User typed | Run |
|---|---|
| `/msg` (no args) | `~/.claude/scripts/kei-message.sh inbox` |
| `/msg @frontend ship it` | `~/.claude/scripts/kei-message.sh send @frontend ship it` |
| `/msg all standup in 5` | `~/.claude/scripts/kei-message.sh send all standup in 5` |
| `/msg list` | `~/.claude/scripts/kei-message.sh list` |
| `/msg who` (or `channels`) | `~/.claude/scripts/kei-message.sh channels` |
Rules for parsing `$ARGUMENTS`:
1. **Empty** → read inbox (`inbox`). Show the messages addressed to this
session or to `all`.
2. **Starts with `@<name>`** → send to that recipient; the rest is the body.
A `@x` that appears later in the body stays literal text.
3. **Starts with `all `** → broadcast; the rest is the body.
4. **`list`** → print the recent whole bus (every from→to line).
5. **`who` / `channels`** → print known recipient names (use this to discover
who is reachable before sending the first message).
6. Anything else with no leading `@`/`all` → treat as a broadcast body, OR ask
the user who the recipient is if it's ambiguous.
## Discovery (first-message problem)
A recipient only appears in `who` after it has sent or been sent a message, so
for the very first contact either broadcast with `all`, or ask the user for the
target session's cwd-basename. Don't invent a recipient name.
## Notes
- Sending never blocks and never notifies the recipient out-of-band — they see
it on their next turn. For a time-sensitive ping, tell the user it's queued.
- This is plain files: `cat ~/.claude/mailbox/messages.jsonl` is the raw bus.
- Bypass the inject hook for a session with `KEI_MAILBOX_BYPASS=1`.

View file

@ -14,7 +14,7 @@
# #
# Env / args: # Env / args:
# KEISEI_ROOT install dir (default: $HOME/.local/share/keisei) # KEISEI_ROOT install dir (default: $HOME/.local/share/keisei)
# KEISEI_REPO git URL (default: https://keigit.com/keisei/KeiSeiKit-1.0.git) # KEISEI_REPO git URL (default: https://github.com/KeiSeiLab/KeiSeiKit-1.0.git)
# KEISEI_REF branch/tag/sha (default: main) # KEISEI_REF branch/tag/sha (default: main)
# --profile=NAME passed through to ./bootstrap.sh # --profile=NAME passed through to ./bootstrap.sh
# --yes passed through to ./bootstrap.sh # --yes passed through to ./bootstrap.sh
@ -24,7 +24,7 @@
set -euo pipefail set -euo pipefail
KEISEI_ROOT="${KEISEI_ROOT:-$HOME/.local/share/keisei}" KEISEI_ROOT="${KEISEI_ROOT:-$HOME/.local/share/keisei}"
KEISEI_REPO="${KEISEI_REPO:-https://keigit.com/keisei/KeiSeiKit-1.0.git}" KEISEI_REPO="${KEISEI_REPO:-https://github.com/KeiSeiLab/KeiSeiKit-1.0.git}"
KEISEI_REF="${KEISEI_REF:-main}" KEISEI_REF="${KEISEI_REF:-main}"
PASS_THROUGH=() PASS_THROUGH=()
@ -78,6 +78,13 @@ mkdir -p "$(dirname "$KEISEI_ROOT")"
if [ -d "$KEISEI_ROOT/.git" ]; then if [ -d "$KEISEI_ROOT/.git" ]; then
say "pulling $KEISEI_REF in $KEISEI_ROOT" say "pulling $KEISEI_REF in $KEISEI_ROOT"
git -C "$KEISEI_ROOT" fetch --depth=1 origin "$KEISEI_REF" git -C "$KEISEI_ROOT" fetch --depth=1 origin "$KEISEI_REF"
# reset --hard discards ANY local edits in this managed clone. It's a managed
# dir (don't hand-edit it — change the repo + push instead), but warn loudly
# so a manual tweak isn't lost silently.
if ! git -C "$KEISEI_ROOT" diff --quiet 2>/dev/null \
|| ! git -C "$KEISEI_ROOT" diff --cached --quiet 2>/dev/null; then
say " ⚠ local changes in $KEISEI_ROOT will be DISCARDED by reset --hard"
fi
git -C "$KEISEI_ROOT" reset --hard "origin/$KEISEI_REF" git -C "$KEISEI_ROOT" reset --hard "origin/$KEISEI_REF"
else else
say "cloning $KEISEI_REPO ($KEISEI_REF) → $KEISEI_ROOT" say "cloning $KEISEI_REPO ($KEISEI_REF) → $KEISEI_ROOT"
@ -91,11 +98,20 @@ say "delegating to $KEISEI_ROOT/bootstrap.sh ${PASS_THROUGH[*]:-}"
cd "$KEISEI_ROOT" cd "$KEISEI_ROOT"
# curl|bash сценарий: stdin = pipe от curl, поэтому wizard'у read нечего читать. # curl|bash сценарий: stdin = pipe от curl, поэтому wizard'у read нечего читать.
# Если есть /dev/tty (т.е. сессия реально интерактивная), переподключаем stdin # Если /dev/tty реально ОТКРЫВАЕТСЯ (сессия интерактивная), запускаем bootstrap
# к терминалу — иначе onboarding/whiptail падают на первом prompt. # со stdin = терминал — иначе onboarding/whiptail падают на первом prompt.
# audit 2026-05-18 bug #4. # ВАЖНО #1: `[ -r /dev/tty ]` недостаточно — путь может stat'иться readable, но
if [ -r /dev/tty ] && [ ! -t 0 ]; then # `exec < /dev/tty` падает с "No such device or address" когда нет управляющего
exec < /dev/tty # терминала (ssh non-interactive, CI, cron). Поэтому пробуем реально открыть его
# через `{ : < /dev/tty; }` и реаттачим ТОЛЬКО при успехе.
# ВАЖНО #2: bash читает ЭТОТ скрипт из трубы curl ПОБАЙТНО. Отдельный
# `exec < /dev/tty` заставил бы bash читать СЛЕДУЮЩУЮ строку (сам запуск
# bootstrap) уже с клавиатуры → вечный висяк после "delegating". Поэтому редирект
# и exec ОБЯЗАНЫ быть ОДНОЙ командой: подменили stdin и сразу заменили процесс —
# bash больше не читает ни байта скрипта из (уже не той) трубы.
# audit 2026-05-18 bug #4; non-TTY e2e fix 2026-05-21; curl|bash hang fix 2026-05-22.
if [ ! -t 0 ] && { : < /dev/tty; } 2>/dev/null; then
exec ./bootstrap.sh "${PASS_THROUGH[@]}" < /dev/tty
fi fi
exec ./bootstrap.sh "${PASS_THROUGH[@]}" exec ./bootstrap.sh "${PASS_THROUGH[@]}"