KeiSeiKit-1.0/install/lib-profile-outcome-only.sh
Parfii-bot 8a885a7d76 fix(release+slices): v0.14.4 publish auth fallback + 4 fix-implementer slices
After v0.14.3 npm-publish failed again with 401 Unauthorized despite
path-scoped _authToken. Direct curl probe to keigit confirmed BOTH Bearer
and Basic auth schemes work — so the issue is npm 10 not sending the
auth header in CI. Likely cause: deprecated `always-auth=true` interfered
with token resolution.

== Publish auth fix ==
- Drop `always-auth=true` (deprecated in npm 10+; warns in logs)
- Keep path-scoped `_authToken` (npm 10 canonical)
- Add legacy Basic-auth fallback rows (username/_password/email) — Forgejo
  accepts both schemes per direct probe; if one resolution path fails,
  npm tries the other
- chmod 600 on $HOME/.npmrc and project .npmrc (defense-in-depth)
- Bump 0.14.3 → 0.14.4

== Slice A — TS server hardening (Sonnet code-implementer-typescript) ==
File: _ts_packages/packages/mcp-server/src/server.ts (+3/-1)
File: _ts_packages/packages/mcp-server/src/index.ts (+14/-4)
- safeEqual constant-time path on length mismatch (timing oracle close)
- HTTP server defaults to 127.0.0.1 bind; --bind <addr> opt-in for 0.0.0.0
- Body cap 1 MiB with 413 response (DoS prevention)
- VERIFIED: tsc -b --noEmit exit 0

== Slice B — Outcome-only profile hardening (Sonnet code-implementer) ==
Files: install.sh, install/lib-args.sh, install/lib-profile-outcome-only.sh
- Confirm-screen gate before destructive install (skips on --dry-run / --yes)
- _outcome_install_ledger return value tracked → summary reflects reality
  (was: false-success "ledger: ..." when init failed)
- --dry-run silent-ignored on non-outcome profiles → now warns
- VERIFIED: end-to-end smoke against fake $HOME with `<<< "y"` — all 5
  files installed, schema v9 + 2 triggers, summary correct

== Slice D — jq-merge dedup tuple (Sonnet code-implementer) ==
File: install/lib-hooks.sh
- Replaced `unique_by(.command)` with reduce-into-object keyed on
  norm-ed command (tilde-vs-absolute path collision fix)
- Snippet-wins precedence on collision
- 3 manual scenario traces pass: tilde+tilde, absolute+tilde, idempotency

== Slice E — Doc honesty pass (Sonnet code-implementer, selective-merged) ==
Files: README.md, docs/{INSTALL,ARCHITECTURE,PROFILE-OUTCOME-ONLY}.md
Note: Slice E worktree was based on an older main commit; merged
selectively to preserve current-main values (565 DNAs, not worktree's 518)
- README:62 plugin marketplace URL: KeiSei84/KeiSeiKit → KeiSei84/KeiSeiKit-1.0
  (consistent with line 66 git clone URL + Cargo.toml repository field)
- README:9-15: per-claim [REAL: <command>] markers on all 8 numerics
- README:124-132 + PROFILE-OUTCOME-ONLY.md:43-55 + ARCHITECTURE.md:288-302:
  rephrase 100-row router claim — now describes Wilson lower-bound
  (δ=0.10, q*=0.70) continuous metric with file:line pointer to select.rs
- INSTALL.md: ESTIMATE-HTC marker covering all install-time / disk-size
  numerics in profile table (RULE 0.18 compliance)
- PROFILE-OUTCOME-ONLY.md privacy section: discloses agent-toolstats.jsonl
  sidecar (was undocumented per W3 finding)
- PROFILE-OUTCOME-ONLY.md uninstall: added 6th rm -f for .bak-* cleanup
  (closes orphan-accumulation per W3+W4 audits)

[FROM-JOURNAL: tasks.jsonl this session — 12 audit agents waves 5+6 +
4 parallel fix-implementer worktrees ran ~25 min wall-time]

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

200 lines
7.9 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"
# Downgrade guard: skip init if DB is at a newer schema (user_version > 9).
if [ -f "$db" ] && command -v sqlite3 >/dev/null 2>&1; then
local current_v
current_v=$(sqlite3 "$db" "PRAGMA user_version;" 2>/dev/null || echo 0)
if [ "${current_v:-0}" -gt 9 ] 2>/dev/null; then
say "ledger already at schema v$current_v (>9); skipping init to preserve newer schema"
return 0
fi
fi
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 1
}
# Append STATUS-TRUTH MARKER instruction to CLAUDE.md (idempotent).
_outcome_install_claude_md() {
local cm="$HOME_DIR/.claude/CLAUDE.md"
mkdir -p "$HOME_DIR/.claude"
# Match HTML comment marker (not generic "STATUS-TRUTH MARKER" text) to avoid
# false-positive skip when user already has RULE 0.16 docs in CLAUDE.md.
if [ -f "$cm" ] && grep -qF '<!-- outcome-only profile (KeiSeiKit) -->' "$cm"; then
say "CLAUDE.md already contains outcome-only marker; 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)"
}
# Confirm gate (Fix 1): show plan + prompt; skip for dry-run or --yes.
_outcome_confirm_if_needed() {
[ "${OUTCOME_DRY_RUN:-0}" = "1" ] && return 0
[ "${ASSUME_YES:-0}" = "1" ] && return 0
say "Outcome-only profile will install:"
say " - 2 hooks (~/.claude/hooks/agent-outcome-backfill.sh, error-spike-detector.sh)"
say " - SQLite ledger (~/.claude/agents/ledger.sqlite)"
say " - 1 line in ~/.claude/CLAUDE.md (STATUS-TRUTH MARKER instruction)"
say " - jq-merge of 2 hook entries into ~/.claude/settings.json"
say " - kei-model-router binary (deferred if cargo missing)"
printf "Continue? [y/N] "
read -r _oc_ans
case "$_oc_ans" in
[Yy]*) ;;
*) say "Aborted."; exit 0 ;;
esac
}
# Copy the 2 hook files to HOOKS_DIR.
_outcome_install_hooks() {
local hook_src hook_dst
mkdir -p "$HOOKS_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
}
# Write or jq-merge the minimal settings-snippet into settings.json.
_outcome_merge_settings() {
local snippet
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
# cp -p aside (not backup_file which MOVES) + register in BACKUP_PAIRS for rollback.
local _ts _bak
_ts=$(date +%s)
_bak="$HOME_DIR/.claude/settings.json.bak-$_ts"
cp -p "$HOME_DIR/.claude/settings.json" "$_bak"
BACKUP_PAIRS+=("$HOME_DIR/.claude/settings.json|$_bak")
if ! _jq_merge_hooks "$snippet" "$HOME_DIR/.claude/settings.json"; then
err "settings.json merge failed; rollback trap will restore from $_bak"
rm -f "$snippet"; return 1
fi
rm -f "$_bak"
fi
rm -f "$snippet"
}
# Public entry — called from install.sh when --profile=outcome-only.
install_profile_outcome_only() {
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 "$AGENTS_DIR"
_outcome_install_hooks || return $?
# Fix 2: track ledger install result so summary reflects reality
local ledger_ok=1
_outcome_install_ledger || ledger_ok=0
_outcome_install_claude_md
_outcome_install_router_if_cargo
_outcome_merge_settings || return $?
say "outcome-only profile installed."
say " hooks: agent-outcome-backfill.sh, error-spike-detector.sh"
if [ "$ledger_ok" = "1" ]; then
say " ledger: $AGENTS_DIR/ledger.sqlite"
else
warn " ledger: NOT INSTALLED — backfill hook will be silent no-op until sqlite3/kei-ledger is available"
fi
say " CLAUDE.md updated (1 line appended)"
say " router: built (if cargo present), else deferred — see docs/PROFILE-OUTCOME-ONLY.md"
}