diff --git a/bridges/kimaki.sh b/bridges/kimaki.sh index ede70b4..e28b86d 100644 --- a/bridges/kimaki.sh +++ b/bridges/kimaki.sh @@ -211,17 +211,20 @@ _kimaki_find_native_binary() { # _kimaki_register_runtime_signature # -# Kimaki 0.13 does not currently export stable session/thread attribution env -# vars to OpenCode/tool subprocesses. Keep this hook so upgrades remove stale -# Kimaki runtime-signature blocks from previous wp-coding-agents releases. -# Rich Discord thread attribution needs upstream Kimaki support first: +# Kimaki does not currently export stable session/thread/channel attribution env +# vars to OpenCode/tool subprocesses. Register the documented upstream #137 +# contract now so the DMC worktree signature and the invocation-scoped Homeboy +# notification adapter consume the same values when Kimaki starts exporting them. +# Until then both consumers remain fail-closed. # https://github.com/remorses/kimaki/issues/137 _kimaki_register_runtime_signature() { - if ! declare -F runtime_signature_unregister >/dev/null; then + if ! declare -F runtime_signature_register >/dev/null; then return 0 fi - runtime_signature_unregister "kimaki" + runtime_signature_register \ + "kimaki" \ + '{"session_id":"KIMAKI_SESSION_ID","thread_id":"KIMAKI_THREAD_ID","channel_id":"KIMAKI_CHANNEL_ID"}' } _kimaki_sync_bin_helpers() { @@ -234,9 +237,32 @@ _kimaki_sync_bin_helpers() { _kimaki_remove_legacy_session_helper "$HELPER_DIR" _kimaki_remove_legacy_command_shims "$HELPER_DIR" + _kimaki_install_homeboy_notification_context_helper "$HELPER_DIR" _kimaki_install_dispatch_helpers } +_kimaki_install_homeboy_notification_context_helper() { + local helper_dir="$1" + local source="$SCRIPT_DIR/bridges/kimaki/homeboy-notification-context.sh" + local target="$helper_dir/wp-coding-agents-homeboy-notification" + + [ -f "$source" ] || return 0 + if [ "${DRY_RUN:-false}" = true ]; then + if ! cmp -s "$source" "$target" 2>/dev/null; then + echo -e "${BLUE}[dry-run]${NC} Would update $target" + fi + return 0 + fi + + mkdir -p "$helper_dir" + if ! cmp -s "$source" "$target" 2>/dev/null; then + cp "$source" "$target" + chmod 0755 "$target" + log " Updated $target" + UPDATED_ITEMS+=("Kimaki Homeboy notification wrapper") + fi +} + _kimaki_install_dispatch_helpers() { if [ "${LOCAL_MODE:-false}" = true ] || [ -z "${SERVICE_USER:-}" ] || [ "$SERVICE_USER" = "root" ]; then return 0 @@ -1208,6 +1234,6 @@ bridge_vps_start_preamble() { # Verify-block addendum printed by upgrade.sh after the standard status line. bridge_verify_extra() { local PLUGINS_DIR="${RESOLVED_KIMAKI_PLUGINS_DIR:-/opt/kimaki-config/plugins}" - echo "test -f $PLUGINS_DIR/dm-context-filter.ts && test -f $PLUGINS_DIR/dm-agent-sync.ts # DM OpenCode plugins installed" + echo "test -f $PLUGINS_DIR/dm-context-filter.ts && test -f $PLUGINS_DIR/dm-agent-sync.ts && test -f $PLUGINS_DIR/homeboy-notification-context.ts # managed OpenCode plugins installed" echo "command -v kimaki >/dev/null # native Kimaki binary available" } diff --git a/bridges/kimaki/homeboy-notification-context.sh b/bridges/kimaki/homeboy-notification-context.sh new file mode 100644 index 0000000..2323b20 --- /dev/null +++ b/bridges/kimaki/homeboy-notification-context.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Invocation-scoped Kimaki -> Homeboy notification context. +# +# This runs inside the OpenCode shell tool subprocess, where upstream Kimaki +# may export attribution for this invocation. Do not move this mapping into the +# OpenCode server process: it can host concurrent Discord sessions. +set -eu + +snowflake_re='^[0-9][0-9]*$' +route='' + +if [ -n "${KIMAKI_THREAD_ID:-}" ] && printf '%s' "$KIMAKI_THREAD_ID" | grep -Eq "$snowflake_re" && [ "${#KIMAKI_THREAD_ID}" -ge 17 ] && [ "${#KIMAKI_THREAD_ID}" -le 20 ]; then + route="discord:v1:thread:$KIMAKI_THREAD_ID" +elif [ -n "${KIMAKI_CHANNEL_ID:-}" ] && printf '%s' "$KIMAKI_CHANNEL_ID" | grep -Eq "$snowflake_re" && [ "${#KIMAKI_CHANNEL_ID}" -ge 17 ] && [ "${#KIMAKI_CHANNEL_ID}" -le 20 ]; then + route="discord:v1:channel:$KIMAKI_CHANNEL_ID" +fi + +if [ -n "$route" ]; then + export HOMEBOY_NOTIFICATION_TRANSPORT='discord.run-completion' + export HOMEBOY_NOTIFICATION_ROUTE="$route" +else + unset HOMEBOY_NOTIFICATION_TRANSPORT HOMEBOY_NOTIFICATION_ROUTE +fi + +exec homeboy "$@" diff --git a/bridges/kimaki/plugins/homeboy-notification-context.ts b/bridges/kimaki/plugins/homeboy-notification-context.ts new file mode 100644 index 0000000..d631ccd --- /dev/null +++ b/bridges/kimaki/plugins/homeboy-notification-context.ts @@ -0,0 +1,25 @@ +// homeboy-notification-context.ts — Invocation-scoped Kimaki -> Homeboy context. +// +// Kimaki's documented attribution variables are inherited by an OpenCode tool +// subprocess. This plugin redirects Homeboy to the managed wrapper in that +// subprocess; the wrapper maps a validated destination without reading or +// writing the shared OpenCode server environment. + +import type { Plugin } from "@opencode-ai/plugin"; + +const HOMEBOY_COMMAND = /(^|[;&|()\s])homeboy(?=\s|$)/g; + +const homeboyNotificationContext: Plugin = async () => { + return { + "tool.execute.before": async (_input, output: { args?: Record }) => { + const command = output.args?.command; + if (typeof command !== "string" || !/(^|[;&|()\s])homeboy(?=\s|$)/.test(command)) { + return; + } + + output.args.command = command.replace(HOMEBOY_COMMAND, "$1wp-coding-agents-homeboy-notification"); + }, + }; +}; + +export default homeboyNotificationContext; diff --git a/bridges/kimaki/post-upgrade.sh b/bridges/kimaki/post-upgrade.sh index 60ed2d1..55bd248 100755 --- a/bridges/kimaki/post-upgrade.sh +++ b/bridges/kimaki/post-upgrade.sh @@ -67,7 +67,7 @@ else SKILLS_DIR="/usr/lib/node_modules/kimaki/skills" fi -REQUIRED_PLUGINS=(dm-context-filter.ts dm-agent-sync.ts) +REQUIRED_PLUGINS=(dm-context-filter.ts dm-agent-sync.ts homeboy-notification-context.ts) WP_CODING_AGENTS_SKILLS=(upgrade-wp-coding-agents) if [[ -n "${KIMAKI_DIST_DIR:-}" ]]; then @@ -274,7 +274,7 @@ if [[ -d "$PLUGIN_SOURCE_DIR" ]]; then fi fi else - echo "kimaki-config: WARNING: persistent plugin source dir not found at $PLUGIN_SOURCE_DIR; dm-context-filter.ts and dm-agent-sync.ts cannot be loaded" + echo "kimaki-config: WARNING: persistent plugin source dir not found at $PLUGIN_SOURCE_DIR; managed OpenCode plugins cannot be loaded" fi missing_required_plugins=0 diff --git a/lib/repair-opencode-json.py b/lib/repair-opencode-json.py index 626a3c4..55aff06 100755 --- a/lib/repair-opencode-json.py +++ b/lib/repair-opencode-json.py @@ -70,7 +70,7 @@ from typing import List, Tuple -MANAGED_KIMAKI_PLUGIN_NAMES = {"dm-context-filter.ts", "dm-agent-sync.ts"} +MANAGED_KIMAKI_PLUGIN_NAMES = {"dm-context-filter.ts", "dm-agent-sync.ts", "homeboy-notification-context.ts"} DM_MEMORY_MARKER = "/datamachine-files/" @@ -102,6 +102,7 @@ def expected_plugins( if chat_bridge == "kimaki": plugins.append(f"{kimaki_plugins_dir}/dm-context-filter.ts") plugins.append(f"{kimaki_plugins_dir}/dm-agent-sync.ts") + plugins.append(f"{kimaki_plugins_dir}/homeboy-notification-context.ts") if claude_code_auth_plugin: plugins.append(claude_code_auth_plugin) diff --git a/operator-entrypoints/wp-coding-agents-setup/verify.md b/operator-entrypoints/wp-coding-agents-setup/verify.md index 24c6217..c8ab01c 100644 --- a/operator-entrypoints/wp-coding-agents-setup/verify.md +++ b/operator-entrypoints/wp-coding-agents-setup/verify.md @@ -101,13 +101,13 @@ Local Kimaki plugin paths: ```bash KIMAKI_PLUGINS_DIR="$(npm root -g)/kimaki/plugins" -test -f "$KIMAKI_PLUGINS_DIR/dm-context-filter.ts" && test -f "$KIMAKI_PLUGINS_DIR/dm-agent-sync.ts" +test -f "$KIMAKI_PLUGINS_DIR/dm-context-filter.ts" && test -f "$KIMAKI_PLUGINS_DIR/dm-agent-sync.ts" && test -f "$KIMAKI_PLUGINS_DIR/homeboy-notification-context.ts" ``` VPS Kimaki plugin paths: ```bash -test -f /opt/kimaki-config/plugins/dm-context-filter.ts && test -f /opt/kimaki-config/plugins/dm-agent-sync.ts +test -f /opt/kimaki-config/plugins/dm-context-filter.ts && test -f /opt/kimaki-config/plugins/dm-agent-sync.ts && test -f /opt/kimaki-config/plugins/homeboy-notification-context.ts ``` If either plugin file is missing, rerun setup or upgrade before trusting a new OpenCode session. OpenCode silently skips missing plugin files. diff --git a/runtimes/opencode.sh b/runtimes/opencode.sh index 62f7459..2516d82 100644 --- a/runtimes/opencode.sh +++ b/runtimes/opencode.sh @@ -187,7 +187,7 @@ runtime_generate_config() { # Resolve Kimaki plugin dir + copy plugin files FIRST, unconditionally. # Setup.sh must be idempotent: whether this site has a fresh install or an # existing opencode.json, the kimaki plugins dir on disk must end up with - # the current dm-context-filter.ts + dm-agent-sync.ts. dm-agent-sync only + # the current managed Kimaki plugins. dm-agent-sync only # recomposes Data Machine memory; it must not write config.agent.* prompts. # Previously this only ran on fresh installs because the whole function # early-returned on an existing file, which left upgraded installs missing @@ -201,6 +201,7 @@ runtime_generate_config() { mkdir -p "$KIMAKI_PLUGINS_DIR" cp "$SCRIPT_DIR/bridges/kimaki/plugins/dm-context-filter.ts" "$KIMAKI_PLUGINS_DIR/" 2>/dev/null || true cp "$SCRIPT_DIR/bridges/kimaki/plugins/dm-agent-sync.ts" "$KIMAKI_PLUGINS_DIR/" 2>/dev/null || true + cp "$SCRIPT_DIR/bridges/kimaki/plugins/homeboy-notification-context.ts" "$KIMAKI_PLUGINS_DIR/" 2>/dev/null || true fi else KIMAKI_PLUGINS_DIR="/opt/kimaki-config/plugins" @@ -238,6 +239,7 @@ runtime_generate_config() { if [ "$CHAT_BRIDGE" = "kimaki" ]; then OPENCODE_PLUGINS="${OPENCODE_PLUGINS}\n \"${KIMAKI_PLUGINS_DIR}/dm-context-filter.ts\"," OPENCODE_PLUGINS="${OPENCODE_PLUGINS}\n \"${KIMAKI_PLUGINS_DIR}/dm-agent-sync.ts\"," + OPENCODE_PLUGINS="${OPENCODE_PLUGINS}\n \"${KIMAKI_PLUGINS_DIR}/homeboy-notification-context.ts\"," fi if opencode_claude_code_auth_enabled; then OPENCODE_PLUGINS="${OPENCODE_PLUGINS}\n \"$(opencode_claude_code_auth_plugin_path)\"," diff --git a/scripts/kimaki-managed-plugin-rig.mjs b/scripts/kimaki-managed-plugin-rig.mjs index 67725ca..21bfc2a 100755 --- a/scripts/kimaki-managed-plugin-rig.mjs +++ b/scripts/kimaki-managed-plugin-rig.mjs @@ -7,7 +7,7 @@ // cycles, and proves the OpenCode plugin hooks execute against Kimaki's live // installed prompt renderer. -import { execFileSync, execSync } from 'node:child_process' +import { execFileSync, execSync, spawn } from 'node:child_process' import crypto from 'node:crypto' import fs from 'node:fs' import os from 'node:os' @@ -34,10 +34,12 @@ const stagedSkillsDir = path.join(kimakiConfigDir, 'skills') const npmSkillsDir = path.join(tempRoot, 'npm-kimaki-skills') const siteDir = path.join(tempRoot, 'site') const homeDir = path.join(tempRoot, 'home') +const binDir = path.join(tempRoot, 'bin') const repoKimakiDir = path.join(repoRoot, 'bridges', 'kimaki') const repoPluginsDir = path.join(repoKimakiDir, 'plugins') const postUpgradePath = path.join(kimakiConfigDir, 'post-upgrade.sh') +const stagedNotificationWrapper = path.join(binDir, 'wp-coding-agents-homeboy-notification') const artifacts = { schema: 'wp-coding-agents/kimaki-managed-plugin-rig/v1', @@ -84,9 +86,13 @@ function stageManagedConfig() { mkdirp(npmSkillsDir) mkdirp(siteDir) mkdirp(homeDir) + mkdirp(binDir) copyFile(path.join(repoPluginsDir, 'dm-context-filter.ts'), path.join(stagedPluginsDir, 'dm-context-filter.ts')) copyFile(path.join(repoPluginsDir, 'dm-agent-sync.ts'), path.join(stagedPluginsDir, 'dm-agent-sync.ts')) + copyFile(path.join(repoPluginsDir, 'homeboy-notification-context.ts'), path.join(stagedPluginsDir, 'homeboy-notification-context.ts')) + copyFile(path.join(repoKimakiDir, 'homeboy-notification-context.sh'), stagedNotificationWrapper) + fs.chmodSync(stagedNotificationWrapper, 0o755) copyFile(path.join(repoKimakiDir, 'post-upgrade.sh'), postUpgradePath) fs.chmodSync(postUpgradePath, 0o755) @@ -110,6 +116,7 @@ function writeOpencodeConfig() { plugin: [ path.join(stagedPluginsDir, 'dm-context-filter.ts'), path.join(stagedPluginsDir, 'dm-agent-sync.ts'), + path.join(stagedPluginsDir, 'homeboy-notification-context.ts'), ], instructions: [], } @@ -121,6 +128,8 @@ function recordStaticEvidence() { artifacts.files['site/opencode.json'] = fileRecord(path.join(siteDir, 'opencode.json')) artifacts.files['kimaki-config/plugins/dm-context-filter.ts'] = fileRecord(path.join(stagedPluginsDir, 'dm-context-filter.ts')) artifacts.files['kimaki-config/plugins/dm-agent-sync.ts'] = fileRecord(path.join(stagedPluginsDir, 'dm-agent-sync.ts')) + artifacts.files['kimaki-config/plugins/homeboy-notification-context.ts'] = fileRecord(path.join(stagedPluginsDir, 'homeboy-notification-context.ts')) + artifacts.files['bin/wp-coding-agents-homeboy-notification'] = fileRecord(stagedNotificationWrapper) artifacts.files['kimaki-config/post-upgrade.sh'] = fileRecord(postUpgradePath) for (const candidate of ['skills-enable-list.txt', 'skills-disable-list.txt']) { const file = path.join(kimakiConfigDir, candidate) @@ -405,6 +414,8 @@ async function runCycle({ name, simulatePackageWipe }) { assert(fs.existsSync(path.join(stagedSkillsDir, 'upgrade-wp-coding-agents', 'SKILL.md')), `${name}: persistent upgrade skill source remains present`, cycle) assert(fs.existsSync(path.join(stagedPluginsDir, 'dm-context-filter.ts')), `${name}: context filter present after restart`, cycle) assert(fs.existsSync(path.join(stagedPluginsDir, 'dm-agent-sync.ts')), `${name}: agent sync present after restart`, cycle) + assert(fs.existsSync(path.join(stagedPluginsDir, 'homeboy-notification-context.ts')), `${name}: notification adapter present after restart`, cycle) + assert(fs.existsSync(stagedNotificationWrapper), `${name}: notification wrapper survives package wipe`, cycle) const permission = expectedSkillPermission() assert(permission?.['*'] === 'deny', `${name}: generated skill permission denies unlisted skills`, cycle) @@ -424,6 +435,56 @@ async function runCycle({ name, simulatePackageWipe }) { assert(promptEvidence.joinedSystemStaleOrchestrationLeaks.length === 0, `${name}: final system transform strips Kimaki promptAsync system field`, cycle) assert(promptEvidence.systemAndMessageTransformsAgree, `${name}: system and message transforms agree`, cycle) assert(promptEvidence.agentSyncLoaded, `${name}: dm-agent-sync module loads`, cycle) + await assertNotificationAdapter(name, cycle) +} + +async function assertNotificationAdapter(name, cycle) { + const module = await import(pathToFileURL(path.join(stagedPluginsDir, 'homeboy-notification-context.ts')).href) + const plugin = await module.default({}) + const before = plugin['tool.execute.before'] + if (typeof before !== 'function') { + throw new Error('homeboy-notification-context did not expose tool.execute.before') + } + + const output = { args: { command: 'homeboy status' } } + await before({}, output) + assert(output.args.command === 'wp-coding-agents-homeboy-notification status', `${name}: Homeboy command is routed through invocation wrapper`, cycle) + + const absent = await runNotificationWrapper({ HOMEBOY_NOTIFICATION_ROUTE: 'discord:v1:thread:11111111111111111' }) + assert(absent.transport === '' && absent.route === '', `${name}: absent Kimaki attribution omits notification context`, cycle) + + const [first, second] = await Promise.all([ + runNotificationWrapper({ KIMAKI_THREAD_ID: '11111111111111111' }), + runNotificationWrapper({ KIMAKI_THREAD_ID: '22222222222222222' }), + ]) + assert(first.route === 'discord:v1:thread:11111111111111111' && second.route === 'discord:v1:thread:22222222222222222', `${name}: concurrent thread invocations retain distinct notification routes`, cycle) +} + +function runNotificationWrapper(extraEnv) { + const fakeHomeboy = path.join(binDir, 'homeboy') + if (!fs.existsSync(fakeHomeboy)) { + fs.writeFileSync(fakeHomeboy, '#!/bin/sh\nprintf "%s\\n%s\\n" "${HOMEBOY_NOTIFICATION_TRANSPORT:-}" "${HOMEBOY_NOTIFICATION_ROUTE:-}"\n', 'utf8') + fs.chmodSync(fakeHomeboy, 0o755) + } + return new Promise((resolve, reject) => { + const child = spawn(stagedNotificationWrapper, ['status'], { + env: { ...process.env, ...extraEnv, PATH: `${binDir}:${process.env.PATH || ''}` }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + let stdout = '' + let stderr = '' + child.stdout.on('data', (chunk) => { stdout += chunk }) + child.stderr.on('data', (chunk) => { stderr += chunk }) + child.on('error', reject) + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`notification wrapper failed (${code}): ${stderr}`)) + return + } + const [transport = '', route = ''] = stdout.trimEnd().split('\n') + resolve({ transport, route }) + }) + }) } function runPostUpgrade(name) { diff --git a/skills/upgrade-wp-coding-agents/SKILL.md b/skills/upgrade-wp-coding-agents/SKILL.md index bc7c9c4..ebf883b 100644 --- a/skills/upgrade-wp-coding-agents/SKILL.md +++ b/skills/upgrade-wp-coding-agents/SKILL.md @@ -40,7 +40,7 @@ The user says something like: 3. **Restart the detected chat bridge.** The script prints the exact restart command for the detected bridge × environment. Run that command after a successful upgrade so the new bridge config, OpenCode plugins, skills, and prompt patches take effect immediately. If the restart command is unavailable or fails, report that as incomplete work. -4. **After restart, verify Kimaki's OpenCode plugins when Kimaki + OpenCode are in use.** The summary's verify block includes a `test -f .../dm-context-filter.ts && test -f .../dm-agent-sync.ts` command. Run it, then inspect the Kimaki startup logs for `kimaki-config: WARNING:` lines. Any warning about a missing persistent plugin source dir or missing required OpenCode plugin means `opencode.json` may reference plugin files OpenCode silently skipped. +4. **After restart, verify Kimaki's OpenCode plugins when Kimaki + OpenCode are in use.** The summary's verify block checks the managed plugin files. Run it, then inspect the Kimaki startup logs for `kimaki-config: WARNING:` lines. Any warning about a missing persistent plugin source dir or missing required OpenCode plugin means `opencode.json` may reference plugin files OpenCode silently skipped. 5. **Verify the filter behavior from the repo when available.** Run: ```bash diff --git a/tests/kimaki-launchd-start.sh b/tests/kimaki-launchd-start.sh index 3198978..8a449ea 100755 --- a/tests/kimaki-launchd-start.sh +++ b/tests/kimaki-launchd-start.sh @@ -32,6 +32,7 @@ chmod +x "$TMP/bin/kimaki" TEST_TMP="$TMP" \ PATH="$TMP/bin:/usr/bin:/bin" \ KIMAKI_DATA_DIR="$TMP/data" \ +KIMAKI_CONFIG_DIR="$TMP/data/kimaki-config" \ "$SCRIPT_DIR/bridges/kimaki/launchd-start.sh" "$TMP/bin/kimaki" --data-dir "$TMP/data" --auto-restart if ! grep -q -- '-TERM -f opencode-ai/bin/.*serve' "$TMP/pkill.log"; then diff --git a/tests/kimaki-managed-plugin-rig.sh b/tests/kimaki-managed-plugin-rig.sh index b4e8bf9..04f6f83 100755 --- a/tests/kimaki-managed-plugin-rig.sh +++ b/tests/kimaki-managed-plugin-rig.sh @@ -139,6 +139,11 @@ for (const cycle of manifest.cycles) { `${cycle.name}: dm-context-filter hook executed`, `${cycle.name}: system and message transforms agree`, `${cycle.name}: dm-agent-sync module loads`, + `${cycle.name}: notification adapter present after restart`, + `${cycle.name}: notification wrapper survives package wipe`, + `${cycle.name}: Homeboy command is routed through invocation wrapper`, + `${cycle.name}: absent Kimaki attribution omits notification context`, + `${cycle.name}: concurrent thread invocations retain distinct notification routes`, ]) { if (!labels.includes(expected)) { throw new Error(`missing check: ${expected}`); diff --git a/tests/opencode-local-plugin-path.sh b/tests/opencode-local-plugin-path.sh index b7b1786..83334f6 100755 --- a/tests/opencode-local-plugin-path.sh +++ b/tests/opencode-local-plugin-path.sh @@ -45,6 +45,7 @@ with open(opencode_json, encoding="utf-8") as handle: expected = [ f"{kimaki_data_dir}/kimaki-config/plugins/dm-context-filter.ts", f"{kimaki_data_dir}/kimaki-config/plugins/dm-agent-sync.ts", + f"{kimaki_data_dir}/kimaki-config/plugins/homeboy-notification-context.ts", f"{opencode_json.rsplit('/', 1)[0]}/.opencode/plugins/claude-code-auth.ts", ] actual = data.get("plugin") @@ -75,6 +76,7 @@ with open(opencode_json, encoding="utf-8") as handle: expected = [ f"{kimaki_data_dir}/kimaki-config/plugins/dm-context-filter.ts", f"{kimaki_data_dir}/kimaki-config/plugins/dm-agent-sync.ts", + f"{kimaki_data_dir}/kimaki-config/plugins/homeboy-notification-context.ts", ] actual = data.get("plugin") if actual != expected: diff --git a/tests/post-upgrade-restore.sh b/tests/post-upgrade-restore.sh index c829011..321996b 100755 --- a/tests/post-upgrade-restore.sh +++ b/tests/post-upgrade-restore.sh @@ -89,6 +89,10 @@ cat > "$SRC_PLUGINS/dm-agent-sync.ts" <<'EOF' // dm-agent-sync.ts export default async () => ({}) EOF +cat > "$SRC_PLUGINS/homeboy-notification-context.ts" <<'EOF' +// homeboy-notification-context.ts +export default async () => ({}) +EOF TEST_SCRIPT_DIR="$TMP/kimaki-config-dir" mkdir -p "$TEST_SCRIPT_DIR" @@ -161,8 +165,10 @@ KIMAKI_PLUGIN_SOURCE_DIR="$SRC_PLUGINS" \ assert_present "$LIVE_PLUGINS/dm-context-filter.ts" assert_present "$LIVE_PLUGINS/dm-agent-sync.ts" +assert_present "$LIVE_PLUGINS/homeboy-notification-context.ts" assert_log_contains_file "$TMP/run-override.log" "restored plugin dm-context-filter.ts" assert_log_contains_file "$TMP/run-override.log" "restored plugin dm-agent-sync.ts" +assert_log_contains_file "$TMP/run-override.log" "restored plugin homeboy-notification-context.ts" # Idempotency: second run with the same state should restore zero plugins. KIMAKI_SKILLS_DIR="$LIVE_SKILLS" \ @@ -198,8 +204,8 @@ if [[ ! -f "$LIVE_PLUGINS/dm-context-filter.ts" ]]; then cat "$TMP/run3.log" exit 1 fi -if ! grep -q "2 plugins restored" "$TMP/run3.log"; then - echo "FAIL: rehydration run should report 2 plugins restored" +if ! grep -q "3 plugins restored" "$TMP/run3.log"; then + echo "FAIL: rehydration run should report 3 plugins restored" cat "$TMP/run3.log" exit 1 fi @@ -268,8 +274,8 @@ KIMAKI_SKILL_ENABLES_FILE="$SKILL_ENABLES" \ KIMAKI_PLUGIN_SOURCE_DIR="$MISSING_SRC" \ "$TEST_SCRIPT_DIR/post-upgrade.sh" > "$TMP/missing.log" 2>&1 -assert_log_contains_file "$TMP/missing.log" "WARNING: persistent plugin source dir not found at $MISSING_SRC; dm-context-filter.ts and dm-agent-sync.ts cannot be loaded" +assert_log_contains_file "$TMP/missing.log" "WARNING: persistent plugin source dir not found at $MISSING_SRC; managed OpenCode plugins cannot be loaded" assert_log_contains_file "$TMP/missing.log" "WARNING: plugins dir not found at $MISSING_LIVE_PLUGINS; opencode.json plugin paths will be skipped by OpenCode" -assert_log_contains_file "$TMP/missing.log" "2 required plugins missing" +assert_log_contains_file "$TMP/missing.log" "3 required plugins missing" echo "PASS: tests/post-upgrade-restore.sh ($(grep -c '' "$TMP/run1.log" || true) lines run1, $(grep -c '' "$TMP/run3.log" || true) lines run3)" diff --git a/tests/repair-opencode-json.sh b/tests/repair-opencode-json.sh index bfa78ed..c5c6708 100644 --- a/tests/repair-opencode-json.sh +++ b/tests/repair-opencode-json.sh @@ -112,7 +112,8 @@ cat > "$TMP/local-plugin-path.json" <<'JSON' { "plugin": [ "/Users/example/.nvm/versions/node/v24/lib/node_modules/kimaki/plugins/dm-context-filter.ts", - "/Users/example/.nvm/versions/node/v24/lib/node_modules/kimaki/plugins/dm-agent-sync.ts" + "/Users/example/.nvm/versions/node/v24/lib/node_modules/kimaki/plugins/dm-agent-sync.ts", + "/Users/example/.nvm/versions/node/v24/lib/node_modules/kimaki/plugins/homeboy-notification-context.ts" ] } JSON @@ -134,6 +135,7 @@ with open(sys.argv[1], encoding="utf-8") as handle: expected = [ "/Users/example/.kimaki/kimaki-config/plugins/dm-context-filter.ts", "/Users/example/.kimaki/kimaki-config/plugins/dm-agent-sync.ts", + "/Users/example/.kimaki/kimaki-config/plugins/homeboy-notification-context.ts", ] if data.get("plugin") != expected: raise SystemExit(f"unexpected plugin paths: {data.get('plugin')}") diff --git a/tests/runtime-signature.sh b/tests/runtime-signature.sh index 5df5b3b..1f2a9ff 100755 --- a/tests/runtime-signature.sh +++ b/tests/runtime-signature.sh @@ -158,13 +158,13 @@ runtime_signature_register "opencode" \ HASH_AFTER=$(file_hash "$MU_FILE") assert_eq "$HASH_AFTER" "$HASH_BEFORE" "file unchanged on re-register" -# --- 3. Add legacy kimaki without disturbing opencode ----------------------- +# --- 3. Add Kimaki contract without disturbing opencode --------------------- # Simulate a legacy 0600 file from a pre-#133 install. The next register call # must self-heal it back to 0644 via the mktemp+mv path. -echo "==> simulate legacy 0600 file and verify self-heal on next register" +echo "==> register Kimaki attribution contract and verify self-heal" chmod 0600 "$MU_FILE" -runtime_signature_register "kimaki" \ - '{"session_id":"KIMAKI_SESSION_ID","thread_id":"KIMAKI_THREAD_ID","thread_url":"KIMAKI_THREAD_URL"}' +source "$SCRIPT_DIR/bridges/kimaki.sh" +_kimaki_register_runtime_signature assert_php_lint "$MU_FILE" "two-runtime file parses with php -l" assert_mode_0664 "$MU_FILE" "mu-plugin mode self-healed to 0664 after sibling register" assert_group "$MU_FILE" "mu-plugin group self-healed after sibling register" @@ -186,10 +186,10 @@ else echo " FAIL opencode block did not pick up new subkey" FAILED=$((FAILED + 1)) fi -if grep -q "KIMAKI_SESSION_ID" "$MU_FILE"; then - echo " ok kimaki block preserved across opencode mutation" +if grep -q "KIMAKI_SESSION_ID" "$MU_FILE" && grep -q "KIMAKI_THREAD_ID" "$MU_FILE" && grep -q "KIMAKI_CHANNEL_ID" "$MU_FILE"; then + echo " ok Kimaki attribution contract preserved across opencode mutation" else - echo " FAIL kimaki block clobbered by opencode re-register" + echo " FAIL Kimaki attribution contract clobbered by opencode re-register" FAILED=$((FAILED + 1)) fi