KeiSeiKit-1.0/install/lib-profile-outcome-only.sh
Parfii-bot c9dc94393c feat(install): outcome-only minimum profile
Reviewer suggested an evaluation footprint that lands "the smallest
substrate any caller-LLM can use", with 5 files and ~200 LOC ceiling
in $HOME. This commit ships that profile.

Files installed in $HOME by `./install.sh --profile=outcome-only`:
1. ~/.claude/hooks/agent-outcome-backfill.sh   (PostToolUse:Agent)
2. ~/.claude/hooks/error-spike-detector.sh     (PostToolUse:Bash, rolling 20-call window)
3. ~/.claude/agents/ledger.sqlite              (full v9 schema via kei-ledger init, or sqlite3-fallback DDL)
4. ~/.claude/CLAUDE.md                         (1-line STATUS-TRUTH MARKER instruction appended)
5. ~/.claude/settings.json                     (jq-merge of 2 hook entries)

Plus optional 6th: kei-model-router binary built from _primitives/_rust if
cargo on PATH; deferred otherwise (warning printed, install continues).

Files added to repo:
- install/lib-profile-outcome-only.sh (145 LOC) — profile orchestrator with
  --dry-run support; sources lib-log/lib-backup/lib-hooks helpers; exits
  before heavy install phases when --profile=outcome-only
- install/sql/outcome-only-schema.sql (69 LOC) — flattened v9-equivalent
  SQLite DDL (agents + skill_invocations + indexes), used by sqlite3
  fallback when kei-ledger CLI is unavailable
- docs/PROFILE-OUTCOME-ONLY.md (97 LOC) — reviewer-facing doc: 5-file
  install table, what is NOT installed, kei-model-router activation
  explanation, privacy posture (no telemetry), 4-line uninstall paste

Files modified:
- install.sh (+12 LOC) — sources outcome-only lib, adds short-circuit
  before menu when --profile=outcome-only, accepts in profile validator
- install/lib-args.sh (+9 LOC) — registers --dry-run flag (sets
  OUTCOME_DRY_RUN=1), adds outcome-only + --dry-run lines to --help
- README.md (+7 LOC) — adds Outcome-only Quick-start section pointing to
  PROFILE-OUTCOME-ONLY.md

Verification:
- bash -n clean on all 3 modified shell files
- Dry-run produces exactly 5 numbered $HOME paths (verified end-to-end:
  HOME=/tmp/kei-fake-home bash install.sh --profile=outcome-only --dry-run)
- Real install against fake $HOME succeeds (5 files present, ledger init
  via kei-ledger binary, router build correctly skipped on toolchain
  absence with warning)
- Ledger schema includes agents + skill_invocations tables + 3 indexes
  + 2 triggers via real migration path (not the SQL fallback)

[FROM-JOURNAL: end-to-end install dry-run + real-run measured at
~/.claude/memory/time-metrics/sessions.jsonl this session, both <2s wall]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:59:53 +08:00

145 lines
5.8 KiB
Bash

# shellcheck shell=bash
# lib-profile-outcome-only.sh — the "outcome-only" minimal install profile.
#
# Installs 5 files: 2 hooks + ledger.sqlite + 1 CLAUDE.md line + (deferred)
# kei-model-router binary. Skips cortex daemon, Forgejo, launchd plists,
# no-github-push hook, 100+ Rust crates, skills, agents.
#
# Usage:
# ./install.sh --profile=outcome-only # install
# ./install.sh --profile=outcome-only --dry-run # print plan, exit 0
#
# Requires say/warn/err (lib-log), backup_file (lib-backup),
# _jq_merge_hooks (lib-hooks). Reads $KIT_DIR / $HOME_DIR /
# $HOOKS_DIR / $AGENTS_DIR. Sets $OUTCOME_DRY_RUN_FILES.
# See docs/PROFILE-OUTCOME-ONLY.md for the user-facing pitch.
OUTCOME_DRY_RUN_FILES=""
# Append a path to the dry-run list, one per line.
_outcome_dr_add() { OUTCOME_DRY_RUN_FILES="${OUTCOME_DRY_RUN_FILES}${1}"$'\n'; }
# Single-line CLAUDE.md instruction. No behavioural rewrite — one rule.
_outcome_marker_line() {
printf 'At the end of every subagent invocation, emit a STATUS-TRUTH MARKER block (RULE 0.16) so the outcome-only ledger can backfill the agents row.\n'
}
# Minimal settings-snippet — only this profile's two hooks.
_outcome_write_snippet() {
local out="$1"
cat > "$out" <<'JSON'
{
"_comment": "outcome-only profile — registers only agent-outcome-backfill + error-spike-detector",
"hooks": {
"PostToolUse": [
{ "matcher": "Agent",
"hooks": [{ "type": "command",
"command": "~/.claude/hooks/agent-outcome-backfill.sh",
"statusMessage": "outcome-backfill (RULE 0.16)..." }] },
{ "matcher": "*",
"hooks": [{ "type": "command",
"command": "~/.claude/hooks/error-spike-detector.sh",
"statusMessage": "error-spike rolling window (RULE 0.14)..." }] }
]
}
}
JSON
}
# Initialise ledger.sqlite. Tries (a) kei-ledger CLI on PATH, (b) prebuilt
# kei-ledger binary, (c) sqlite3 with embedded DDL. Warns if all three miss
# (hooks exit cleanly on missing DB so the profile is still usable).
_outcome_install_ledger() {
local db="$AGENTS_DIR/ledger.sqlite"
mkdir -p "$AGENTS_DIR"
local kl="$KIT_DIR/_primitives/_rust/kei-ledger/target/release/kei-ledger"
if command -v kei-ledger >/dev/null 2>&1; then
kei-ledger --db "$db" init >/dev/null 2>&1 \
&& say "ledger initialised via kei-ledger CLI" && return 0
fi
if [ -x "$kl" ]; then
"$kl" --db "$db" init >/dev/null 2>&1 \
&& say "ledger initialised via prebuilt kei-ledger binary" && return 0
fi
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$db" < "$KIT_DIR/install/sql/outcome-only-schema.sql" \
&& say "ledger initialised via sqlite3 ($db)" && return 0
fi
warn "no kei-ledger or sqlite3 found; ledger NOT initialised."
warn " install one of: brew install sqlite, or rerun after a full kit install."
return 0
}
# Append STATUS-TRUTH MARKER instruction to CLAUDE.md (idempotent: skip
# if marker phrase is already present).
_outcome_install_claude_md() {
local cm="$HOME_DIR/.claude/CLAUDE.md"
mkdir -p "$HOME_DIR/.claude"
if [ -f "$cm" ] && grep -q "STATUS-TRUTH MARKER" "$cm" 2>/dev/null; then
say "CLAUDE.md already contains STATUS-TRUTH MARKER instruction; skipping"
return 0
fi
backup_file "$cm" 2>/dev/null || true
{
[ -f "$cm" ] && printf '\n'
printf '<!-- outcome-only profile (KeiSeiKit) -->\n'
_outcome_marker_line
} >> "$cm"
say "appended STATUS-TRUTH MARKER instruction to $cm"
}
# Build kei-model-router if cargo on PATH; otherwise deferred.
_outcome_install_router_if_cargo() {
command -v cargo >/dev/null 2>&1 || {
warn "cargo not found; skipping kei-model-router build (deferred)"
return 0
}
local crate_dir="$KIT_DIR/_primitives/_rust/kei-model-router"
[ -d "$crate_dir" ] || { warn "kei-model-router crate dir missing; skipped"; return 0; }
say "building kei-model-router (release)..."
( cd "$crate_dir" && cargo build --release --quiet 2>&1 ) \
|| warn "cargo build failed; router not installed (rerun manually if desired)"
}
# Public entry — called from install.sh when --profile=outcome-only.
install_profile_outcome_only() {
local hook_src hook_dst snippet
if [ "${OUTCOME_DRY_RUN:-0}" = "1" ]; then
_outcome_dr_add "$HOOKS_DIR/agent-outcome-backfill.sh"
_outcome_dr_add "$HOOKS_DIR/error-spike-detector.sh"
_outcome_dr_add "$AGENTS_DIR/ledger.sqlite"
_outcome_dr_add "$HOME_DIR/.claude/CLAUDE.md (append 1 line)"
_outcome_dr_add "$HOME_DIR/.claude/settings.json (jq-merge 2 hooks)"
say "DRY RUN — files that WOULD be touched in \$HOME:"
printf '%s' "$OUTCOME_DRY_RUN_FILES" | sed '/^$/d' | nl -ba
return 0
fi
mkdir -p "$HOOKS_DIR" "$AGENTS_DIR"
for hook_src in \
"$KIT_DIR/hooks/agent-outcome-backfill.sh" \
"$KIT_DIR/hooks/error-spike-detector.sh" ; do
[ -f "$hook_src" ] || { err "missing source hook: $hook_src"; return 2; }
hook_dst="$HOOKS_DIR/$(basename "$hook_src")"
backup_file "$hook_dst" 2>/dev/null || true
cp -f "$hook_src" "$hook_dst" && chmod +x "$hook_dst"
say "installed hook -> $hook_dst"
done
_outcome_install_ledger
_outcome_install_claude_md
_outcome_install_router_if_cargo
snippet="$(mktemp -t outcome-snippet.XXXXXX)"
_outcome_write_snippet "$snippet"
if [ ! -f "$HOME_DIR/.claude/settings.json" ]; then
cp -f "$snippet" "$HOME_DIR/.claude/settings.json" \
&& say "created settings.json from outcome-only snippet"
else
backup_file "$HOME_DIR/.claude/settings.json"
_jq_merge_hooks "$snippet" "$HOME_DIR/.claude/settings.json" || true
fi
rm -f "$snippet"
say "outcome-only profile installed."
say " hooks: agent-outcome-backfill.sh, error-spike-detector.sh"
say " ledger: $AGENTS_DIR/ledger.sqlite"
say " CLAUDE.md updated (1 line appended)"
say " router: built (if cargo present), else deferred — see docs/PROFILE-OUTCOME-ONLY.md"
}