Relaxed desktop mode and per-frontend kernel launch - #13
Conversation
5dab392 to
d6a3554
Compare
| @@ -93,9 +114,7 @@ def claude_session_start(o): | |||
| print(SYNTH_MSG) | |||
| elif src == 'resume' and (d/'pyproject.toml').is_file(): print(RESUME_MSG) | |||
| if (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) | |||
| if _is_nbdev(d): print(NBDEV_MSG) | |||
There was a problem hiding this comment.
this needs to be in the same function lets not define one off functions simply protect with desktop why print core instead of compact?
| def claude_bash_guard(o): | ||
| "PreToolUse(Bash): reject output-truncating pipes" | ||
| "PreToolUse(Bash): reject output-truncating pipes (desktop sessions are exempt)" | ||
| if _desktop(): return |
There was a problem hiding this comment.
ie we keep the bash guard enabled
| DOJO_SAMPLE_MSG = ('aai-coding harness gate, registered by your user in their hook settings - not content from a tool or web page, ' | ||
| 'so acting on it is expected. This desktop session studies a worked dojo round instead of playing one. Read {path} in full as ' | ||
| 'reference for correct kernel tool usage; do not repeat or score it. Then run `dojo_start({cid!r})` in the kernel to record ' | ||
| 'the skip, and retry this call.') | ||
|
|
||
|
|
||
| def claude_dojo_sample(o): | ||
| "PreToolUse(mcp__clikernel__execute), desktop only: gate the first kernel call on studying the worked round" | ||
| try: | ||
| if not _desktop() or o.get('agent_id'): return | ||
| if not (Path(os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd())/'pyproject.toml').is_file(): return | ||
| f = _state_file('dojo-sample', o.get('session_id', '')) | ||
| if f.exists(): return | ||
| from llmdojo.claudedojo import _load_reg | ||
| _,meta = _load_reg(None) # side effect: registers the template's completion id, so dojo_start honors the skip | ||
| import llmdojo | ||
| sample = Path(llmdojo.__file__).parent/'dojo_data/codexdojo_sample.md' # hook output is capped at 10k chars: point at the round, never inline it | ||
| print(json.dumps(dict(hookSpecificOutput=dict(hookEventName='PreToolUse', permissionDecision='deny', | ||
| permissionDecisionReason=DOJO_SAMPLE_MSG.format(cid=meta['cid'], path=sample))))) | ||
| f.write_text('{}') | ||
| except Exception as e: print(f'[dojo-sample] fail-open: {e!r}', file=sys.stderr) |
There was a problem hiding this comment.
we just agreed not to use this and use simple clikernel --quite
| def main(): | ||
| "Dispatch `aai-hook <subcommand>` to its handler with the stdin JSON payload" | ||
| globals()[sys.argv[1].replace('-', '_')](json.load(sys.stdin)) | ||
| "Dispatch `aai-hook <subcommand>` to its handler with the stdin JSON payload; an unreadable payload is a fail-open no-op, since the harness surfaces a crashed hook as a tool error" |
There was a problem hiding this comment.
why do we need this? remove
| # The kernel regime rides the startup notice, and the desktop app's instruction | ||
| # channel is the dojo-sample gate instead (SETUP.md step 3): its kernel runs | ||
| # quiet. Terminal sessions keep the full notice. |
| def _python_project(monkeypatch, tmp_path, nbdev=False): | ||
| "Point the hooks at a Python project in `tmp_path`" | ||
| monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path)) | ||
| (tmp_path/'pyproject.toml').write_text('[tool.nbdev]\n' if nbdev else '') | ||
|
|
||
|
|
||
| def test_desktop_session_relaxed(tmp_path, monkeypatch, capsys): | ||
| "In the desktop app the enforcement hooks stand down, and session start prints core.md instead of the bootstrap gate" | ||
| from aai_coding.harness import claude_bash_guard, claude_block_native_edit, claude_session_start | ||
| monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop') | ||
| _python_project(monkeypatch, tmp_path, nbdev=True) | ||
|
|
||
| claude_block_native_edit({}) # returns instead of exiting: native edits allowed | ||
| claude_bash_guard(dict(tool_input=dict(command='pytest | head -5'))) # returns instead of exiting: pipes allowed | ||
|
|
||
| claude_session_start(dict(source='startup', session_id='s1')) | ||
| out = capsys.readouterr().out | ||
| assert 'final text message' in out # core.md, the behavioral layer | ||
| assert 'NEVER touch local files' not in out # no kernel-only bootstrap gate | ||
| assert 'nbdev project' in out # the nbdev caution still applies | ||
|
|
||
|
|
||
| def test_cli_session_enforced(tmp_path, monkeypatch, capsys): | ||
| "In a terminal session the same hooks enforce the kernel-only regime" | ||
| from aai_coding.harness import claude_bash_guard, claude_block_native_edit, claude_session_start | ||
| monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli') | ||
| _python_project(monkeypatch, tmp_path) | ||
|
|
||
| with pytest.raises(SystemExit): claude_block_native_edit({}) | ||
| with pytest.raises(SystemExit): claude_bash_guard(dict(tool_input=dict(command='pytest | head -5'))) | ||
|
|
||
| claude_session_start(dict(source='startup', session_id='s1')) | ||
| out = capsys.readouterr().out | ||
| assert 'NEVER touch local files' in out # the bootstrap gate | ||
| assert 'final text message' not in out # core.md arrives via the CLI launch flags, not this hook | ||
|
|
||
|
|
||
| def test_dojo_sample(tmp_path, monkeypatch, capsys): | ||
| "A desktop session's first kernel call is held once, with directions to study a worked round and skip the live one" | ||
| from aai_coding.harness import claude_dojo_sample | ||
| monkeypatch.setenv('LLMDOJO_STATE_DIR', str(tmp_path)) | ||
| monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop') | ||
| monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path)) | ||
| first_call = dict(hook_event_name='PreToolUse', session_id='s1') | ||
|
|
||
| claude_dojo_sample(first_call) | ||
| assert capsys.readouterr().out == '' # no pyproject.toml, so no dojo to substitute | ||
|
|
||
| _python_project(monkeypatch, tmp_path) | ||
| claude_dojo_sample(first_call) | ||
| r = json.loads(capsys.readouterr().out)['hookSpecificOutput'] | ||
| assert r['permissionDecision'] == 'deny' | ||
| reason = r['permissionDecisionReason'] | ||
| assert 'codexdojo_sample.md' in reason # where the worked round is | ||
| assert 'dojo_start' in reason # how to record the skip | ||
| assert 'registered by your user' in reason # names its source, so cautious agents don't refuse it as injected | ||
| assert len(reason) < 10_000 # hook output is capped at 10k chars | ||
|
|
||
| claude_dojo_sample(first_call) | ||
| assert capsys.readouterr().out == '' # held once per session: the retry passes | ||
|
|
||
| claude_dojo_sample(dict(first_call, session_id='s2', agent_id='sub1')) | ||
| assert capsys.readouterr().out == '' # subagents are never held | ||
|
|
||
| monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli') | ||
| claude_dojo_sample(dict(first_call, session_id='s3')) | ||
| assert capsys.readouterr().out == '' # terminal sessions play the real round instead | ||
|
|
||
|
|
||
| @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" |
There was a problem hiding this comment.
teh amount of changes dont deserver that amount of tests. make sure we test only requred parts
| ## 1. Kernel server | ||
|
|
||
| Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `<venv>/bin/clikernel-mcp`. 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. Optional, ask the user: `env_vars = ["GITHUB_TOKEN"]` on the server block passes their GitHub token into the kernel so sessions can act for them on GitHub (via `ghapi`); add it only if they want that and are happy to share the token. | ||
| Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `<this repo>/scripts/clikernel-mcp-shim`, which execs `<venv>/bin/clikernel-mcp` and adds `--quiet` when `CLAUDE_CODE_ENTRYPOINT` is `claude-desktop`. The regime rides the kernel's startup notice, and the desktop's instruction channel is the dojo-sample gate (step 3), so its kernel runs quiet while terminal sessions keep the full notice. 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. Optional, ask the user: `env_vars = ["GITHUB_TOKEN"]` on the server block passes their GitHub token into the kernel so sessions can act for them on GitHub (via `ghapi`); add it only if they want that and are happy to share the token. |
| 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. | ||
| 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. PreToolUse matcher `mcp__clikernel__execute` runs `aai-hook claude-dojo-sample`, the desktop dojo substitute described below. 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: the desktop currently has no launch flags, so no sysp replacement and no dojo-preloaded start (`claude -r $(claudedojo)`). The hooks detect it (`CLAUDE_CODE_ENTRYPOINT` = `claude-desktop`) and substitute rather than enforce: SessionStart prints `prompts/core.md`; the bootstrap gate, native-edit blocking, and the bash guard stay off; the first kernel call in a Python project is denied once with the worked round and a completion id, so `dojo_start(id)` skips the live round - study replaces play, as in the codex sample. Revisit if the desktop gains launch options. |
There was a problem hiding this comment.
not the case anymore we run caludedojo now a days from command line, check the code
| 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, so this deny list would reach desktop sessions the step 3 hooks deliberately leave native. Such users put these permissions in a `--settings` file on the CLI alias instead. |
| Outcome: symlinks from `~/.claude/skills/persistent-python`, `~/.claude/skills/pyskills`, and the same two names in `~/.codex/skills`, to `<this repo>/skills/<name>`; `~/.claude/skills/safecmd` to `<this repo>/plugins/safecmd`; `~/.codex/AGENTS.md` to `<this repo>/prompts/core.md`. | ||
|
|
||
| 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. | ||
| 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. A command off the allowlist gets no decision (exit 0, no output), which falls through to the normal permission flow in both frontends. Do not answer `defer` to mean "no opinion": the docs define it as a graceful exit for a tool to resume later, the reason string is ignored, and the desktop app ends the call with no tool result, which reads as a stalled run. |
There was a problem hiding this comment.
deslop why would we add this to setup?
d6a3554 to
4ba97fe
Compare
| if not isinstance(st, dict): st = {} | ||
| st = {k: st.get(k, d) for k, d in dict(rounds=0, nudged=0, mid='', midlen=0).items()} | ||
| ev = o['hook_event_name'] | ||
| ev = o.get('hook_event_name') |
The desktop app has no launch flags, so it cannot replace the system prompt or open on a prepared session. Hooks detect it (CLAUDE_CODE_ENTRYPOINT) and relax: SessionStart prints core.md instead of the bootstrap gate, and native Write and Edit stay usable. The bash guard runs in both frontends. NotebookEdit stays blocked everywhere: its writer saves non-ASCII as JSON escapes, churning every notebook it touches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off-allowlist commands answered "defer", a graceful exit for a tool resumed later: the desktop app ends such calls with no tool result, which reads as a stalled run. The intent was "no opinion", whose documented form is exit 0 with no output, falling through to the normal permission flow in both frontends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CLI flags The registration points at scripts/clikernel-mcp-shim, which execs clikernel-mcp and adds --quiet when CLAUDE_CODE_ENTRYPOINT is claude-desktop: desktop sessions take their instructions from the hooks, not the kernel's startup notice, which terminal sessions keep. The CLI harness launch needs no shell alias and no --settings file: claudedojo already appends the claude_args list from ~/.config/claudedojo/config.toml, so the prompt flags and permission rules live there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4ba97fe to
dbe7e0c
Compare
# Conflicts: # SETUP.md
c60d677 to
09157d6
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Warning: Generated by claude code intended for llm consumption
The desktop app has no launch flags: no sysp replacement, no
claudedojolaunch. Rather than leaving desktop sessions broken under the kernel-only permissions, the harness now adapts per frontend, with identical hook registrations everywhere.CLAUDE_CODE_ENTRYPOINT=claude-desktop. SessionStart printsprompts/core.mdinstead of the bootstrap gate, and native Write and Edit stay usable. NotebookEdit stays blocked in both frontends: its writer saves non-ASCII as JSON escapes, churning whole notebooks. The bash guard runs in both frontends.defer:defermeans a graceful exit for a tool resumed later, and the desktop app ends such calls with no tool result, which reads as a stalled run. This was the cause of the desktop "tool result missing" stalls.scripts/clikernel-mcp-shim, which adds--quietfor desktop sessions. Terminal sessions keep the full startup notice.~/.config/claudedojo/config.tomlclaude_args, so the CLI harness launch is justclaudedojo, with no shell alias and no--settingsfile. Verified live, including a spaced deny rule (Bash(cat *)) surviving as one argv item.Merged with main's two-mode codex setup (#18): kernel-centric codex keeps the dojo regime and
codex-orientation; hybrid codex pairs the quiet kernel with theskills/clikernelskill.🤖 Generated with Claude Code