diff --git a/SETUP.md b/SETUP.md index ede7aa5..041e26c 100644 --- a/SETUP.md +++ b/SETUP.md @@ -11,7 +11,7 @@ Codex has two supported modes. This choice applies only to codex; Claude Code re ## 1. Kernel server -Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `/bin/clikernel-mcp`. Kernel-centric codex: a `[mcp_servers.clikernel]` block in `~/.codex/config.toml` with `command` set to that binary, `startup_timeout_sec = 30`, `tool_timeout_sec = 3600`, and `approval_mode = "approve"` for its `execute`, `connect`, `restart`, and `interrupt` tools. +Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `/scripts/clikernel-mcp-shim`, which execs `/bin/clikernel-mcp`, adding `--quiet` when `CLAUDE_CODE_ENTRYPOINT` is `claude-desktop`: desktop kernels skip the startup notice, since desktop sessions take their instructions from the hooks (step 3). Kernel-centric codex: a `[mcp_servers.clikernel]` block in `~/.codex/config.toml` with `command` set to that binary, `startup_timeout_sec = 30`, `tool_timeout_sec = 3600`, and `approval_mode = "approve"` for its `execute`, `connect`, `restart`, and `interrupt` tools. Hybrid codex: use the following exact working configuration, changing the `command` path if the workspace is elsewhere: @@ -59,6 +59,8 @@ Settle first: existing non-symlink files at those paths. Outcome, Claude Code, in `~/.claude/settings.json` under `hooks`: PreToolUse matcher `Write|Edit|NotebookEdit` runs `aai-hook claude-block-native-edit`; PreToolUse matcher `Bash` runs `aai-hook claude-bash-guard`; UserPromptSubmit runs `aai-hook claude-prompt-submit`; SessionStart runs `aai-hook claude-session-start`; UserPromptSubmit, MessageDisplay, and PostToolBatch each also run `aai-hook claude-air` (the come-up-for-air nudge: after 8 tool-call rounds with no text response of 100+ chars, it injects a reminder to surface and reassess, repeating every 5 further rounds). The air nudge is Claude-only: codex has no message-level hook event, so it cannot observe the "text happened" reset condition - the codex-shaped substitute is a sentence in AGENTS.md; revisit if codex grows one. PostToolBatch and Stop also each run `aai-hook claude-drop-sentinel`, a Python port of podlayer/message-drop-sentinel (MIT): it detects the thinking-sandwich message-drop platform bug from the transcript scar (two adjacent thinking blocks) and tells the agent its text was probably eaten: restate it in the turn-final message, or say it now and end the turn if the user needs it immediately. Retire the sentinel entries when the upstream bug is fixed (re-test recipe and issue links in that repo's README). UserPromptSubmit and MessageDisplay also each run `aai-hook claude-slop`: MessageDisplay buffers each displayed assistant message, and at the next prompt the hook scores the previous turn's final message with the `slopometer` CLI, injecting the flagged patterns as context. A prompt that is a bare `;` means the user did not understand the previous reply, and the hook injects an instruction to restate it in plain English. Bare `aai-hook` resolves because the user's shell profile puts the workspace venv on PATH; if it does not, use the absolute venv path. +Desktop app: it has no launch flags, so no sysp replacement and no `claudedojo` launch. Hooks detect it (`CLAUDE_CODE_ENTRYPOINT` = `claude-desktop`): SessionStart prints `prompts/core.md` instead of the bootstrap gate, and native Write and Edit stay usable. NotebookEdit stays blocked everywhere: its writer saves non-ASCII as JSON escapes, churning whole notebooks. The bash guard runs in both frontends. Revisit if the desktop gains launch options. + Outcome, kernel-centric codex, in `~/.codex/hooks.json`: PostCompact, SessionStart with matcher `compact`, and PreToolUse with matcher `mcp__clikernel__execute` each run `/bin/aai-hook codex-orientation`; UserPromptSubmit runs `/bin/aai-hook codex-prompt-submit`. Hybrid codex does not install `codex-orientation`, since it does not run the dojo; it may still install `codex-prompt-submit`. codex asks the user to trust hooks on the first start after any `hooks.json` change; tell them to expect that prompt. Check: `aai-hook claude-prompt-submit` fed `{"prompt": "test?"}` on stdin prints the question notice. @@ -71,7 +73,7 @@ Outcome, in `settings.json`: `permissions.deny` includes `Read`, `Edit`, `Write` Recommended, ask the user: `disableBundledSkills` set to `true` in `settings.json`, turning off the built-in skills (`init`, `review`, `code-review`, `security-review`, `simplify`, `verify`, `run`, `dataviz`, `artifact-design`, `fewer-permission-prompts`, `update-config`, `keybindings-help`), which assume the native file tools this deny list removes. -Settle first: any existing rule that conflicts. In particular a broad `Bash` allow rule defeats both the bash guard and safecmd; surface that one explicitly. +Settle first: any existing rule that conflicts. In particular a broad `Bash` allow rule defeats both the bash guard and safecmd; surface that one explicitly. Also whether the user works in the desktop app: settings cannot branch by frontend, and this deny list would strip desktop sessions too. Such users carry these rules in `~/.config/claudedojo/config.toml` instead (step 5). Check: the file still parses as JSON after editing. @@ -81,7 +83,16 @@ Outcome: symlinks from `~/.claude/skills/persistent-python` and `~/.claude/skill safecmd auto-approves allowlisted Bash commands. The `safecmd` package is a workspace member, so it is already installed; its allowlist lives at `~/.config/safecmd/config.ini` and the defaults are fine to start. -Optional, Claude Code: the user might like `/prompts/core.md` appended to the system prompt; a shell alias adding `--append-system-prompt-file /prompts/core.md` to `claude` does it. The stronger option is the team's full behavioral prompt: symlink `~/.claude/sysp` to `/prompts/sysp.md` and alias `claude` to `claude --system-prompt-file ~/.claude/sysp --append-system-prompt-file /prompts/core.md`, which replaces Claude Code's default prompt entirely. Explain the trade to the user before wiring it: the default's tool schemas survive replacement, but its dynamic environment block and scratchpad path do not, and the behavioral text takes over from the default's guidance. +Optional, Claude Code: `claudedojo` launches `claude` on a session opening with the worked dojo round, adding the `claude_args` list from `~/.config/claudedojo/config.toml` (each `~`-expanded). That file carries the whole CLI launch: no shell alias, no `--settings` file: + + claude_args = [ + "--system-prompt-file", "~/.claude/sysp", + "--append-system-prompt-file", "/prompts/core.md", + "--allowedTools", "WebSearch", "WebFetch", "mcp__clikernel__restart", + "--disallowedTools", "Read", "Edit", "Write", "Grep", "Glob", "NotebookEdit", "Bash(cat *)", "Bash(python -c:*)", + ] + +`--system-prompt-file` replaces Claude Code's default prompt with sysp.md (symlink `~/.claude/sysp` to `/prompts/sysp.md`). Explain the trade before wiring it: the default's tool schemas survive replacement, but its dynamic environment block and scratchpad path do not. Optional, codex: the analogue of the full behavioral prompt is `model_instructions_file = "/prompts/codex-sysp.md"` (absolute path) in `~/.codex/config.toml`, replacing codex's built-in instructions entirely; `~/.codex/AGENTS.md` (and so `core.md`) still loads on top, and no symlink is involved since the key points straight into the checkout. Explain the trade to the user before wiring it: the file is the team's edited reconstruction of the built-in instructions, so upstream changes to codex's own prompt stop arriving until the file is revised. diff --git a/aai_coding/harness.py b/aai_coding/harness.py index c27260e..ef9d76c 100644 --- a/aai_coding/harness.py +++ b/aai_coding/harness.py @@ -80,8 +80,16 @@ def _forget(session_id, boundary=None): except Exception: pass +def _desktop(): + "True in a Claude desktop app session, which runs the relaxed harness: the desktop can neither replace the system prompt nor start dojo-preloaded" + return os.environ.get('CLAUDE_CODE_ENTRYPOINT') == 'claude-desktop' + + +CORE_MD = Path(__file__).parent.parent/'prompts'/'core.md' + + def claude_session_start(o): - "SessionStart: orientation notice by source, then Python-project bootstrap and nbdev addenda" + "SessionStart: orientation notice by source, then Python-project bootstrap and nbdev addenda; the desktop app gets core.md instead of the bootstrap gate" d = Path(os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd()) src = o.get('source', '') if src in ('resume', 'compact'): print(f'[{src} at {datetime.now():%H:%M:%S}]') @@ -92,7 +100,8 @@ def claude_session_start(o): _forget(o.get('session_id', ''), bt) print(SYNTH_MSG) elif src == 'resume' and (d/'pyproject.toml').is_file(): print(RESUME_MSG) - if (d/'pyproject.toml').is_file(): print(BOOTSTRAP_MSG) + if _desktop(): print(CORE_MD.read_text()) + elif (d/'pyproject.toml').is_file(): print(BOOTSTRAP_MSG) try: nb = any(l.startswith('[tool.nbdev]') for l in (d/'pyproject.toml').open()) except OSError: nb = False if nb: print(NBDEV_MSG) @@ -122,7 +131,8 @@ def claude_bash_guard(o): def claude_block_native_edit(o): - "PreToolUse(Write|Edit|NotebookEdit): route edits to the kernel tooling" + "PreToolUse(Write|Edit|NotebookEdit): route edits to the kernel tooling. Desktop sessions keep Write and Edit but never NotebookEdit: its writer saves non-ASCII as JSON escapes, churning every notebook it touches" + if _desktop() and o.get('tool_name') != 'NotebookEdit': return print(BLOCK_EDIT_MSG, file=sys.stderr) sys.exit(2) @@ -285,6 +295,8 @@ def claude_slop(o): if notes: print(json.dumps(dict(hookSpecificOutput=dict( hookEventName='UserPromptSubmit', additionalContext='\n'.join(notes))))) except Exception as e: print(f'[slop] fail-open: {e!r}', file=sys.stderr) + + def codex_orientation(o): "codex PostCompact/SessionStart/PreToolUse: post-compaction doc-state reset and one-shot reorientation" state = Path(os.environ.get('LLMDOJO_STATE_DIR', Path.home()/'.local/state/llmdojo')) diff --git a/plugins/safecmd/scripts/validator.py b/plugins/safecmd/scripts/validator.py index 2c6e4a4..b0363bf 100755 --- a/plugins/safecmd/scripts/validator.py +++ b/plugins/safecmd/scripts/validator.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Claude Code PreToolUse hook that auto-approves safe bash commands using safecmd.""" -import sys,json +import sys,json,os def respond(decision, reason): print(json.dumps({"hookSpecificOutput": { @@ -11,7 +11,7 @@ def respond(decision, reason): sys.exit(0) try: from safecmd import validate, DisallowedError -except ImportError as e: respond("defer", f"safecmd import failed: {e}") +except ImportError: sys.exit(0) # no opinion: exit 0 with no output falls through to the normal permission flow def main(): try: hook_input = json.load(sys.stdin) @@ -24,6 +24,6 @@ def main(): validate(cmd) respond("allow", "safecmd: validated") except DisallowedError: - respond("defer", "Not on safecmd allowlist") + sys.exit(0) # off-allowlist: no opinion, normal permission flow ("defer" means resume-later and ends the run in the desktop app) if __name__ == "__main__": main() diff --git a/scripts/clikernel-mcp-shim b/scripts/clikernel-mcp-shim new file mode 100755 index 0000000..49c32a4 --- /dev/null +++ b/scripts/clikernel-mcp-shim @@ -0,0 +1,6 @@ +#!/bin/sh +# Runs clikernel-mcp, adding --quiet in the desktop app: desktop sessions take +# their instructions from the hooks, not from the kernel's startup notice. +mcp="$(CDPATH= cd "$(dirname "$0")/../.." && pwd)/.venv/bin/clikernel-mcp" +[ "$CLAUDE_CODE_ENTRYPOINT" = claude-desktop ] && exec "$mcp" --quiet "$@" +exec "$mcp" "$@" diff --git a/tests/test_harness.py b/tests/test_harness.py index 0bb7434..63c0163 100644 --- a/tests/test_harness.py +++ b/tests/test_harness.py @@ -150,6 +150,24 @@ def out(): return capsys.readouterr().out assert out() == '' # unparseable transcript: fail-open, silent on stdout +def test_desktop_relaxed(tmp_path, monkeypatch, capsys): + "Desktop sessions keep native Write/Edit (never NotebookEdit) and get core.md instead of the bootstrap gate; terminal sessions enforce both" + from aai_coding.harness import claude_block_native_edit, claude_session_start + monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path)) + (tmp_path/'pyproject.toml').write_text('') + monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop') + claude_block_native_edit(dict(tool_name='Edit')) + with pytest.raises(SystemExit): claude_block_native_edit(dict(tool_name='NotebookEdit')) # its writer's escape churn corrupts notebooks + claude_session_start(dict(source='startup', session_id='s1')) + out = capsys.readouterr().out + assert 'final text message' in out and 'NEVER touch local files' not in out + monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli') + with pytest.raises(SystemExit): claude_block_native_edit(dict(tool_name='Edit')) + claude_session_start(dict(source='startup', session_id='s1')) + out = capsys.readouterr().out + assert 'NEVER touch local files' in out and 'final text message' not in out + + @pytest.mark.skipif(not which('slopometer'), reason='slopometer not installed') def test_slop(tmp_path, monkeypatch, capsys): "Sloppy previous message -> context rows at the next prompt; repeats, subagents, short and clean prose stay silent" @@ -200,6 +218,7 @@ def out(): return capsys.readouterr().out def test_synthetic_wipe_guard(tmp_path, monkeypatch): import llmdojo.rules as lr + monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli') # the wipe guard lives on the terminal path; don't inherit a desktop env monkeypatch.setenv('LLMDOJO_STATE_DIR', str(tmp_path/'state')) monkeypatch.setenv('CLAUDE_CODE_SESSION_ID', 'tsid') monkeypatch.delenv('CODEX_THREAD_ID', raising=False)