smlflg commited on
Commit
462dc2b
·
verified ·
1 Parent(s): a20bdb2

Initial public upload from Projekte/Sidecar-Workspace (part 17)

Browse files
codex-sidecarV1/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ .pytest_cache/
3
+ .coverage
codex-sidecarV1/.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
codex-sidecarV1/.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
codex-sidecarV1/.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
codex-sidecarV1/.pytest_cache/v/cache/lastfailed ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "tests/test_governor.py::GovernorTests": true,
3
+ "tests/test_state.py::StateTests": true,
4
+ "tests/test_watcher.py::WatcherTests": true
5
+ }
codex-sidecarV1/.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "tests/test_cli.py::CliTests::test_exec_dry_run_allows_safe_prompt",
3
+ "tests/test_cli.py::CliTests::test_exec_dry_run_refuses_risky_prompt",
4
+ "tests/test_cli.py::CliTests::test_replay_json_returns_subagent_summary",
5
+ "tests/test_cli.py::CliTests::test_status_json_reports_capabilities_truth",
6
+ "tests/test_governor.py::GovernorTests::test_build_gate_without_repo_falls_back_to_plan",
7
+ "tests/test_governor.py::GovernorTests::test_build_ready_requires_repo_and_check",
8
+ "tests/test_governor.py::GovernorTests::test_contract_has_stable_labels",
9
+ "tests/test_governor.py::GovernorTests::test_empty_prompt_fail_open",
10
+ "tests/test_governor.py::GovernorTests::test_wrapper_refuses_risky_prompt",
11
+ "tests/test_state.py::StateTests::test_build_without_check_produces_plan_contract_and_advisory",
12
+ "tests/test_state.py::StateTests::test_replay_builds_subagent_aware_snapshot",
13
+ "tests/test_state.py::StateTests::test_risky_shell_is_advisory_not_fake_block",
14
+ "tests/test_watcher.py::WatcherTests::test_normalize_realistic_subagent_session"
15
+ ]
codex-sidecarV1/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # codex-sidecarV1
2
+
3
+ Standalone Codex sidecar V1 workspace.
4
+
5
+ This repo now implements the practical V1 shape for the real Codex runtime:
6
+
7
+ - replay-first watcher over `~/.codex/sessions/*.jsonl`
8
+ - explicit mode contracts for prompts
9
+ - advisory findings from observed sessions
10
+ - no hidden context injection and no fake tool blocking
11
+ - optional wrapper boundary through `codex-sidecar exec`
12
+
13
+ ## Usage
14
+
15
+ Run it directly without installation:
16
+
17
+ ```bash
18
+ PYTHONPATH=src python3 -m codex_sidecar contract "build only parser module and run tests"
19
+ PYTHONPATH=src python3 -m codex_sidecar status --json
20
+ PYTHONPATH=src python3 -m codex_sidecar replay ~/.codex/sessions/.../rollout-....jsonl --json
21
+ PYTHONPATH=src python3 -m codex_sidecar exec --prompt "review current diff" --dry-run -- codex
22
+ ```
23
+
24
+ The public Codex V1 capability contract is:
25
+
26
+ - `supports_context_injection = false`
27
+ - `supports_tool_blocking = false`
28
+ - `supports_session_replay = true`
29
+ - `supports_post_tool_observation = true`
30
+ - advisory outputs only unless launched through the wrapper boundary
31
+
32
+ Split baseline:
33
+
34
+ - Source repo: `../Sidecar-V8`
35
+ - Baseline commit: `1950d86`
36
+ - Split date: `2026-04-24`
37
+ - Intent: leave `Sidecar-V8` in place and continue new work here
38
+
39
+ `Sidecar-V8` worktree was intentionally left dirty at split time. Status snapshot:
40
+
41
+ - `M adapters/claude_code/adapter.py`
42
+ - `M adapters/codex/adapter.py`
43
+ - `M core/sidecar_core/__init__.py`
44
+ - `M core/sidecar_core/adapter.py`
45
+ - `M core/sidecar_core/core.py`
46
+ - `M core/tests/test_adapter.py`
47
+ - `M core/tests/test_sidecar_core.py`
48
+ - `?? core/sidecar_core/capabilities.py`
49
+ - `?? core/sidecar_core/evaluation_bridge.py`
50
+ - `?? core/sidecar_core/proof_registry.py`
51
+ - `?? core/sidecar_core/runtime.py`
52
+ - `?? core/tests/test_proof_registry.py`
53
+ - `?? core/tests/test_runtime.py`
codex-sidecarV1/pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "codex-sidecar"
7
+ version = "0.1.0"
8
+ description = "Standalone Codex sidecar V1 for replay, contracts, and advisory governance."
9
+ requires-python = ">=3.11"
10
+ readme = "README.md"
11
+ authors = [{ name = "Codex" }]
12
+ dependencies = []
13
+
14
+ [project.scripts]
15
+ codex-sidecar = "codex_sidecar.cli:main"
16
+
17
+ [tool.setuptools]
18
+ package-dir = { "" = "src" }
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
codex-sidecarV1/src/codex_sidecar/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone Codex sidecar V1."""
2
+
3
+ from .capabilities import CapabilityProfile, CODEX_V1_CAPABILITIES
4
+ from .governor import ModeContract, WrapperDecision, advise_on_snapshot, contract_for_prompt
5
+ from .state import SessionSnapshot, build_snapshot
6
+ from .watcher import NormalizedEvent, find_latest_session, normalize_session_file, resolve_session_path
7
+
8
+ __all__ = [
9
+ "CODEX_V1_CAPABILITIES",
10
+ "CapabilityProfile",
11
+ "ModeContract",
12
+ "NormalizedEvent",
13
+ "SessionSnapshot",
14
+ "WrapperDecision",
15
+ "advise_on_snapshot",
16
+ "build_snapshot",
17
+ "contract_for_prompt",
18
+ "find_latest_session",
19
+ "normalize_session_file",
20
+ "resolve_session_path",
21
+ ]
codex-sidecarV1/src/codex_sidecar/__main__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Module entrypoint for `python -m codex_sidecar`."""
2
+
3
+ from .cli import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ raise SystemExit(main())
codex-sidecarV1/src/codex_sidecar/capabilities.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Capability profile for the current Codex runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class CapabilityProfile:
10
+ """Public capability contract for Codex sidecar V1."""
11
+
12
+ adapter_name: str
13
+ event_source: str
14
+ supports_context_injection: bool
15
+ supports_tool_blocking: bool
16
+ supports_session_replay: bool
17
+ supports_post_tool_observation: bool
18
+ advisory_only: bool
19
+ wrapper_boundary_available: bool
20
+
21
+ def to_dict(self) -> dict[str, bool | str]:
22
+ return {
23
+ "adapter_name": self.adapter_name,
24
+ "event_source": self.event_source,
25
+ "supports_context_injection": self.supports_context_injection,
26
+ "supports_tool_blocking": self.supports_tool_blocking,
27
+ "supports_session_replay": self.supports_session_replay,
28
+ "supports_post_tool_observation": self.supports_post_tool_observation,
29
+ "advisory_only": self.advisory_only,
30
+ "wrapper_boundary_available": self.wrapper_boundary_available,
31
+ }
32
+
33
+
34
+ CODEX_V1_CAPABILITIES = CapabilityProfile(
35
+ adapter_name="codex",
36
+ event_source="pull",
37
+ supports_context_injection=False,
38
+ supports_tool_blocking=False,
39
+ supports_session_replay=True,
40
+ supports_post_tool_observation=True,
41
+ advisory_only=True,
42
+ wrapper_boundary_available=True,
43
+ )
codex-sidecarV1/src/codex_sidecar/cli.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI surfaces for Codex sidecar V1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import subprocess
9
+ import sys
10
+
11
+ from .capabilities import CODEX_V1_CAPABILITIES
12
+ from .governor import render_contract, wrapper_decision
13
+ from .state import build_snapshot
14
+ from .watcher import find_latest_session, resolve_session_path
15
+
16
+
17
+ def main(argv: list[str] | None = None) -> int:
18
+ """Run the CLI."""
19
+ parser = _build_parser()
20
+ args = parser.parse_args(argv)
21
+ return args.func(args)
22
+
23
+
24
+ def _build_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(prog="codex-sidecar")
26
+ subparsers = parser.add_subparsers(dest="command", required=True)
27
+
28
+ contract = subparsers.add_parser("contract", help="Render a bounded visible contract.")
29
+ contract.add_argument("prompt")
30
+ contract.add_argument("--cwd", default=".")
31
+ contract.add_argument("--json", action="store_true")
32
+ contract.set_defaults(func=_cmd_contract)
33
+
34
+ status = subparsers.add_parser("status", help="Show current or latest session status.")
35
+ status.add_argument("--session", default="")
36
+ status.add_argument("--root", default="")
37
+ status.add_argument("--json", action="store_true")
38
+ status.set_defaults(func=_cmd_status)
39
+
40
+ replay = subparsers.add_parser("replay", help="Replay a Codex rollout file.")
41
+ replay.add_argument("session")
42
+ replay.add_argument("--root", default="")
43
+ replay.add_argument("--json", action="store_true")
44
+ replay.set_defaults(func=_cmd_replay)
45
+
46
+ exec_parser = subparsers.add_parser("exec", help="Wrapper-style launch boundary.")
47
+ exec_parser.add_argument("--prompt", required=True)
48
+ exec_parser.add_argument("--cwd", default=".")
49
+ exec_parser.add_argument("--json", action="store_true")
50
+ exec_parser.add_argument("--dry-run", action="store_true")
51
+ exec_parser.add_argument("--force", action="store_true")
52
+ exec_parser.add_argument("cmd", nargs=argparse.REMAINDER)
53
+ exec_parser.set_defaults(func=_cmd_exec)
54
+
55
+ return parser
56
+
57
+
58
+ def _cmd_contract(args: argparse.Namespace) -> int:
59
+ decision = wrapper_decision(args.prompt, cwd=args.cwd)
60
+ payload = {
61
+ "contract": decision.contract.to_dict(),
62
+ "findings": [finding.to_dict() for finding in decision.findings],
63
+ "capabilities": CODEX_V1_CAPABILITIES.to_dict(),
64
+ }
65
+ if args.json:
66
+ print(json.dumps(payload, indent=2, sort_keys=True))
67
+ return 0
68
+ print(render_contract(decision.contract))
69
+ if decision.findings:
70
+ print("")
71
+ print("Findings:")
72
+ for finding in decision.findings:
73
+ print(f"- [{finding.severity}] {finding.message} -> {finding.recommendation}")
74
+ print("")
75
+ print("Capabilities:")
76
+ for key, value in CODEX_V1_CAPABILITIES.to_dict().items():
77
+ print(f"- {key}: {value}")
78
+ return 0
79
+
80
+
81
+ def _cmd_status(args: argparse.Namespace) -> int:
82
+ root = args.root or None
83
+ if args.session:
84
+ session_path = resolve_session_path(args.session, root=root)
85
+ else:
86
+ latest = find_latest_session(root=root)
87
+ if latest is None:
88
+ return _emit({"status": "empty", "reason": "no session files found"}, args.json)
89
+ session_path = latest
90
+ snapshot = build_snapshot(session_path)
91
+ payload = {"status": "ok", "session": snapshot.to_dict()}
92
+ return _emit(payload, args.json)
93
+
94
+
95
+ def _cmd_replay(args: argparse.Namespace) -> int:
96
+ session_path = resolve_session_path(args.session, root=args.root or None)
97
+ snapshot = build_snapshot(session_path)
98
+ payload = {
99
+ "status": "ok",
100
+ "mode": "replay",
101
+ "session": snapshot.to_dict(),
102
+ }
103
+ return _emit(payload, args.json)
104
+
105
+
106
+ def _cmd_exec(args: argparse.Namespace) -> int:
107
+ decision = wrapper_decision(args.prompt, cwd=args.cwd)
108
+ cmd = list(args.cmd)
109
+ if cmd and cmd[0] == "--":
110
+ cmd = cmd[1:]
111
+
112
+ payload = {
113
+ "decision": decision.to_dict(),
114
+ "command": cmd,
115
+ "cwd": str(Path(args.cwd).resolve()),
116
+ }
117
+ if args.json:
118
+ print(json.dumps(payload, indent=2, sort_keys=True))
119
+ if not decision.allowed and not args.force:
120
+ return 2
121
+ if args.dry_run or not cmd:
122
+ return 0
123
+ else:
124
+ print(render_contract(decision.contract), file=sys.stderr)
125
+ if decision.findings:
126
+ print("", file=sys.stderr)
127
+ print("Findings:", file=sys.stderr)
128
+ for finding in decision.findings:
129
+ print(
130
+ f"- [{finding.severity}] {finding.message} -> {finding.recommendation}",
131
+ file=sys.stderr,
132
+ )
133
+ print("", file=sys.stderr)
134
+
135
+ if not decision.allowed and not args.force:
136
+ if not args.json:
137
+ print(
138
+ f"Wrapper refused launch: {decision.reason}",
139
+ file=sys.stderr,
140
+ )
141
+ return 2
142
+
143
+ if args.dry_run or not cmd:
144
+ return 0
145
+
146
+ completed = subprocess.run(cmd, cwd=args.cwd, check=False)
147
+ return int(completed.returncode)
148
+
149
+
150
+ def _emit(payload: dict[str, object], as_json: bool) -> int:
151
+ if as_json:
152
+ print(json.dumps(payload, indent=2, sort_keys=True))
153
+ return 0
154
+
155
+ session = payload.get("session")
156
+ if not isinstance(session, dict):
157
+ print(payload.get("reason", "no session data"))
158
+ return 0
159
+
160
+ contract = session.get("contract")
161
+ print(f"Session: {session['session_id']}")
162
+ print(f"Path: {session['session_path']}")
163
+ print(f"CWD: {session['cwd']}")
164
+ print(f"Workflow Phase: {session['workflow_phase']}")
165
+ print(f"Prompt: {session['prompt'] or '(none captured)'}")
166
+ if isinstance(contract, dict):
167
+ print(f"Mode: {contract['mode']} ({contract['reason']})")
168
+ print(f"Verification Seen: {session['verification_seen']}")
169
+ print(f"Errors: {session['errors']}")
170
+
171
+ subagent = session.get("subagent", {})
172
+ if isinstance(subagent, dict) and subagent:
173
+ print(
174
+ "Subagent: "
175
+ f"{subagent.get('agent_nickname') or '?'} / {subagent.get('agent_role') or '?'} "
176
+ f"(depth={subagent.get('depth')}, parent={subagent.get('parent_thread_id')})"
177
+ )
178
+
179
+ findings = session.get("findings", [])
180
+ if findings:
181
+ print("Findings:")
182
+ for finding in findings:
183
+ print(f"- [{finding['severity']}] {finding['message']}")
184
+
185
+ capabilities = session.get("capabilities", {})
186
+ if isinstance(capabilities, dict):
187
+ print("Capabilities:")
188
+ for key, value in capabilities.items():
189
+ print(f"- {key}: {value}")
190
+ return 0
codex-sidecarV1/src/codex_sidecar/governor.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bounded advisory governor for Codex sidecar V1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import re
7
+
8
+ from .models import Finding, ModeContract, SessionSnapshot, WrapperDecision
9
+
10
+ MODES = ("plan", "build", "review", "debug", "research", "eval", "meta")
11
+
12
+ PLAN_TERMS = (
13
+ "plan",
14
+ "scope",
15
+ "decide",
16
+ "decide whether",
17
+ "design",
18
+ "concept",
19
+ "structure",
20
+ "think through",
21
+ )
22
+ BUILD_TERMS = (
23
+ "implement",
24
+ "build",
25
+ "fix ",
26
+ "create",
27
+ "update",
28
+ "edit",
29
+ "change",
30
+ "commit",
31
+ "push",
32
+ "deploy",
33
+ )
34
+ RISK_TERMS = (
35
+ "push directly",
36
+ "push to production",
37
+ "production",
38
+ "skip tests",
39
+ "delete the old tests",
40
+ "rm -rf",
41
+ "force push",
42
+ )
43
+ REVIEW_TERMS = (
44
+ "review",
45
+ "findings",
46
+ "audit",
47
+ "check the diff",
48
+ "code review",
49
+ )
50
+ DEBUG_TERMS = (
51
+ "debug",
52
+ "bug",
53
+ "traceback",
54
+ "error",
55
+ "failing",
56
+ "root cause",
57
+ "reproduce",
58
+ )
59
+ RESEARCH_TERMS = (
60
+ "research",
61
+ "look up",
62
+ "source",
63
+ "sources",
64
+ "docs",
65
+ "compare options",
66
+ "what is",
67
+ "how does",
68
+ )
69
+ EVAL_TERMS = (
70
+ "eval",
71
+ "benchmark",
72
+ "metric",
73
+ "measure",
74
+ "compare prompts",
75
+ "goldset",
76
+ "real sessions",
77
+ )
78
+ META_TERMS = (
79
+ "meta",
80
+ "harness",
81
+ "sidecar",
82
+ "governor",
83
+ "workflow",
84
+ ".codex",
85
+ "claude code",
86
+ "codex runtime",
87
+ )
88
+ SCOPE_TERMS = ("only", "scope", "module", "repo", "file", "folder", "path", "directory")
89
+ CHECK_TERMS = ("test", "tests", "pytest", "smoke", "check", "verify", "done")
90
+
91
+ PROMPT_RISK_MAP = {
92
+ "risk_direct_production": ("production", "Prompt pushes directly toward production risk."),
93
+ "risk_skip_tests": ("skip tests", "Prompt asks to bypass verification."),
94
+ "risk_force_push": ("force push", "Prompt asks for a force-push style workflow."),
95
+ "risk_destructive_delete": ("rm -rf", "Prompt contains destructive deletion intent."),
96
+ }
97
+
98
+ SHELL_RISK_PATTERNS = {
99
+ "dangerous_rm": r"\brm\s+-rf\b",
100
+ "git_reset_hard": r"\bgit\s+reset\s+--hard\b",
101
+ "force_push": r"\bgit\s+push\b.*--force",
102
+ "production_deploy": r"\bdeploy\b.*\bprod",
103
+ "sudo_write": r"\bsudo\b",
104
+ }
105
+
106
+ TEST_COMMAND_PATTERNS = (
107
+ "pytest",
108
+ "unittest",
109
+ "npm test",
110
+ "pnpm test",
111
+ "cargo test",
112
+ "go test",
113
+ "uv run pytest",
114
+ )
115
+
116
+
117
+ def contract_for_prompt(prompt: str, cwd: str | Path | None = None) -> ModeContract | None:
118
+ """Return a compact mode contract, or None for fail-open empty input."""
119
+ prompt = " ".join((prompt or "").split())
120
+ if not prompt:
121
+ return None
122
+ mode, reason = classify_prompt(prompt, cwd=cwd, with_reason=True)
123
+ target = _target_from_prompt(prompt)
124
+ return _contract(mode, target, reason)
125
+
126
+
127
+ def classify_prompt(
128
+ prompt: str,
129
+ cwd: str | Path | None = None,
130
+ *,
131
+ with_reason: bool = False,
132
+ ) -> str | tuple[str, str]:
133
+ """Classify a prompt into the bounded sidecar modes."""
134
+ prompt = " ".join((prompt or "").split())
135
+ lower = prompt.lower()
136
+ cwd_path = Path(cwd or ".").resolve()
137
+
138
+ explicit = _explicit_mode(lower)
139
+ if explicit:
140
+ if explicit == "build":
141
+ reason = _build_blocker(prompt, cwd_path)
142
+ if reason:
143
+ return _result("plan", reason, with_reason)
144
+ return _result(explicit, "explicit_mode", with_reason)
145
+
146
+ if _has_any(lower, REVIEW_TERMS):
147
+ return _result("review", "review_terms", with_reason)
148
+ if _has_any(lower, DEBUG_TERMS):
149
+ return _result("debug", "debug_terms", with_reason)
150
+ if _has_any(lower, EVAL_TERMS):
151
+ return _result("eval", "eval_terms", with_reason)
152
+ if _has_any(lower, RESEARCH_TERMS):
153
+ return _result("research", "research_terms", with_reason)
154
+ if _has_any(lower, META_TERMS):
155
+ return _result("meta", "meta_terms", with_reason)
156
+ if _has_any(lower, RISK_TERMS):
157
+ return _result("plan", "risk_requires_plan", with_reason)
158
+ if _has_any(lower, BUILD_TERMS):
159
+ reason = _build_blocker(prompt, cwd_path)
160
+ if reason:
161
+ return _result("plan", reason, with_reason)
162
+ return _result("build", "build_ready", with_reason)
163
+ if _has_any(lower, PLAN_TERMS) or _looks_fuzzy(lower):
164
+ return _result("plan", "planning_or_fuzzy", with_reason)
165
+ return _result("plan", "default_plan", with_reason)
166
+
167
+
168
+ def render_contract(contract: ModeContract) -> str:
169
+ """Render a stable, explicit user-visible contract."""
170
+ return "\n".join(
171
+ [
172
+ f"Mode: {contract.mode}",
173
+ f"Reason: {contract.reason}",
174
+ f"Goal: {contract.goal}",
175
+ f"Non-Goal: {contract.non_goal}",
176
+ f"Stop-Condition: {contract.stop_condition}",
177
+ f"Allowed Behavior: {contract.allowed_behavior}",
178
+ f"Failure Signal: {contract.failure_signal}",
179
+ ]
180
+ )
181
+
182
+
183
+ def detect_prompt_risks(prompt: str) -> list[Finding]:
184
+ """Detect explicitly risky prompt intent."""
185
+ lower = prompt.lower()
186
+ findings: list[Finding] = []
187
+ for code, (needle, message) in PROMPT_RISK_MAP.items():
188
+ if needle in lower:
189
+ findings.append(
190
+ Finding(
191
+ code=code,
192
+ severity="high",
193
+ message=message,
194
+ recommendation="Switch to plan mode and require an explicit verification path.",
195
+ )
196
+ )
197
+ return findings
198
+
199
+
200
+ def detect_shell_risks(command: str) -> list[Finding]:
201
+ """Detect clearly risky shell commands."""
202
+ findings: list[Finding] = []
203
+ for code, pattern in SHELL_RISK_PATTERNS.items():
204
+ if re.search(pattern, command):
205
+ findings.append(
206
+ Finding(
207
+ code=code,
208
+ severity="high",
209
+ message=f"Observed risky shell command: {command}",
210
+ recommendation="Stop and switch to plan unless the wrapper owner explicitly forces execution.",
211
+ )
212
+ )
213
+ return findings
214
+
215
+
216
+ def advise_on_snapshot(snapshot: SessionSnapshot) -> tuple[Finding, ...]:
217
+ """Produce advisory findings from an observed session snapshot."""
218
+ findings: list[Finding] = []
219
+ findings.extend(detect_prompt_risks(snapshot.prompt))
220
+
221
+ if snapshot.contract and snapshot.contract.mode == "build" and not snapshot.verification_seen:
222
+ findings.append(
223
+ Finding(
224
+ code="verification_missing",
225
+ severity="medium",
226
+ message="Build-like activity was observed without a verification signal.",
227
+ recommendation="Switch to plan or add a concrete test/smoke-check before treating the task as complete.",
228
+ )
229
+ )
230
+
231
+ if snapshot.subagent:
232
+ findings.append(
233
+ Finding(
234
+ code="subagent_lineage_present",
235
+ severity="info",
236
+ message="Session originated from a spawned subagent thread.",
237
+ recommendation="Keep summaries explicit about parent thread, depth, and delegated role.",
238
+ )
239
+ )
240
+
241
+ for call in snapshot.tool_calls:
242
+ if call.shell_command:
243
+ findings.extend(detect_shell_risks(call.shell_command))
244
+ if call.exit_code not in (None, 0):
245
+ findings.append(
246
+ Finding(
247
+ code="tool_error",
248
+ severity="medium",
249
+ message=f"Tool call {call.tool_name} exited with code {call.exit_code}.",
250
+ recommendation="Summarize the failure and decide whether the next step is debug or plan.",
251
+ )
252
+ )
253
+ return tuple(_dedupe_findings(findings))
254
+
255
+
256
+ def wrapper_decision(prompt: str, cwd: str | Path | None = None) -> WrapperDecision:
257
+ """Return the explicit wrapper-mode launch decision."""
258
+ contract = contract_for_prompt(prompt, cwd=cwd)
259
+ if contract is None:
260
+ fallback = _contract("plan", "The current task", "empty_prompt")
261
+ return WrapperDecision(
262
+ allowed=True,
263
+ reason="empty_prompt_fail_open",
264
+ contract=fallback,
265
+ findings=(),
266
+ )
267
+
268
+ findings = tuple(detect_prompt_risks(prompt))
269
+ blocking_codes = {
270
+ "risk_direct_production",
271
+ "risk_skip_tests",
272
+ "risk_force_push",
273
+ "risk_destructive_delete",
274
+ }
275
+ if any(finding.code in blocking_codes for finding in findings):
276
+ return WrapperDecision(
277
+ allowed=False,
278
+ reason="wrapper_refused_risky_prompt",
279
+ contract=contract,
280
+ findings=findings,
281
+ )
282
+ return WrapperDecision(
283
+ allowed=True,
284
+ reason="wrapper_allowed_visible_contract",
285
+ contract=contract,
286
+ findings=findings,
287
+ )
288
+
289
+
290
+ def _contract(mode: str, target: str, reason: str) -> ModeContract:
291
+ if mode == "plan":
292
+ return ModeContract(
293
+ mode="plan",
294
+ reason=reason,
295
+ goal=f"Turn {target} into a buildable plan or a single clear open question.",
296
+ non_goal="No code edits, no speculative runtime, no orchestration sprawl.",
297
+ stop_condition="Stops with a buildable plan, or with one explicit blocking question.",
298
+ allowed_behavior="Read/search/replay only. Use advisory findings, not hidden injection or fake blocking.",
299
+ failure_signal="The task is still fuzzy, risky, or not verification-ready.",
300
+ )
301
+ if mode == "build":
302
+ return ModeContract(
303
+ mode="build",
304
+ reason=reason,
305
+ goal=f"Implement {target} as one stable, testable change.",
306
+ non_goal="No scope creep, no unrelated refactors, no new controller layer.",
307
+ stop_condition="Stops with a diff plus a concrete test, smoke-check, or explicit failure.",
308
+ allowed_behavior="Edit only within clear scope and keep verification visible.",
309
+ failure_signal="Repo, scope, or verification signal becomes unclear and must fall back to plan.",
310
+ )
311
+ if mode == "review":
312
+ return ModeContract(
313
+ mode="review",
314
+ reason=reason,
315
+ goal=f"Assess {target} for bugs, regressions, and missing checks.",
316
+ non_goal="No fixes, no refactors, no second build task.",
317
+ stop_condition='Stops with findings or an explicit "no findings" result plus remaining test gaps.',
318
+ allowed_behavior="Read/search/diff/tests for evidence only.",
319
+ failure_signal="The review scope mutates into implementation or the evidence is weak.",
320
+ )
321
+ if mode == "debug":
322
+ return ModeContract(
323
+ mode="debug",
324
+ reason=reason,
325
+ goal=f"Reproduce and narrow {target}, then fix minimally or define the next experiment.",
326
+ non_goal="No feature work, no guesswork, no cosmetic refactor.",
327
+ stop_condition="Stops with root cause, minimal fix, or the next reproducible experiment.",
328
+ allowed_behavior="Prefer reproduction, logs, and minimal edits after a concrete hypothesis.",
329
+ failure_signal="No reproduction, no hypothesis, or the fix grows beyond the bug.",
330
+ )
331
+ if mode == "research":
332
+ return ModeContract(
333
+ mode="research",
334
+ reason=reason,
335
+ goal=f"Clarify {target} with sources, options, and a recommendation.",
336
+ non_goal="No implementation and no unsourced claims.",
337
+ stop_condition="Stops with sources, options, and a decision recommendation.",
338
+ allowed_behavior="Use replay, docs, and repo reads. Keep tradeoffs explicit.",
339
+ failure_signal="No reliable sources or the question fragments into multiple decisions.",
340
+ )
341
+ if mode == "eval":
342
+ return ModeContract(
343
+ mode="eval",
344
+ reason=reason,
345
+ goal=f"Measure or compare {target} into a usable result.",
346
+ non_goal="No product feature work during measurement.",
347
+ stop_condition="Stops with a metric, comparison, or a clearly testable hypothesis.",
348
+ allowed_behavior="Use sessions, logs, tables, and reproducible checks only.",
349
+ failure_signal="The metric is not measurable or the result would be anecdotal.",
350
+ )
351
+ if mode == "meta":
352
+ return ModeContract(
353
+ mode="meta",
354
+ reason=reason,
355
+ goal=f"Simplify or clarify {target} without inventing a new control plane.",
356
+ non_goal="No new agent system, no hidden injector, no daemon-first architecture.",
357
+ stop_condition="Stops with lower complexity or a clear reason to stop.",
358
+ allowed_behavior="Read existing rules and keep changes bounded to explicit interfaces.",
359
+ failure_signal="Meta work starts creating more machinery than it removes.",
360
+ )
361
+ raise ValueError(f"unknown mode: {mode}")
362
+
363
+
364
+ def _result(mode: str, reason: str, with_reason: bool) -> str | tuple[str, str]:
365
+ return (mode, reason) if with_reason else mode
366
+
367
+
368
+ def _explicit_mode(lower: str) -> str | None:
369
+ match = re.match(r"^/?(plan|build|review|debug|research|eval|meta)(?:mode)?\b", lower)
370
+ return match.group(1) if match else None
371
+
372
+
373
+ def _build_blocker(prompt: str, cwd: Path) -> str | None:
374
+ lower = prompt.lower()
375
+ if not _repo_is_clear(cwd):
376
+ return "build_gated_no_repo"
377
+ if len(prompt.strip()) < 16:
378
+ return "build_gated_unclear_goal"
379
+ if not (_has_any(lower, SCOPE_TERMS) or len(prompt.split()) >= 6):
380
+ return "build_gated_unclear_scope"
381
+ if not _has_any(lower, CHECK_TERMS):
382
+ return "build_gated_missing_check"
383
+ return None
384
+
385
+
386
+ def _repo_is_clear(cwd: Path) -> bool:
387
+ for path in (cwd, *cwd.parents):
388
+ if (path / ".git").exists():
389
+ return True
390
+ return False
391
+
392
+
393
+ def _target_from_prompt(prompt: str) -> str:
394
+ target = prompt.strip()
395
+ target = re.sub(
396
+ r"^/?(?:plan|build|review|debug|research|eval|meta)(?:mode)?[:\s-]*",
397
+ "",
398
+ target,
399
+ flags=re.I,
400
+ )
401
+ if not target:
402
+ return "the current task"
403
+ if len(target) > 140:
404
+ return target[:137].rstrip() + "..."
405
+ return target
406
+
407
+
408
+ def _has_any(text: str, terms: tuple[str, ...]) -> bool:
409
+ return any(term in text for term in terms)
410
+
411
+
412
+ def _looks_fuzzy(lower: str) -> bool:
413
+ return "?" in lower or any(
414
+ term in lower
415
+ for term in ("how can", "what do you think", "idea", "unclear", "everything at once")
416
+ )
417
+
418
+
419
+ def _dedupe_findings(findings: list[Finding]) -> list[Finding]:
420
+ seen: set[tuple[str, str]] = set()
421
+ unique: list[Finding] = []
422
+ for finding in findings:
423
+ key = (finding.code, finding.message)
424
+ if key in seen:
425
+ continue
426
+ seen.add(key)
427
+ unique.append(finding)
428
+ return unique
codex-sidecarV1/src/codex_sidecar/models.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared data models for Codex sidecar V1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class NormalizedEvent:
11
+ """Canonical event shape extracted from Codex JSONL traces."""
12
+
13
+ kind: str
14
+ session_id: str
15
+ timestamp: float
16
+ raw_type: str
17
+ call_id: str = ""
18
+ tool_name: str = ""
19
+ cwd: str = ""
20
+ message: str = ""
21
+ arguments: dict[str, Any] = field(default_factory=dict)
22
+ output: str = ""
23
+ data: dict[str, Any] = field(default_factory=dict)
24
+
25
+ def to_dict(self) -> dict[str, Any]:
26
+ return asdict(self)
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class ModeContract:
31
+ """Compact explicit contract for a prompt or replayed task."""
32
+
33
+ mode: str
34
+ reason: str
35
+ goal: str
36
+ non_goal: str
37
+ stop_condition: str
38
+ allowed_behavior: str
39
+ failure_signal: str
40
+
41
+ def to_dict(self) -> dict[str, str]:
42
+ return asdict(self)
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class Finding:
47
+ """Advisory output produced by the governor."""
48
+
49
+ code: str
50
+ severity: str
51
+ message: str
52
+ recommendation: str
53
+
54
+ def to_dict(self) -> dict[str, str]:
55
+ return asdict(self)
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class ToolCall:
60
+ """Normalized tool-call record reconstructed from replay data."""
61
+
62
+ call_id: str
63
+ tool_name: str
64
+ arguments: dict[str, Any]
65
+ started_at: float
66
+ completed_at: float = 0.0
67
+ output: str = ""
68
+ cwd: str = ""
69
+ shell_command: str = ""
70
+ exit_code: int | None = None
71
+ status: str = "started"
72
+ touched_files: tuple[str, ...] = ()
73
+ risk_codes: tuple[str, ...] = ()
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ return asdict(self)
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class WrapperDecision:
81
+ """Wrapper-mode decision for pre-launch boundaries."""
82
+
83
+ allowed: bool
84
+ reason: str
85
+ contract: ModeContract
86
+ findings: tuple[Finding, ...]
87
+
88
+ def to_dict(self) -> dict[str, Any]:
89
+ return {
90
+ "allowed": self.allowed,
91
+ "reason": self.reason,
92
+ "contract": self.contract.to_dict(),
93
+ "findings": [finding.to_dict() for finding in self.findings],
94
+ }
95
+
96
+
97
+ @dataclass(frozen=True, slots=True)
98
+ class SessionSnapshot:
99
+ """Current truth reconstructed from a Codex rollout file."""
100
+
101
+ session_id: str
102
+ session_path: str
103
+ cwd: str
104
+ started_at: float
105
+ last_timestamp: float
106
+ workflow_phase: str
107
+ workflow_transitions: tuple[dict[str, Any], ...]
108
+ prompt: str
109
+ contract: ModeContract | None
110
+ tool_calls: tuple[ToolCall, ...]
111
+ touched_files: tuple[str, ...]
112
+ subagent: dict[str, Any]
113
+ agent_messages: tuple[str, ...]
114
+ risky_intent_codes: tuple[str, ...]
115
+ findings: tuple[Finding, ...]
116
+ errors: int
117
+ verification_seen: bool
118
+ rate_limits: dict[str, Any]
119
+ capabilities: dict[str, Any]
120
+
121
+ def to_dict(self) -> dict[str, Any]:
122
+ payload = asdict(self)
123
+ payload["contract"] = self.contract.to_dict() if self.contract else None
124
+ payload["tool_calls"] = [call.to_dict() for call in self.tool_calls]
125
+ payload["findings"] = [finding.to_dict() for finding in self.findings]
126
+ return payload
codex-sidecarV1/src/codex_sidecar/state.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """State reconstruction for Codex sidecar V1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import re
7
+ import shlex
8
+ from typing import Any
9
+
10
+ from .capabilities import CODEX_V1_CAPABILITIES
11
+ from .governor import TEST_COMMAND_PATTERNS, advise_on_snapshot, contract_for_prompt, detect_prompt_risks, detect_shell_risks
12
+ from .models import SessionSnapshot, ToolCall
13
+ from .watcher import normalize_session_file
14
+
15
+ PATH_KEYS = ("path", "file", "file_path", "workdir", "cwd", "target", "target_file")
16
+
17
+
18
+ def build_snapshot(path: str | Path) -> SessionSnapshot:
19
+ """Build a replay snapshot from a rollout file."""
20
+ events = normalize_session_file(path)
21
+ raw_path = str(Path(path).expanduser())
22
+ session_id = ""
23
+ cwd = ""
24
+ started_at = 0.0
25
+ last_timestamp = 0.0
26
+ workflow_phase = "bootstrapping"
27
+ workflow_transitions: list[dict[str, Any]] = []
28
+ prompt = ""
29
+ agent_messages: list[str] = []
30
+ risky_codes: set[str] = set()
31
+ rate_limits: dict[str, Any] = {}
32
+ subagent: dict[str, Any] = {}
33
+ turn_context_cwd = ""
34
+ pending_calls: dict[str, dict[str, Any]] = {}
35
+ finalized_calls: list[ToolCall] = []
36
+ touched_files: list[str] = []
37
+
38
+ def transition(phase: str, reason: str, timestamp: float) -> None:
39
+ nonlocal workflow_phase
40
+ if workflow_phase == phase:
41
+ return
42
+ workflow_phase = phase
43
+ workflow_transitions.append(
44
+ {"phase": phase, "reason": reason, "timestamp": timestamp}
45
+ )
46
+
47
+ for event in events:
48
+ if not started_at and event.timestamp:
49
+ started_at = event.timestamp
50
+ last_timestamp = max(last_timestamp, event.timestamp)
51
+
52
+ if event.kind == "session_meta":
53
+ session_id = event.session_id or session_id
54
+ cwd = event.cwd or cwd
55
+ source = event.data.get("source", {})
56
+ thread_spawn = source.get("subagent", {}).get("thread_spawn", {})
57
+ if thread_spawn:
58
+ subagent = {
59
+ "parent_thread_id": thread_spawn.get("parent_thread_id", ""),
60
+ "depth": thread_spawn.get("depth", 0),
61
+ "agent_nickname": event.data.get("agent_nickname", ""),
62
+ "agent_role": event.data.get("agent_role", ""),
63
+ }
64
+ transition("bootstrapping", "session_meta", event.timestamp)
65
+ continue
66
+
67
+ if event.kind == "turn_context":
68
+ turn_context_cwd = event.cwd or turn_context_cwd
69
+ continue
70
+
71
+ if event.kind == "user_prompt" and event.message and not event.message.startswith("<environment_context>"):
72
+ prompt = event.message
73
+ risky_codes.update(finding.code for finding in detect_prompt_risks(prompt))
74
+ transition("prompted", "user_prompt", event.timestamp)
75
+ continue
76
+
77
+ if event.kind == "task_started":
78
+ transition("prompted", "task_started", event.timestamp)
79
+ continue
80
+
81
+ if event.kind == "tool_call":
82
+ call_data = {
83
+ "call_id": event.call_id,
84
+ "tool_name": event.tool_name,
85
+ "arguments": dict(event.arguments),
86
+ "started_at": event.timestamp,
87
+ "cwd": cwd or turn_context_cwd,
88
+ }
89
+ if event.tool_name == "exec_command":
90
+ shell_command = str(event.arguments.get("cmd", "")).strip()
91
+ call_data["shell_command"] = shell_command
92
+ risky_codes.update(finding.code for finding in detect_shell_risks(shell_command))
93
+ call_data["touched_files"] = tuple(_extract_paths_from_shell(shell_command))
94
+ else:
95
+ call_data["shell_command"] = ""
96
+ call_data["touched_files"] = tuple(_extract_paths_from_args(event.arguments))
97
+ pending_calls[event.call_id] = call_data
98
+ for candidate in call_data["touched_files"]:
99
+ if candidate not in touched_files:
100
+ touched_files.append(candidate)
101
+ transition("tooling", f"tool_call:{event.tool_name}", event.timestamp)
102
+ continue
103
+
104
+ if event.kind == "tool_output":
105
+ call = pending_calls.get(event.call_id)
106
+ if call is not None:
107
+ call["output"] = event.output
108
+ exit_code = _extract_exit_code(event.output)
109
+ if exit_code is not None:
110
+ call["exit_code"] = exit_code
111
+ call["status"] = "completed" if exit_code == 0 else "failed"
112
+ continue
113
+
114
+ if event.kind == "command_end":
115
+ call = pending_calls.get(event.call_id)
116
+ if call is not None:
117
+ payload = event.data
118
+ command = payload.get("command", [])
119
+ call["cwd"] = event.cwd or call["cwd"]
120
+ call["completed_at"] = event.timestamp
121
+ call["status"] = str(payload.get("status", "completed"))
122
+ call["output"] = event.output or call.get("output", "")
123
+ exit_code = payload.get("exit_code")
124
+ call["exit_code"] = int(exit_code) if isinstance(exit_code, int) else call.get("exit_code")
125
+ if not call.get("shell_command") and isinstance(command, list):
126
+ shell_command = " ".join(str(part) for part in command if part)
127
+ call["shell_command"] = shell_command
128
+ continue
129
+
130
+ if event.kind == "agent_message" and event.message:
131
+ agent_messages.append(event.message)
132
+ transition("responded", "agent_message", event.timestamp)
133
+ continue
134
+
135
+ if event.kind == "task_complete":
136
+ if event.message:
137
+ agent_messages.append(event.message)
138
+ transition("responded", "task_complete", event.timestamp)
139
+ continue
140
+
141
+ if event.kind == "token_count":
142
+ rate_limits = dict(event.data.get("rate_limits", {}))
143
+
144
+ for call in pending_calls.values():
145
+ finalized_calls.append(
146
+ ToolCall(
147
+ call_id=str(call.get("call_id", "")),
148
+ tool_name=str(call.get("tool_name", "")),
149
+ arguments=dict(call.get("arguments", {})),
150
+ started_at=float(call.get("started_at", 0.0)),
151
+ completed_at=float(call.get("completed_at", 0.0)),
152
+ output=str(call.get("output", "")),
153
+ cwd=str(call.get("cwd", "")),
154
+ shell_command=str(call.get("shell_command", "")),
155
+ exit_code=call.get("exit_code"),
156
+ status=str(call.get("status", "started")),
157
+ touched_files=tuple(call.get("touched_files", ())),
158
+ risk_codes=tuple(sorted(risky_codes_for_call(call))),
159
+ )
160
+ )
161
+
162
+ active_cwd = cwd or turn_context_cwd or str(Path(raw_path).parent)
163
+ contract = contract_for_prompt(prompt, cwd=active_cwd) if prompt else None
164
+ verification_seen = any(_call_has_verification(call) for call in finalized_calls)
165
+ errors = sum(1 for call in finalized_calls if call.exit_code not in (None, 0))
166
+
167
+ provisional = SessionSnapshot(
168
+ session_id=session_id or Path(raw_path).stem,
169
+ session_path=raw_path,
170
+ cwd=active_cwd,
171
+ started_at=started_at,
172
+ last_timestamp=last_timestamp,
173
+ workflow_phase=workflow_phase,
174
+ workflow_transitions=tuple(workflow_transitions),
175
+ prompt=prompt,
176
+ contract=contract,
177
+ tool_calls=tuple(finalized_calls),
178
+ touched_files=tuple(touched_files),
179
+ subagent=subagent,
180
+ agent_messages=tuple(agent_messages),
181
+ risky_intent_codes=tuple(sorted(risky_codes)),
182
+ findings=(),
183
+ errors=errors,
184
+ verification_seen=verification_seen,
185
+ rate_limits=rate_limits,
186
+ capabilities=CODEX_V1_CAPABILITIES.to_dict(),
187
+ )
188
+ findings = advise_on_snapshot(provisional)
189
+ return SessionSnapshot(
190
+ session_id=provisional.session_id,
191
+ session_path=provisional.session_path,
192
+ cwd=provisional.cwd,
193
+ started_at=provisional.started_at,
194
+ last_timestamp=provisional.last_timestamp,
195
+ workflow_phase=provisional.workflow_phase,
196
+ workflow_transitions=provisional.workflow_transitions,
197
+ prompt=provisional.prompt,
198
+ contract=provisional.contract,
199
+ tool_calls=provisional.tool_calls,
200
+ touched_files=provisional.touched_files,
201
+ subagent=provisional.subagent,
202
+ agent_messages=provisional.agent_messages,
203
+ risky_intent_codes=provisional.risky_intent_codes,
204
+ findings=findings,
205
+ errors=provisional.errors,
206
+ verification_seen=provisional.verification_seen,
207
+ rate_limits=provisional.rate_limits,
208
+ capabilities=provisional.capabilities,
209
+ )
210
+
211
+
212
+ def _call_has_verification(call: ToolCall) -> bool:
213
+ command = call.shell_command.lower()
214
+ return any(pattern in command for pattern in TEST_COMMAND_PATTERNS)
215
+
216
+
217
+ def _extract_exit_code(output: str) -> int | None:
218
+ match = re.search(r"Process exited with code (\d+)", output)
219
+ if match:
220
+ return int(match.group(1))
221
+ return None
222
+
223
+
224
+ def _extract_paths_from_args(arguments: dict[str, Any]) -> list[str]:
225
+ candidates: list[str] = []
226
+ for key, value in arguments.items():
227
+ if key not in PATH_KEYS:
228
+ continue
229
+ if isinstance(value, str) and _looks_like_path(value):
230
+ candidates.append(value)
231
+ return candidates
232
+
233
+
234
+ def _extract_paths_from_shell(command: str) -> list[str]:
235
+ try:
236
+ tokens = shlex.split(command)
237
+ except ValueError:
238
+ return []
239
+
240
+ candidates: list[str] = []
241
+ for token in tokens:
242
+ if token.startswith("-"):
243
+ continue
244
+ if _looks_like_path(token):
245
+ candidates.append(token)
246
+ return candidates
247
+
248
+
249
+ def _looks_like_path(value: str) -> bool:
250
+ if not value or value.startswith("<"):
251
+ return False
252
+ if "/" in value:
253
+ return True
254
+ return value.endswith((".py", ".md", ".toml", ".json", ".yaml", ".yml", ".txt", ".html", ".css", ".js", ".ts"))
255
+
256
+
257
+ def risky_codes_for_call(call: dict[str, Any]) -> tuple[str, ...]:
258
+ shell_command = str(call.get("shell_command", ""))
259
+ return tuple(sorted(finding.code for finding in detect_shell_risks(shell_command)))
codex-sidecarV1/src/codex_sidecar/watcher.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Replay-oriented watcher over Codex JSONL sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ import json
8
+ from typing import Any, Iterator
9
+
10
+ from .models import NormalizedEvent
11
+
12
+
13
+ def session_root(default: str | Path | None = None) -> Path:
14
+ """Return the default Codex sessions root."""
15
+ if default is not None:
16
+ return Path(default).expanduser()
17
+ return Path.home() / ".codex" / "sessions"
18
+
19
+
20
+ def find_latest_session(root: str | Path | None = None) -> Path | None:
21
+ """Return the newest JSONL rollout file under the sessions root."""
22
+ entries = sorted(session_root(root).rglob("*.jsonl"))
23
+ return entries[-1] if entries else None
24
+
25
+
26
+ def resolve_session_path(session: str | Path, root: str | Path | None = None) -> Path:
27
+ """Resolve a user-supplied session id or path to a JSONL file."""
28
+ path = Path(session).expanduser()
29
+ if path.exists():
30
+ return path
31
+
32
+ root_path = session_root(root)
33
+ candidates = sorted(root_path.rglob(f"*{session}*.jsonl"))
34
+ if not candidates:
35
+ raise FileNotFoundError(f"could not resolve session: {session}")
36
+ return candidates[-1]
37
+
38
+
39
+ def iter_raw_records(path: str | Path) -> Iterator[dict[str, Any]]:
40
+ """Yield raw JSON objects from a rollout JSONL file."""
41
+ with Path(path).expanduser().open(encoding="utf-8") as handle:
42
+ for line in handle:
43
+ stripped = line.strip()
44
+ if not stripped:
45
+ continue
46
+ yield json.loads(stripped)
47
+
48
+
49
+ def normalize_record(raw: dict[str, Any]) -> list[NormalizedEvent]:
50
+ """Normalize one raw Codex JSONL record into zero or more canonical events."""
51
+ raw_type = str(raw.get("type", ""))
52
+ payload = raw.get("payload", {})
53
+ timestamp = _coerce_timestamp(raw.get("timestamp"))
54
+
55
+ if raw_type == "session_meta":
56
+ session_id = str(payload.get("id", ""))
57
+ return [
58
+ NormalizedEvent(
59
+ kind="session_meta",
60
+ session_id=session_id,
61
+ timestamp=timestamp,
62
+ raw_type=raw_type,
63
+ cwd=str(payload.get("cwd", "")),
64
+ data=payload,
65
+ )
66
+ ]
67
+
68
+ if raw_type == "turn_context":
69
+ return [
70
+ NormalizedEvent(
71
+ kind="turn_context",
72
+ session_id="",
73
+ timestamp=timestamp,
74
+ raw_type=raw_type,
75
+ cwd=str(payload.get("cwd", "")),
76
+ data=payload,
77
+ )
78
+ ]
79
+
80
+ session_id = str(raw.get("session_id", ""))
81
+
82
+ if raw_type == "response_item":
83
+ payload_type = str(payload.get("type", ""))
84
+ if payload_type == "function_call":
85
+ return [
86
+ NormalizedEvent(
87
+ kind="tool_call",
88
+ session_id=session_id,
89
+ timestamp=timestamp,
90
+ raw_type=raw_type,
91
+ call_id=str(payload.get("call_id", "")),
92
+ tool_name=str(payload.get("name", "")),
93
+ arguments=_decode_arguments(payload.get("arguments")),
94
+ data=payload,
95
+ )
96
+ ]
97
+ if payload_type == "function_call_output":
98
+ return [
99
+ NormalizedEvent(
100
+ kind="tool_output",
101
+ session_id=session_id,
102
+ timestamp=timestamp,
103
+ raw_type=raw_type,
104
+ call_id=str(payload.get("call_id", "")),
105
+ output=_coerce_text(payload.get("output")),
106
+ data=payload,
107
+ )
108
+ ]
109
+ if payload_type == "message":
110
+ role = str(payload.get("role", ""))
111
+ content_text = _message_text(payload.get("content", []))
112
+ kind = "user_prompt" if role == "user" and content_text else "agent_message"
113
+ return [
114
+ NormalizedEvent(
115
+ kind=kind,
116
+ session_id=session_id,
117
+ timestamp=timestamp,
118
+ raw_type=raw_type,
119
+ message=content_text,
120
+ data=payload,
121
+ )
122
+ ]
123
+ return []
124
+
125
+ if raw_type == "event_msg":
126
+ payload_type = str(payload.get("type", ""))
127
+ if payload_type == "task_started":
128
+ return [
129
+ NormalizedEvent(
130
+ kind="task_started",
131
+ session_id=session_id,
132
+ timestamp=timestamp,
133
+ raw_type=raw_type,
134
+ data=payload,
135
+ )
136
+ ]
137
+ if payload_type == "task_complete":
138
+ return [
139
+ NormalizedEvent(
140
+ kind="task_complete",
141
+ session_id=session_id,
142
+ timestamp=timestamp,
143
+ raw_type=raw_type,
144
+ message=str(payload.get("last_agent_message", "")),
145
+ data=payload,
146
+ )
147
+ ]
148
+ if payload_type == "agent_message":
149
+ return [
150
+ NormalizedEvent(
151
+ kind="agent_message",
152
+ session_id=session_id,
153
+ timestamp=timestamp,
154
+ raw_type=raw_type,
155
+ message=str(payload.get("message", "")),
156
+ data=payload,
157
+ )
158
+ ]
159
+ if payload_type == "user_message":
160
+ return [
161
+ NormalizedEvent(
162
+ kind="user_prompt",
163
+ session_id=session_id,
164
+ timestamp=timestamp,
165
+ raw_type=raw_type,
166
+ message=str(payload.get("message", "")),
167
+ data=payload,
168
+ )
169
+ ]
170
+ if payload_type == "token_count":
171
+ return [
172
+ NormalizedEvent(
173
+ kind="token_count",
174
+ session_id=session_id,
175
+ timestamp=timestamp,
176
+ raw_type=raw_type,
177
+ data=payload,
178
+ )
179
+ ]
180
+ if payload_type == "exec_command_end":
181
+ return [
182
+ NormalizedEvent(
183
+ kind="command_end",
184
+ session_id=session_id,
185
+ timestamp=timestamp,
186
+ raw_type=raw_type,
187
+ call_id=str(payload.get("call_id", "")),
188
+ cwd=str(payload.get("cwd", "")),
189
+ output=str(payload.get("aggregated_output", "")),
190
+ data=payload,
191
+ )
192
+ ]
193
+ return []
194
+
195
+
196
+ def normalize_session_file(path: str | Path) -> list[NormalizedEvent]:
197
+ """Normalize an entire rollout JSONL file."""
198
+ events: list[NormalizedEvent] = []
199
+ for raw in iter_raw_records(path):
200
+ events.extend(normalize_record(raw))
201
+ return events
202
+
203
+
204
+ def _decode_arguments(arguments: Any) -> dict[str, Any]:
205
+ if isinstance(arguments, dict):
206
+ return arguments
207
+ if isinstance(arguments, str):
208
+ try:
209
+ decoded = json.loads(arguments)
210
+ except json.JSONDecodeError:
211
+ return {"raw_arguments": arguments}
212
+ if isinstance(decoded, dict):
213
+ return decoded
214
+ return {"value": decoded}
215
+ return {}
216
+
217
+
218
+ def _coerce_text(value: Any) -> str:
219
+ if value is None:
220
+ return ""
221
+ if isinstance(value, str):
222
+ return value
223
+ return json.dumps(value, ensure_ascii=True, sort_keys=True)
224
+
225
+
226
+ def _message_text(content: Any) -> str:
227
+ if not isinstance(content, list):
228
+ return ""
229
+ parts: list[str] = []
230
+ for item in content:
231
+ if not isinstance(item, dict):
232
+ continue
233
+ if item.get("type") == "input_text":
234
+ text = str(item.get("text", "")).strip()
235
+ if text:
236
+ parts.append(text)
237
+ return "\n".join(parts).strip()
238
+
239
+
240
+ def _coerce_timestamp(value: Any) -> float:
241
+ if isinstance(value, (int, float)):
242
+ return float(value)
243
+ if isinstance(value, str) and value:
244
+ normalized = value.replace("Z", "+00:00")
245
+ try:
246
+ return datetime.fromisoformat(normalized).timestamp()
247
+ except ValueError:
248
+ return 0.0
249
+ return 0.0
codex-sidecarV1/tests/fixtures/build_without_checks.jsonl ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {"timestamp":"2026-04-24T12:00:00.000Z","type":"session_meta","payload":{"id":"019dbf14-build-0000","timestamp":"2026-04-24T12:00:00.000Z","cwd":"/home/smlflg/Projekte/codex-sidecarV1","originator":"codex-tui","cli_version":"0.124.0","model_provider":"openai"}}
2
+ {"timestamp":"2026-04-24T12:00:01.000Z","type":"event_msg","session_id":"019dbf14-build-0000","payload":{"type":"task_started","turn_id":"turn-build","started_at":1777027267}}
3
+ {"timestamp":"2026-04-24T12:00:02.000Z","type":"event_msg","session_id":"019dbf14-build-0000","payload":{"type":"user_message","message":"build only parser module in src/parser.py and update status output","images":[],"local_images":[],"text_elements":[]}}
4
+ {"timestamp":"2026-04-24T12:00:03.000Z","type":"response_item","session_id":"019dbf14-build-0000","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"sed -n '1,120p' src/parser.py\",\"workdir\":\"/home/smlflg/Projekte/codex-sidecarV1\"}","call_id":"call-build-read"}}
5
+ {"timestamp":"2026-04-24T12:00:03.500Z","type":"response_item","session_id":"019dbf14-build-0000","payload":{"type":"function_call_output","call_id":"call-build-read","output":"Chunk ID: 2\\nProcess exited with code 0\\nOutput:\\nparser"}}
6
+ {"timestamp":"2026-04-24T12:00:04.000Z","type":"event_msg","session_id":"019dbf14-build-0000","payload":{"type":"exec_command_end","call_id":"call-build-read","process_id":"124","turn_id":"turn-build","command":["/bin/bash","-lc","sed -n '1,120p' src/parser.py"],"cwd":"/home/smlflg/Projekte/codex-sidecarV1","parsed_cmd":[{"type":"unknown","cmd":"sed -n '1,120p' src/parser.py"}],"source":"unified_exec_startup","stdout":"","stderr":"","aggregated_output":"parser","exit_code":0,"duration":{"secs":0,"nanos":8031},"formatted_output":"","status":"completed"}}
7
+ {"timestamp":"2026-04-24T12:00:05.000Z","type":"event_msg","session_id":"019dbf14-build-0000","payload":{"type":"agent_message","message":"I updated the parser path mentally but no tests were requested.","phase":"commentary","memory_citation":null}}
codex-sidecarV1/tests/fixtures/risky_shell.jsonl ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {"timestamp":"2026-04-24T12:10:00.000Z","type":"session_meta","payload":{"id":"019dbf14-risk-0000","timestamp":"2026-04-24T12:10:00.000Z","cwd":"/home/smlflg/Projekte/codex-sidecarV1","originator":"codex-tui","cli_version":"0.124.0","model_provider":"openai"}}
2
+ {"timestamp":"2026-04-24T12:10:01.000Z","type":"event_msg","session_id":"019dbf14-risk-0000","payload":{"type":"task_started","turn_id":"turn-risk","started_at":1777027267}}
3
+ {"timestamp":"2026-04-24T12:10:02.000Z","type":"event_msg","session_id":"019dbf14-risk-0000","payload":{"type":"user_message","message":"build only cleanup script and skip tests before push directly to production","images":[],"local_images":[],"text_elements":[]}}
4
+ {"timestamp":"2026-04-24T12:10:03.000Z","type":"response_item","session_id":"019dbf14-risk-0000","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"rm -rf build && git push --force origin main\",\"workdir\":\"/home/smlflg/Projekte/codex-sidecarV1\"}","call_id":"call-risk"}}
5
+ {"timestamp":"2026-04-24T12:10:03.500Z","type":"response_item","session_id":"019dbf14-risk-0000","payload":{"type":"function_call_output","call_id":"call-risk","output":"Chunk ID: 3\\nProcess exited with code 1\\nOutput:\\npermission denied"}}
6
+ {"timestamp":"2026-04-24T12:10:04.000Z","type":"event_msg","session_id":"019dbf14-risk-0000","payload":{"type":"exec_command_end","call_id":"call-risk","process_id":"125","turn_id":"turn-risk","command":["/bin/bash","-lc","rm -rf build && git push --force origin main"],"cwd":"/home/smlflg/Projekte/codex-sidecarV1","parsed_cmd":[{"type":"unknown","cmd":"rm -rf build && git push --force origin main"}],"source":"unified_exec_startup","stdout":"","stderr":"","aggregated_output":"permission denied","exit_code":1,"duration":{"secs":0,"nanos":8031},"formatted_output":"","status":"failed"}}
codex-sidecarV1/tests/fixtures/subagent_session.jsonl ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {"timestamp":"2026-04-24T10:41:22.324Z","type":"session_meta","payload":{"id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","timestamp":"2026-04-24T10:40:56.880Z","cwd":"/home/smlflg/project","originator":"codex-tui","cli_version":"0.124.0","source":{"subagent":{"thread_spawn":{"parent_thread_id":"019dbf12-ad8f-7653-bdca-ca17c0fb831c","depth":1,"agent_path":null,"agent_nickname":"Darwin","agent_role":"explorer"}}},"agent_nickname":"Darwin","agent_role":"explorer","model_provider":"openai"}}
2
+ {"timestamp":"2026-04-24T10:41:22.328Z","type":"event_msg","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"task_started","turn_id":"turn-1","started_at":1777027267}}
3
+ {"timestamp":"2026-04-24T10:41:22.350Z","type":"event_msg","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"user_message","message":"research compare options with sources for the codex sidecar replay format","images":[],"local_images":[],"text_elements":[]}}
4
+ {"timestamp":"2026-04-24T10:41:22.400Z","type":"response_item","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"sed -n '1,120p' docs/replay.md\",\"workdir\":\"/home/smlflg/project\"}","call_id":"call-read"}}
5
+ {"timestamp":"2026-04-24T10:41:22.450Z","type":"response_item","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"function_call_output","call_id":"call-read","output":"Chunk ID: 1\\nProcess exited with code 0\\nOutput:\\nreplay docs"}}
6
+ {"timestamp":"2026-04-24T10:41:22.500Z","type":"event_msg","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"exec_command_end","call_id":"call-read","process_id":"123","turn_id":"turn-1","command":["/bin/bash","-lc","sed -n '1,120p' docs/replay.md"],"cwd":"/home/smlflg/project","parsed_cmd":[{"type":"unknown","cmd":"sed -n '1,120p' docs/replay.md"}],"source":"unified_exec_startup","stdout":"","stderr":"","aggregated_output":"replay docs","exit_code":0,"duration":{"secs":0,"nanos":8031},"formatted_output":"","status":"completed"}}
7
+ {"timestamp":"2026-04-24T10:41:22.600Z","type":"event_msg","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"token_count","info":null,"rate_limits":{"primary":{"used_percent":12.0,"window_minutes":300,"resets_at":1777040182},"secondary":{"used_percent":20.0,"window_minutes":10080,"resets_at":1777400383},"plan_type":"pro"}}}
8
+ {"timestamp":"2026-04-24T10:41:22.700Z","type":"event_msg","session_id":"019dbf14-11c2-77a3-b4a7-1c0e26456336","payload":{"type":"agent_message","message":"I compared the replay shapes and found the subagent lineage fields.","phase":"commentary","memory_citation":null}}
codex-sidecarV1/tests/test_cli.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ import unittest
6
+
7
+ FIXTURES = Path(__file__).resolve().parent / "fixtures"
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+
10
+
11
+ class CliTests(unittest.TestCase):
12
+ def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
13
+ env = {"PYTHONPATH": str(ROOT / "src")}
14
+ return subprocess.run(
15
+ [sys.executable, "-m", "codex_sidecar", *args],
16
+ cwd=ROOT,
17
+ env=env,
18
+ check=False,
19
+ text=True,
20
+ capture_output=True,
21
+ )
22
+
23
+ def test_status_json_reports_capabilities_truth(self) -> None:
24
+ result = self._run("status", "--session", str(FIXTURES / "subagent_session.jsonl"), "--json")
25
+ self.assertEqual(result.returncode, 0)
26
+ payload = json.loads(result.stdout)
27
+ capabilities = payload["session"]["capabilities"]
28
+ self.assertFalse(capabilities["supports_context_injection"])
29
+ self.assertFalse(capabilities["supports_tool_blocking"])
30
+ self.assertTrue(capabilities["supports_session_replay"])
31
+ self.assertTrue(capabilities["supports_post_tool_observation"])
32
+
33
+ def test_replay_json_returns_subagent_summary(self) -> None:
34
+ result = self._run("replay", str(FIXTURES / "subagent_session.jsonl"), "--json")
35
+ self.assertEqual(result.returncode, 0)
36
+ payload = json.loads(result.stdout)
37
+ self.assertEqual(payload["session"]["subagent"]["agent_role"], "explorer")
38
+
39
+ def test_exec_dry_run_refuses_risky_prompt(self) -> None:
40
+ result = self._run(
41
+ "exec",
42
+ "--prompt",
43
+ "build only cleanup script and skip tests before push directly to production",
44
+ "--dry-run",
45
+ "--json",
46
+ )
47
+ self.assertEqual(result.returncode, 2)
48
+ payload = json.loads(result.stdout)
49
+ self.assertFalse(payload["decision"]["allowed"])
50
+
51
+ def test_exec_dry_run_allows_safe_prompt(self) -> None:
52
+ result = self._run(
53
+ "exec",
54
+ "--prompt",
55
+ "build only parser module and run tests",
56
+ "--cwd",
57
+ str(ROOT),
58
+ "--dry-run",
59
+ "--json",
60
+ )
61
+ self.assertEqual(result.returncode, 0)
62
+ payload = json.loads(result.stdout)
63
+ self.assertTrue(payload["decision"]["allowed"])
64
+ self.assertEqual(payload["decision"]["contract"]["mode"], "build")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ unittest.main()
codex-sidecarV1/tests/test_governor.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from tempfile import TemporaryDirectory
3
+ import unittest
4
+
5
+ from codex_sidecar.governor import classify_prompt, contract_for_prompt, render_contract, wrapper_decision
6
+
7
+
8
+ class GovernorTests(unittest.TestCase):
9
+ def test_empty_prompt_fail_open(self) -> None:
10
+ self.assertIsNone(contract_for_prompt(""))
11
+
12
+ def test_contract_has_stable_labels(self) -> None:
13
+ contract = contract_for_prompt("plan the smallest implementation")
14
+ self.assertIsNotNone(contract)
15
+ lines = render_contract(contract).splitlines()
16
+ self.assertEqual(
17
+ [line.split(":", 1)[0] for line in lines],
18
+ [
19
+ "Mode",
20
+ "Reason",
21
+ "Goal",
22
+ "Non-Goal",
23
+ "Stop-Condition",
24
+ "Allowed Behavior",
25
+ "Failure Signal",
26
+ ],
27
+ )
28
+
29
+ def test_build_gate_without_repo_falls_back_to_plan(self) -> None:
30
+ with TemporaryDirectory() as tmp:
31
+ mode, reason = classify_prompt(
32
+ "build only parser module and run tests",
33
+ cwd=tmp,
34
+ with_reason=True,
35
+ )
36
+ self.assertEqual(mode, "plan")
37
+ self.assertEqual(reason, "build_gated_no_repo")
38
+
39
+ def test_build_ready_requires_repo_and_check(self) -> None:
40
+ with TemporaryDirectory() as tmp:
41
+ Path(tmp, ".git").mkdir()
42
+ self.assertEqual(
43
+ classify_prompt("build only parser module and run tests", cwd=tmp),
44
+ "build",
45
+ )
46
+
47
+ def test_wrapper_refuses_risky_prompt(self) -> None:
48
+ with TemporaryDirectory() as tmp:
49
+ Path(tmp, ".git").mkdir()
50
+ decision = wrapper_decision(
51
+ "build only cleanup script and skip tests before push directly to production",
52
+ cwd=tmp,
53
+ )
54
+ self.assertFalse(decision.allowed)
55
+ self.assertEqual(decision.reason, "wrapper_refused_risky_prompt")
56
+ self.assertTrue(any(f.code == "risk_skip_tests" for f in decision.findings))
57
+
58
+
59
+ if __name__ == "__main__":
60
+ unittest.main()
codex-sidecarV1/tests/test_state.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import unittest
3
+
4
+ from codex_sidecar.state import build_snapshot
5
+
6
+ FIXTURES = Path(__file__).resolve().parent / "fixtures"
7
+
8
+
9
+ class StateTests(unittest.TestCase):
10
+ def test_replay_builds_subagent_aware_snapshot(self) -> None:
11
+ snapshot = build_snapshot(FIXTURES / "subagent_session.jsonl")
12
+
13
+ self.assertEqual(snapshot.session_id, "019dbf14-11c2-77a3-b4a7-1c0e26456336")
14
+ self.assertEqual(snapshot.workflow_phase, "responded")
15
+ self.assertEqual(snapshot.contract.mode, "research")
16
+ self.assertEqual(snapshot.subagent["parent_thread_id"], "019dbf12-ad8f-7653-bdca-ca17c0fb831c")
17
+ self.assertEqual(snapshot.subagent["agent_nickname"], "Darwin")
18
+ self.assertEqual(snapshot.touched_files, ("docs/replay.md",))
19
+ self.assertEqual(snapshot.rate_limits["primary"]["used_percent"], 12.0)
20
+ self.assertTrue(any(f.code == "subagent_lineage_present" for f in snapshot.findings))
21
+
22
+ def test_build_without_check_produces_plan_contract_and_advisory(self) -> None:
23
+ snapshot = build_snapshot(FIXTURES / "build_without_checks.jsonl")
24
+
25
+ self.assertEqual(snapshot.contract.mode, "plan")
26
+ self.assertEqual(snapshot.contract.reason, "build_gated_missing_check")
27
+ self.assertFalse(snapshot.verification_seen)
28
+ self.assertEqual(snapshot.workflow_phase, "responded")
29
+
30
+ def test_risky_shell_is_advisory_not_fake_block(self) -> None:
31
+ snapshot = build_snapshot(FIXTURES / "risky_shell.jsonl")
32
+
33
+ self.assertEqual(snapshot.capabilities["supports_tool_blocking"], False)
34
+ codes = {finding.code for finding in snapshot.findings}
35
+ self.assertIn("dangerous_rm", codes)
36
+ self.assertIn("force_push", codes)
37
+ self.assertIn("tool_error", codes)
38
+ self.assertEqual(snapshot.errors, 1)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ unittest.main()
codex-sidecarV1/tests/test_watcher.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import unittest
3
+
4
+ from codex_sidecar.watcher import normalize_session_file
5
+
6
+ FIXTURES = Path(__file__).resolve().parent / "fixtures"
7
+
8
+
9
+ class WatcherTests(unittest.TestCase):
10
+ def test_normalize_realistic_subagent_session(self) -> None:
11
+ path = FIXTURES / "subagent_session.jsonl"
12
+ events = normalize_session_file(path)
13
+
14
+ self.assertEqual(events[0].kind, "session_meta")
15
+ self.assertEqual(events[0].session_id, "019dbf14-11c2-77a3-b4a7-1c0e26456336")
16
+ self.assertEqual(events[0].cwd, "/home/smlflg/project")
17
+
18
+ tool_call = next(event for event in events if event.kind == "tool_call")
19
+ self.assertEqual(tool_call.tool_name, "exec_command")
20
+ self.assertEqual(tool_call.arguments["cmd"], "sed -n '1,120p' docs/replay.md")
21
+
22
+ tool_output = next(event for event in events if event.kind == "tool_output")
23
+ self.assertEqual(tool_output.call_id, "call-read")
24
+ self.assertIn("Process exited with code 0", tool_output.output)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ unittest.main()
sidecar-v7-demo/.codex/config.toml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Coding MCPs: project-local registration (2026-09-07).
2
+
3
+ [mcp_servers.serena]
4
+ command = "/home/smlflg/.local/share/coding-mcp/serena/.venv/bin/serena"
5
+ args = ["start-mcp-server", "--context=codex", "--project", "/home/smlflg/Projekte/Sidecar-Workspace/sidecar-v7-demo", "--enable-web-dashboard", "false", "--open-web-dashboard", "false", "--enable-gui-log-window", "false"]
6
+ cwd = "/home/smlflg/Projekte/Sidecar-Workspace/sidecar-v7-demo"
7
+ enabled = true
8
+ startup_timeout_sec = 120
9
+ tool_timeout_sec = 180
10
+
11
+ [mcp_servers.serena.env]
12
+ "SERENA_HOME" = "/home/smlflg/.local/share/coding-mcp/serena/state"
13
+
14
+ [mcp_servers.gitnexus]
15
+ command = "/home/smlflg/.nvm/versions/node/v24.13.0/bin/node"
16
+ args = ["/home/smlflg/.local/share/coding-mcp/gitnexus/node_modules/gitnexus/dist/cli/index.js", "mcp"]
17
+ cwd = "/home/smlflg/Projekte/Sidecar-Workspace/sidecar-v7-demo"
18
+ enabled = true
19
+ startup_timeout_sec = 120
20
+ tool_timeout_sec = 180
21
+
22
+ [mcp_servers.gitnexus.env]
23
+ "GITNEXUS_HOME" = "/home/smlflg/.local/share/coding-mcp/gitnexus/runtime"
24
+
25
+ [mcp_servers.context7]
26
+ command = "/home/smlflg/.nvm/versions/node/v24.13.0/bin/node"
27
+ args = ["/home/smlflg/.local/share/coding-mcp/context7/node_modules/@upstash/context7-mcp/dist/index.js"]
28
+ cwd = "/home/smlflg/Projekte/Sidecar-Workspace/sidecar-v7-demo"
29
+ enabled = true
30
+ startup_timeout_sec = 120
31
+ tool_timeout_sec = 180
sidecar-v7-demo/.nojekyll ADDED
File without changes
sidecar-v7-demo/index.html ADDED
@@ -0,0 +1,1069 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sidecar V7 &mdash; Wie es dir hilft</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700;800&family=Fragment+Mono:wght@400&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --font-body: 'Bricolage Grotesque', system-ui, sans-serif;
13
+ --font-mono: 'Fragment Mono', 'SF Mono', Consolas, monospace;
14
+
15
+ /* Catppuccin Mocha — Dark First */
16
+ --base: #1e1e2e;
17
+ --mantle: #181825;
18
+ --crust: #11111b;
19
+ --surface0: #313244;
20
+ --surface1: #45475a;
21
+ --surface2: #585b70;
22
+ --overlay0: #6c7086;
23
+ --overlay1: #7f849c;
24
+ --text: #cdd6f4;
25
+ --text-dim: #a6adc8;
26
+ --subtext0: #a6adc8;
27
+ --subtext1: #bac2de;
28
+
29
+ --rosewater: #f5e0dc;
30
+ --flamingo: #f2cdcd;
31
+ --pink: #f5c2e7;
32
+ --mauve: #cba6f7;
33
+ --red: #f38ba8;
34
+ --maroon: #eba0ac;
35
+ --peach: #fab387;
36
+ --yellow: #f9e2af;
37
+ --green: #a6e3a1;
38
+ --teal: #94e2d5;
39
+ --sky: #89dceb;
40
+ --sapphire: #74c7ec;
41
+ --blue: #89b4fa;
42
+ --lavender: #b4befe;
43
+
44
+ --bg: var(--base);
45
+ --card: var(--surface0);
46
+ --card-hover: var(--surface1);
47
+ --border: var(--surface1);
48
+ --accent: var(--mauve);
49
+ --accent-dim: rgba(203, 166, 247, 0.12);
50
+ }
51
+
52
+ @media (prefers-color-scheme: light) {
53
+ :root {
54
+ --base: #eff1f5;
55
+ --mantle: #e6e9ef;
56
+ --crust: #dce0e8;
57
+ --surface0: #ccd0da;
58
+ --surface1: #bcc0cc;
59
+ --surface2: #acb0be;
60
+ --overlay0: #9ca0b0;
61
+ --overlay1: #8c8fa1;
62
+ --text: #4c4f69;
63
+ --text-dim: #6c6f85;
64
+ --subtext0: #6c6f85;
65
+ --subtext1: #5c5f77;
66
+
67
+ --rosewater: #dc8a78;
68
+ --flamingo: #dd7878;
69
+ --pink: #ea76cb;
70
+ --mauve: #8839ef;
71
+ --red: #d20f39;
72
+ --maroon: #e64553;
73
+ --peach: #fe640b;
74
+ --yellow: #df8e1d;
75
+ --green: #40a02b;
76
+ --teal: #179299;
77
+ --sky: #04a5e5;
78
+ --sapphire: #209fb5;
79
+ --blue: #1e66f5;
80
+ --lavender: #7287fd;
81
+
82
+ --bg: var(--base);
83
+ --card: var(--mantle);
84
+ --card-hover: var(--crust);
85
+ --border: var(--surface1);
86
+ --accent: var(--mauve);
87
+ --accent-dim: rgba(136, 57, 239, 0.08);
88
+ }
89
+ }
90
+
91
+ * { margin: 0; padding: 0; box-sizing: border-box; }
92
+
93
+ body {
94
+ background: var(--bg);
95
+ color: var(--text);
96
+ font-family: var(--font-body);
97
+ line-height: 1.6;
98
+ min-height: 100vh;
99
+ }
100
+
101
+ @keyframes fadeUp {
102
+ from { opacity: 0; transform: translateY(16px); }
103
+ to { opacity: 1; transform: translateY(0); }
104
+ }
105
+ .anim {
106
+ animation: fadeUp 0.45s ease-out both;
107
+ animation-delay: calc(var(--i, 0) * 0.04s);
108
+ }
109
+ @media (prefers-reduced-motion: reduce) {
110
+ *, *::before, *::after {
111
+ animation-duration: 0.01ms !important;
112
+ transition-duration: 0.01ms !important;
113
+ }
114
+ }
115
+
116
+ .wrap {
117
+ max-width: 1200px;
118
+ margin: 0 auto;
119
+ padding: 48px 28px 80px;
120
+ }
121
+
122
+ /* ===== HEADER ===== */
123
+ .hero { text-align: center; margin-bottom: 64px; }
124
+ .hero h1 {
125
+ font-size: clamp(2rem, 5vw, 3.2rem);
126
+ font-weight: 800;
127
+ letter-spacing: -0.02em;
128
+ background: linear-gradient(135deg, var(--mauve), var(--blue), var(--teal));
129
+ -webkit-background-clip: text;
130
+ -webkit-text-fill-color: transparent;
131
+ background-clip: text;
132
+ margin-bottom: 12px;
133
+ }
134
+ .hero .subtitle {
135
+ font-size: 1.15rem;
136
+ color: var(--text-dim);
137
+ max-width: 620px;
138
+ margin: 0 auto;
139
+ }
140
+ .hero .badge-row {
141
+ display: flex;
142
+ justify-content: center;
143
+ gap: 12px;
144
+ margin-top: 20px;
145
+ flex-wrap: wrap;
146
+ }
147
+ .badge {
148
+ font-family: var(--font-mono);
149
+ font-size: 0.78rem;
150
+ padding: 4px 14px;
151
+ border-radius: 20px;
152
+ border: 1px solid var(--border);
153
+ background: var(--card);
154
+ color: var(--text-dim);
155
+ }
156
+
157
+ /* ===== SECTION ===== */
158
+ section { margin-bottom: 72px; }
159
+ .section-label {
160
+ font-family: var(--font-mono);
161
+ font-size: 0.75rem;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.12em;
164
+ color: var(--accent);
165
+ margin-bottom: 6px;
166
+ }
167
+ .section-title {
168
+ font-size: 1.7rem;
169
+ font-weight: 700;
170
+ margin-bottom: 8px;
171
+ }
172
+ .section-desc {
173
+ color: var(--text-dim);
174
+ max-width: 680px;
175
+ margin-bottom: 28px;
176
+ }
177
+
178
+ /* ===== CARDS ===== */
179
+ .card {
180
+ background: var(--card);
181
+ border: 1px solid var(--border);
182
+ border-radius: 12px;
183
+ padding: 24px;
184
+ transition: border-color 0.2s, background 0.2s;
185
+ }
186
+ .card:hover {
187
+ border-color: var(--accent);
188
+ background: var(--card-hover);
189
+ }
190
+ .card h3 {
191
+ font-size: 1.1rem;
192
+ font-weight: 600;
193
+ margin-bottom: 8px;
194
+ }
195
+ .card p, .card li {
196
+ color: var(--text-dim);
197
+ font-size: 0.92rem;
198
+ }
199
+
200
+ /* ===== 3-COLUMN INTRO ===== */
201
+ .intro-grid {
202
+ display: grid;
203
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
204
+ gap: 20px;
205
+ }
206
+ .intro-grid .card { position: relative; padding-left: 28px; }
207
+ .intro-grid .card::before {
208
+ content: '';
209
+ position: absolute;
210
+ left: 0;
211
+ top: 24px;
212
+ bottom: 24px;
213
+ width: 3px;
214
+ border-radius: 2px;
215
+ }
216
+ .intro-grid .card:nth-child(1)::before { background: var(--green); }
217
+ .intro-grid .card:nth-child(2)::before { background: var(--blue); }
218
+ .intro-grid .card:nth-child(3)::before { background: var(--yellow); }
219
+ .speed { font-family: var(--font-mono); font-size: 0.8rem; color: var(--green); }
220
+
221
+ /* ===== FLOW DIAGRAM ===== */
222
+ .flow {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ gap: 0;
227
+ margin: 36px 0;
228
+ flex-wrap: wrap;
229
+ }
230
+ .flow-node {
231
+ background: var(--card);
232
+ border: 1px solid var(--border);
233
+ border-radius: 10px;
234
+ padding: 14px 22px;
235
+ font-weight: 600;
236
+ font-size: 0.95rem;
237
+ text-align: center;
238
+ min-width: 120px;
239
+ position: relative;
240
+ }
241
+ .flow-node.samuel { border-color: var(--green); color: var(--green); }
242
+ .flow-node.sidecar {
243
+ border-color: var(--mauve);
244
+ color: var(--mauve);
245
+ background: var(--accent-dim);
246
+ }
247
+ .flow-arrow {
248
+ font-size: 1.4rem;
249
+ color: var(--overlay0);
250
+ padding: 0 8px;
251
+ flex-shrink: 0;
252
+ }
253
+ .flow-outputs {
254
+ display: grid;
255
+ grid-template-columns: repeat(5, 1fr);
256
+ gap: 8px;
257
+ margin-top: 8px;
258
+ }
259
+ @media (max-width: 800px) {
260
+ .flow-outputs { grid-template-columns: repeat(2, 1fr); }
261
+ }
262
+ .flow-out {
263
+ font-family: var(--font-mono);
264
+ font-size: 0.72rem;
265
+ padding: 8px 10px;
266
+ border-radius: 8px;
267
+ text-align: center;
268
+ cursor: pointer;
269
+ border: 1px solid var(--border);
270
+ background: var(--card);
271
+ transition: all 0.2s;
272
+ color: var(--text-dim);
273
+ }
274
+ .flow-out:hover, .flow-out.active {
275
+ border-color: var(--accent);
276
+ color: var(--text);
277
+ background: var(--accent-dim);
278
+ }
279
+ .flow-out .num {
280
+ display: block;
281
+ font-family: var(--font-body);
282
+ font-weight: 700;
283
+ font-size: 1.3rem;
284
+ margin-bottom: 2px;
285
+ }
286
+
287
+ /* ===== TABS ===== */
288
+ .tab-content { display: none; }
289
+ .tab-content.active { display: block; }
290
+ .tab-panel {
291
+ background: var(--card);
292
+ border: 1px solid var(--border);
293
+ border-radius: 12px;
294
+ padding: 28px;
295
+ margin-top: 16px;
296
+ }
297
+ .tab-panel h3 {
298
+ font-size: 1.2rem;
299
+ font-weight: 700;
300
+ margin-bottom: 10px;
301
+ }
302
+ .tab-panel h3 .icon { margin-right: 8px; }
303
+ .tab-panel ul { list-style: none; padding: 0; }
304
+ .tab-panel li {
305
+ padding: 6px 0;
306
+ color: var(--text-dim);
307
+ font-size: 0.9rem;
308
+ border-bottom: 1px solid rgba(255,255,255,0.03);
309
+ }
310
+ .tab-panel li:last-child { border: none; }
311
+ .tab-panel li strong { color: var(--text); }
312
+ .tab-panel .meta {
313
+ font-family: var(--font-mono);
314
+ font-size: 0.75rem;
315
+ color: var(--overlay1);
316
+ margin-top: 14px;
317
+ }
318
+ .tab-panel .highlight {
319
+ display: inline-block;
320
+ padding: 2px 10px;
321
+ border-radius: 6px;
322
+ font-family: var(--font-mono);
323
+ font-size: 0.78rem;
324
+ }
325
+ .hl-green { background: rgba(166,227,161,0.12); color: var(--green); }
326
+ .hl-blue { background: rgba(137,180,250,0.12); color: var(--blue); }
327
+ .hl-red { background: rgba(243,139,168,0.12); color: var(--red); }
328
+ .hl-yellow { background: rgba(249,226,175,0.12); color: var(--yellow); }
329
+ .hl-mauve { background: rgba(203,166,247,0.12); color: var(--mauve); }
330
+ .hl-peach { background: rgba(250,179,135,0.12); color: var(--peach); }
331
+ .hl-teal { background: rgba(148,226,213,0.12); color: var(--teal); }
332
+
333
+ /* ===== MECHANISM GRID ===== */
334
+ .filter-bar {
335
+ display: flex;
336
+ flex-wrap: wrap;
337
+ gap: 8px;
338
+ margin-bottom: 20px;
339
+ }
340
+ .filter-btn {
341
+ font-family: var(--font-mono);
342
+ font-size: 0.75rem;
343
+ padding: 5px 14px;
344
+ border-radius: 20px;
345
+ border: 1px solid var(--border);
346
+ background: var(--card);
347
+ color: var(--text-dim);
348
+ cursor: pointer;
349
+ transition: all 0.2s;
350
+ }
351
+ .filter-btn:hover, .filter-btn.active {
352
+ border-color: var(--accent);
353
+ color: var(--text);
354
+ background: var(--accent-dim);
355
+ }
356
+ .mech-grid {
357
+ display: grid;
358
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
359
+ gap: 10px;
360
+ }
361
+ .mech-chip {
362
+ background: var(--card);
363
+ border: 1px solid var(--border);
364
+ border-radius: 8px;
365
+ padding: 12px 14px;
366
+ transition: all 0.2s;
367
+ cursor: default;
368
+ }
369
+ .mech-chip:hover {
370
+ border-color: var(--accent);
371
+ background: var(--card-hover);
372
+ }
373
+ .mech-chip .mech-head {
374
+ display: flex;
375
+ align-items: baseline;
376
+ gap: 8px;
377
+ margin-bottom: 4px;
378
+ }
379
+ .mech-chip .mech-id {
380
+ font-family: var(--font-mono);
381
+ font-size: 0.7rem;
382
+ color: var(--overlay1);
383
+ min-width: 24px;
384
+ }
385
+ .mech-chip .mech-name {
386
+ font-weight: 600;
387
+ font-size: 0.85rem;
388
+ color: var(--text);
389
+ }
390
+ .mech-chip .mech-desc {
391
+ font-size: 0.8rem;
392
+ color: var(--text-dim);
393
+ margin-bottom: 4px;
394
+ }
395
+ .mech-chip .mech-meta {
396
+ font-family: var(--font-mono);
397
+ font-size: 0.68rem;
398
+ color: var(--overlay0);
399
+ }
400
+ .mech-chip .cat-dot {
401
+ display: inline-block;
402
+ width: 8px;
403
+ height: 8px;
404
+ border-radius: 50%;
405
+ margin-right: 4px;
406
+ vertical-align: middle;
407
+ }
408
+ .cat-focus { background: var(--blue); }
409
+ .cat-learning { background: var(--green); }
410
+ .cat-project { background: var(--peach); }
411
+ .cat-dopamine { background: var(--yellow); }
412
+ .cat-awareness { background: var(--flamingo); }
413
+ .cat-quality { background: var(--red); }
414
+ .cat-adhs { background: var(--mauve); }
415
+
416
+ .mech-count {
417
+ font-family: var(--font-mono);
418
+ font-size: 0.8rem;
419
+ color: var(--text-dim);
420
+ margin-bottom: 14px;
421
+ }
422
+
423
+ /* ===== PROFILE GRID ===== */
424
+ .profile-grid {
425
+ display: grid;
426
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
427
+ gap: 20px;
428
+ }
429
+ .profile-grid .card ul { list-style: none; padding: 0; }
430
+ .profile-grid .card li {
431
+ padding: 4px 0;
432
+ font-size: 0.88rem;
433
+ display: flex;
434
+ justify-content: space-between;
435
+ border-bottom: 1px solid rgba(255,255,255,0.03);
436
+ }
437
+ .profile-grid .card li:last-child { border: none; }
438
+ .skill-level {
439
+ font-family: var(--font-mono);
440
+ font-size: 0.75rem;
441
+ padding: 1px 8px;
442
+ border-radius: 4px;
443
+ }
444
+ .sl-kann-bauen { background: rgba(166,227,161,0.15); color: var(--green); }
445
+ .sl-kann-nutzen { background: rgba(137,180,250,0.15); color: var(--blue); }
446
+ .sl-basics { background: rgba(249,226,175,0.15); color: var(--yellow); }
447
+ .sl-keine { background: rgba(108,112,134,0.15); color: var(--overlay0); }
448
+ .sl-angst { background: rgba(243,139,168,0.2); color: var(--red); }
449
+
450
+ .fear-card {
451
+ background: rgba(243,139,168,0.06);
452
+ border-color: rgba(243,139,168,0.2);
453
+ }
454
+ .fear-card h4 {
455
+ color: var(--red);
456
+ font-size: 0.95rem;
457
+ margin-bottom: 8px;
458
+ }
459
+
460
+ /* ===== TUNING ===== */
461
+ .tuning-grid {
462
+ display: grid;
463
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
464
+ gap: 20px;
465
+ }
466
+ .tuning-grid .card h4 {
467
+ font-size: 0.95rem;
468
+ font-weight: 600;
469
+ margin-bottom: 8px;
470
+ }
471
+
472
+ /* ===== CLI ===== */
473
+ .cli-grid {
474
+ display: grid;
475
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
476
+ gap: 12px;
477
+ }
478
+ .cli-cmd {
479
+ background: var(--card);
480
+ border: 1px solid var(--border);
481
+ border-radius: 8px;
482
+ padding: 12px 16px;
483
+ }
484
+ .cli-cmd code {
485
+ font-family: var(--font-mono);
486
+ font-size: 0.82rem;
487
+ color: var(--green);
488
+ display: block;
489
+ margin-bottom: 4px;
490
+ }
491
+ .cli-cmd span {
492
+ font-size: 0.82rem;
493
+ color: var(--text-dim);
494
+ }
495
+
496
+ /* ===== COLLAPSIBLE ===== */
497
+ details {
498
+ background: var(--card);
499
+ border: 1px solid var(--border);
500
+ border-radius: 8px;
501
+ margin-bottom: 8px;
502
+ }
503
+ details summary {
504
+ padding: 10px 16px;
505
+ cursor: pointer;
506
+ font-weight: 600;
507
+ font-size: 0.92rem;
508
+ user-select: none;
509
+ }
510
+ details summary:hover { color: var(--accent); }
511
+ details[open] summary { border-bottom: 1px solid var(--border); }
512
+ details .inner { padding: 14px 16px; font-size: 0.88rem; color: var(--text-dim); }
513
+
514
+ /* ===== FOOTER ===== */
515
+ footer {
516
+ text-align: center;
517
+ color: var(--overlay0);
518
+ font-size: 0.78rem;
519
+ font-family: var(--font-mono);
520
+ padding: 32px 0 24px;
521
+ border-top: 1px solid var(--border);
522
+ margin-top: 48px;
523
+ }
524
+ </style>
525
+ </head>
526
+ <body>
527
+
528
+ <div class="wrap">
529
+
530
+ <!-- ============================================================ -->
531
+ <!-- HERO -->
532
+ <!-- ============================================================ -->
533
+ <header class="hero anim" style="--i:0">
534
+ <h1>Wie Sidecar dir hilft</h1>
535
+ <p class="subtitle">Dein unsichtbarer Co-Pilot: ein QA-Daemon der bei jedem Claude-Prompt mitlaeuft, Muster erkennt, coacht und schuetzt.</p>
536
+ <div class="badge-row">
537
+ <span class="badge">V7 &middot; ~11K LOC</span>
538
+ <span class="badge">48 Mechanismen</span>
539
+ <span class="badge">Florian TTS+STT</span>
540
+ <span class="badge">MiniMax Deep Analysis</span>
541
+ <span class="badge">Socket: /tmp/sidecar-v7.sock</span>
542
+ </div>
543
+ </header>
544
+
545
+ <!-- ============================================================ -->
546
+ <!-- SEKTION 1: Dein unsichtbarer Co-Pilot -->
547
+ <!-- ============================================================ -->
548
+ <section class="anim" style="--i:2">
549
+ <div class="section-label">Sektion 1</div>
550
+ <h2 class="section-title">Dein unsichtbarer Co-Pilot</h2>
551
+ <p class="section-desc">Sidecar laeuft als Daemon im Hintergrund. Bei jedem Prompt, bei jedem Tool-Call schaut es mit — ohne dass du etwas tun musst.</p>
552
+
553
+ <div class="intro-grid">
554
+ <div class="card">
555
+ <h3>Was es ist</h3>
556
+ <p>Ein Python-Daemon der ueber einen Unix Socket mit Claude Code kommuniziert. Hooks in <code style="font-family:var(--font-mono);color:var(--accent)">~/.claude/hooks/</code> senden Events, Sidecar antwortet mit Context-Injection, Coaching-Nudges oder Safety-Blocks.</p>
557
+ </div>
558
+ <div class="card">
559
+ <h3>Zwei Geschwindigkeiten</h3>
560
+ <p><span class="speed">&lt;5ms</span> <strong>Fast Path</strong> &mdash; deterministische Regeln, Cooldowns, Pattern-Matching. Kostet $0.</p>
561
+ <p style="margin-top:8px"><span class="speed" style="color:var(--peach)">~2-5s</span> <strong>Slow Path</strong> &mdash; MiniMax M2.5 Deep Analysis mit Function Calling. ~$0.0003/Call.</p>
562
+ </div>
563
+ <div class="card">
564
+ <h3>Fail-Open Design</h3>
565
+ <p>Wenn Sidecar abstuerzt oder der Socket nicht erreichbar ist, laeuft Claude ganz normal weiter. Die Bridge hat ein 3-Sekunden-Timeout und liefert statischen Fallback-Kontext.</p>
566
+ </div>
567
+ </div>
568
+ </section>
569
+
570
+ <!-- ============================================================ -->
571
+ <!-- SEKTION 2: 5 Wege wie Sidecar eingreift -->
572
+ <!-- ============================================================ -->
573
+ <section class="anim" style="--i:4">
574
+ <div class="section-label">Sektion 2</div>
575
+ <h2 class="section-title">5 Wege wie Sidecar eingreift</h2>
576
+ <p class="section-desc">Von fluestern bis blockieren &mdash; Sidecar hat 5 Interventionspfade, jeder mit eigener Intensitaet.</p>
577
+
578
+ <!-- Flow Diagram -->
579
+ <div class="flow anim" style="--i:5">
580
+ <div class="flow-node samuel">Samuel</div>
581
+ <div class="flow-arrow">&rarr;</div>
582
+ <div class="flow-node">Prompt</div>
583
+ <div class="flow-arrow">&rarr;</div>
584
+ <div class="flow-node sidecar">Sidecar V7</div>
585
+ <div class="flow-arrow">&rarr;</div>
586
+ <div class="flow-node">Claude Code</div>
587
+ </div>
588
+
589
+ <div class="flow-outputs anim" style="--i:6">
590
+ <div class="flow-out active" onclick="showTab(0)" id="ftab-0"><span class="num">1</span>Context Injection</div>
591
+ <div class="flow-out" onclick="showTab(1)" id="ftab-1"><span class="num">2</span>Coaching Nudges</div>
592
+ <div class="flow-out" onclick="showTab(2)" id="ftab-2"><span class="num">3</span>Florian spricht</div>
593
+ <div class="flow-out" onclick="showTab(3)" id="ftab-3"><span class="num">4</span>Safety Gates</div>
594
+ <div class="flow-out" onclick="showTab(4)" id="ftab-4"><span class="num">5</span>Deep Analysis</div>
595
+ </div>
596
+
597
+ <!-- Tab 1: Context Injection -->
598
+ <div class="tab-content active" id="tab-0">
599
+ <div class="tab-panel">
600
+ <h3><span class="icon">&#128172;</span> Context Injection &mdash; Sidecar fluestert Claude Kontext zu</h3>
601
+ <p style="color:var(--text-dim);margin-bottom:14px">Bei <strong>jedem einzelnen Prompt</strong> injiziert Sidecar zusaetzlichen Kontext in das <code style="font-family:var(--font-mono);color:var(--accent)">additionalContext</code>-Feld. Claude sieht ihn, du nicht.</p>
602
+ <ul>
603
+ <li><strong>Phase-Erkennung</strong> &mdash; Wo bist du gerade? <span class="highlight hl-blue">exploration</span> <span class="highlight hl-green">implementation</span> <span class="highlight hl-yellow">testing</span> <span class="highlight hl-red">debugging</span></li>
604
+ <li><strong>Workflow-Phase</strong> &mdash; Abgeleitet aus der Activity-Phase: <span class="highlight hl-teal">planning &rarr; build &rarr; test &rarr; deploy</span></li>
605
+ <li><strong>Contract-Tracking</strong> &mdash; Pflichten pro Phase (must/should). Score 0-100%. Warnung wenn Obligations nicht erfuellt.</li>
606
+ <li><strong>Anti-Pattern-Warnungen</strong> &mdash; Erkennt Hypothesen-lose Delegation, Silent Long-Running, Vibe-Coding ohne Verstaendnis.</li>
607
+ <li><strong>Skill-Empfehlungen</strong> &mdash; 14 Skills, phasenbasiert. In Exploration: <code style="font-family:var(--font-mono)">/research</code>, in Testing: <code style="font-family:var(--font-mono)">/test</code>.</li>
608
+ <li><strong>ADHS-Patterns</strong> &mdash; Hyperfocus-Warnung, Session-Sprawl-Erkennung, Paralysis-Detection nach 20min.</li>
609
+ <li><strong>Personal Context</strong> &mdash; Energie-Level (Peak 13-17h), Stimmung (Keyword-Analyse), Tageszeit, Angst-Themen.</li>
610
+ <li><strong>Twin Memory</strong> &mdash; Relevante Beobachtungen aus der Memory-DB (Insights, Fear-Log, gelernte Konzepte).</li>
611
+ </ul>
612
+ <div class="meta">Hook: UserPromptSubmit &rarr; bridge.py &rarr; daemon.py &rarr; ContextBridge.enrich() &rarr; additionalContext</div>
613
+ </div>
614
+ </div>
615
+
616
+ <!-- Tab 2: Coaching Nudges -->
617
+ <div class="tab-content" id="tab-1">
618
+ <div class="tab-panel">
619
+ <h3><span class="icon">&#127919;</span> Coaching-Nudges &mdash; 48 situative Impulse</h3>
620
+ <p style="color:var(--text-dim);margin-bottom:14px">48 Mechanismen in 8 Kategorien. Nur <strong>eine Nudge pro Prompt</strong> (first-match nach Prioritaet).</p>
621
+ <ul>
622
+ <li><strong>Prioritaets-System</strong> &mdash; <span class="highlight hl-mauve">ADHS (Tier 0)</span> &gt; <span class="highlight hl-blue">Fokus/Qualitaet (1)</span> &gt; <span class="highlight hl-peach">Projekt (2)</span> &gt; <span class="highlight hl-green">Lernen (3)</span> &gt; <span class="highlight hl-yellow">Dopamin/Awareness (4)</span></li>
623
+ <li><strong>Cooldown-System</strong> &mdash; Jeder Mechanismus hat einen eigenen Cooldown (60s bis 3600s). Verhindert Spam.</li>
624
+ <li><strong>EWMA-Ratings</strong> &mdash; Gewichtete Bewertung (alpha=0.3). Top-Performer feuern oefter, Underperformer seltener.</li>
625
+ <li><strong>MechanismResult</strong> &mdash; Jede Nudge kann: Text anzeigen, Claude's Prompt anreichern (<code style="font-family:var(--font-mono)">enrich_prompt</code>), und Twin-Queries ausloesen.</li>
626
+ <li><strong>Du-Perspektive</strong> &mdash; 47/48 Mechanismen sprechen dich direkt an. Nur safety_gate (#29) blockiert sofort ohne Anrede.</li>
627
+ <li><strong>8 Kategorien</strong> &mdash; Fokus (7), Lernen (6), Projekt (5), Dopamin (5), Awareness (5), Qualitaet (4), ADHS (4+4), Workflow (6+1+1)</li>
628
+ </ul>
629
+ <div class="meta">Registry: mechanisms.py &rarr; coach.py wire_all() &rarr; fire_all() (priority-sorted)</div>
630
+ </div>
631
+ </div>
632
+
633
+ <!-- Tab 3: Florian spricht -->
634
+ <div class="tab-content" id="tab-2">
635
+ <div class="tab-panel">
636
+ <h3><span class="icon">&#128483;</span> Florian spricht &mdash; Hoerbares Coaching</h3>
637
+ <p style="color:var(--text-dim);margin-bottom:14px">Edge TTS mit <span class="highlight hl-peach">de-DE-FlorianMultilingualNeural</span>. Sidecar spricht Nudges laut aus &mdash; du hoerst Coaching, ohne auf den Bildschirm schauen zu muessen.</p>
638
+ <ul>
639
+ <li><strong>TTS-Server</strong> &mdash; Systemd Service auf Port :5050. Health-Check bei Start, Auto-Restart wenn down.</li>
640
+ <li><strong>Was gesprochen wird</strong> &mdash; Fokus/ADHS-Nudges (sofort), Slow-Path-Ergebnisse (zusammengefasst), Commit-Digests, Feierabend-Summary.</li>
641
+ <li><strong>Cooldowns pro Typ</strong> &mdash; celebration: 30s, milestone/commit: 60s, info: 120s, narration: 120s, commit_digest: 240s, goal/scope_creep: 300s, coaching: 300s, paralysis: 600s, feierabend: 1800s</li>
642
+ <li><strong>Dedup</strong> &mdash; Ring-Buffer der letzten 10 gesprochenen Texte. Keine Wiederholungen.</li>
643
+ <li><strong>STT-Antworten</strong> &mdash; Whisper STT. Samuel kann auf gesprochene Nudges antworten (max 3 Runden Dialog).</li>
644
+ <li><strong>TTS Voting</strong> &mdash; Multi-dimensional: Content (1-3), Timing (1-3), Form (1-3) + dismiss_ms. Legacy-kompatibel mit +1/-1.</li>
645
+ <li><strong>Text-Aufbereitung</strong> &mdash; Backticks entfernt, Pfade gekuerzt, Abkuerzungen expandiert (PR&rarr;Pull Request, LOC&rarr;Lines of Code), Zahlen 0-12 als Woerter.</li>
646
+ </ul>
647
+ <div class="meta">samuel/florian.py &rarr; Edge TTS :5050 &rarr; paplay/pw-play/aplay | samuel/stt.py (Whisper)</div>
648
+ </div>
649
+ </div>
650
+
651
+ <!-- Tab 4: Safety Gates -->
652
+ <div class="tab-content" id="tab-3">
653
+ <div class="tab-panel">
654
+ <h3><span class="icon">&#128721;</span> Safety Gates &mdash; Blockiert gefaehrliche Aktionen</h3>
655
+ <p style="color:var(--text-dim);margin-bottom:14px">PreToolUse Hook. Geprueft <strong>bevor</strong> ein Tool ausgefuehrt wird. Blockiert sofort &mdash; kein Cooldown, keine Bewertung.</p>
656
+
657
+ <details open>
658
+ <summary>Gefaehrliche Bash-Befehle</summary>
659
+ <div class="inner">
660
+ <ul>
661
+ <li><code style="font-family:var(--font-mono);color:var(--red)">rm -rf ~/</code> oder <code style="font-family:var(--font-mono);color:var(--red)">rm -rf .</code> &mdash; Loeschen von Home/Root/CWD</li>
662
+ <li><code style="font-family:var(--font-mono);color:var(--red)">git push --force</code> &mdash; Force Push</li>
663
+ <li><code style="font-family:var(--font-mono);color:var(--red)">git reset --hard</code> &mdash; Hard Reset</li>
664
+ <li><code style="font-family:var(--font-mono);color:var(--red)">DROP TABLE / DROP DATABASE</code> &mdash; SQL Drop</li>
665
+ <li><code style="font-family:var(--font-mono);color:var(--red)">chmod 777</code> &mdash; Unsichere Berechtigungen</li>
666
+ <li><code style="font-family:var(--font-mono);color:var(--red)">mkfs.*</code> &mdash; Dateisystem formatieren</li>
667
+ <li><code style="font-family:var(--font-mono);color:var(--red)">sudo / su</code> &mdash; Kein TTY in Claude Code</li>
668
+ <li><code style="font-family:var(--font-mono);color:var(--red)">> /dev/sd*</code> &mdash; Direktes Schreiben auf Block Devices</li>
669
+ </ul>
670
+ </div>
671
+ </details>
672
+ <details>
673
+ <summary>Secret Exposure (Warnung)</summary>
674
+ <div class="inner">
675
+ <p>Bei Write/Edit: Warnt wenn API Keys, Passwörter oder Tokens im Code erscheinen. ANTHROPIC_API_KEY, MINIMAX_API_KEY, OPENAI_API_KEY, AWS_SECRET_ACCESS_KEY, password=, token=.</p>
676
+ </div>
677
+ </details>
678
+ <details>
679
+ <summary>WebSearch/WebFetch in SubAgents</summary>
680
+ <div class="inner">
681
+ <p>Blockiert WebSearch und WebFetch komplett &mdash; erzeugen Permission-Prompts die Samuel unterbrechen. Stattdessen: Gemini MCP als Background-Agent.</p>
682
+ <p>Prueft auch Agent-Prompts auf "websearch"/"webfetch" Keywords und blockiert diese.</p>
683
+ </div>
684
+ </details>
685
+ <div class="meta">Hook: PreToolUse &rarr; safety.py check_safety() + check_secret_exposure() &rarr; block/allow</div>
686
+ </div>
687
+ </div>
688
+
689
+ <!-- Tab 5: Deep Analysis -->
690
+ <div class="tab-content" id="tab-4">
691
+ <div class="tab-panel">
692
+ <h3><span class="icon">&#129504;</span> Deep Analysis &mdash; MiniMax Slow Path</h3>
693
+ <p style="color:var(--text-dim);margin-bottom:14px">Periodische LLM-Bewertung deiner Arbeit durch MiniMax M2.5 mit sandboxed Function Calling.</p>
694
+ <ul>
695
+ <li><strong>4 Tools</strong> &mdash; <code style="font-family:var(--font-mono)">read_file</code>, <code style="font-family:var(--font-mono)">list_dir</code>, <code style="font-family:var(--font-mono)">search_code</code>, <code style="font-family:var(--font-mono)">write_protocol</code> (sandboxed, read-only + Protokoll)</li>
696
+ <li><strong>Adaptives Intervall</strong> &mdash; 10min (clean), 5min (1+ Fehler), 3min (3+ Fehler)</li>
697
+ <li><strong>Event-Trigger</strong> &mdash; Error-Burst, Phase-Change, Stuck-Detection loesen sofortige Analyse aus</li>
698
+ <li><strong>Budget</strong> &mdash; $0.50/Tag, ~$0.0003/Analyse. BudgetTracker verhindert Ueberschreitung.</li>
699
+ <li><strong>Ergebnis-Pipeline</strong> &mdash; Analyse-Ergebnis wird auf den naechsten Prompt geprepended + optional von Florian vorgelesen.</li>
700
+ <li><strong>Coaching Rules</strong> &mdash; MiniMax kennt Samuels Regeln aus <code style="font-family:var(--font-mono)">coaching_rules.txt</code>: Research First, Delegation, CLI First, Intent First, Session-Hygiene.</li>
701
+ </ul>
702
+ <div class="meta">samuel/harness.py &rarr; MiniMax M2.5 Agent Loop &rarr; Function Calling &rarr; write_protocol &rarr; next prompt</div>
703
+ </div>
704
+ </div>
705
+ </section>
706
+
707
+ <!-- ============================================================ -->
708
+ <!-- SEKTION 3: 48 Mechanismen -->
709
+ <!-- ============================================================ -->
710
+ <section class="anim" style="--i:7">
711
+ <div class="section-label">Sektion 3</div>
712
+ <h2 class="section-title">48 Mechanismen &mdash; dein Coaching-Team</h2>
713
+ <p class="section-desc">Jeder Mechanismus erkennt ein Muster und reagiert darauf. Filtere nach Kategorie.</p>
714
+
715
+ <div class="filter-bar">
716
+ <button class="filter-btn active" onclick="filterMechs('all')">Alle (48)</button>
717
+ <button class="filter-btn" onclick="filterMechs('focus')"><span class="cat-dot cat-focus"></span>Fokus (7)</button>
718
+ <button class="filter-btn" onclick="filterMechs('learning')"><span class="cat-dot cat-learning"></span>Lernen (6)</button>
719
+ <button class="filter-btn" onclick="filterMechs('project')"><span class="cat-dot cat-project"></span>Projekt (5)</button>
720
+ <button class="filter-btn" onclick="filterMechs('dopamine')"><span class="cat-dot cat-dopamine"></span>Dopamin (5)</button>
721
+ <button class="filter-btn" onclick="filterMechs('awareness')"><span class="cat-dot cat-awareness"></span>Awareness (5)</button>
722
+ <button class="filter-btn" onclick="filterMechs('quality')"><span class="cat-dot cat-quality"></span>Qualitaet (10)</button>
723
+ <button class="filter-btn" onclick="filterMechs('adhs')"><span class="cat-dot cat-adhs"></span>ADHS (6)</button>
724
+ </div>
725
+ <div class="mech-count" id="mech-count">48 Mechanismen</div>
726
+
727
+ <div class="mech-grid" id="mech-grid">
728
+ <!-- Fokus & Session 1-7 -->
729
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#1</span><span class="mech-name">goal_enforcement</span></div><div class="mech-desc">Session-Ziele setzen und erinnern</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 15min</div></div>
730
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#2</span><span class="mech-name">session_timer</span></div><div class="mech-desc">90-Minuten Session-Limit</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 15min</div></div>
731
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#3</span><span class="mech-name">break_reminder</span></div><div class="mech-desc">Pausen-Erinnerung nach langer Arbeit</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 5min</div></div>
732
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#4</span><span class="mech-name">scope_creep_detection</span></div><div class="mech-desc">Scope Creep erkennen bei Projektwechsel</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 5min</div></div>
733
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#5</span><span class="mech-name">context_switch_cost</span></div><div class="mech-desc">Kosten von Kontextwechseln aufzeigen</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 5min</div></div>
734
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#6</span><span class="mech-name">deep_work_protector</span></div><div class="mech-desc">Deep Work Phasen schuetzen</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 5min</div></div>
735
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#7</span><span class="mech-name">paralysis_breaker</span></div><div class="mech-desc">Paralysis nach 20 Min erkennen</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 5min</div></div>
736
+
737
+ <!-- Lernen & Wissen 8-13 -->
738
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#8</span><span class="mech-name">skill_matrix_check</span></div><div class="mech-desc">Skill-Level bei neuen Konzepten pruefen (Angst-Themen!)</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 5min</div></div>
739
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#9</span><span class="mech-name">explain_check</span></div><div class="mech-desc">Erklaer-Check nach Vibe-Coding</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 30min</div></div>
740
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#10</span><span class="mech-name">learning_path</span></div><div class="mech-desc">Lernpfad-Vorschlaege via MiniMax</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 5min</div></div>
741
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#11</span><span class="mech-name">micro_lesson</span></div><div class="mech-desc">Micro-Lessons bei Wissenluecken</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 5min</div></div>
742
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#12</span><span class="mech-name">pattern_recognition</span></div><div class="mech-desc">Wiederkehrende Muster erkennen</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 5min</div></div>
743
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#13</span><span class="mech-name">documentation_nudge</span></div><div class="mech-desc">Dokumentation nach Feature anregen</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 5min</div></div>
744
+
745
+ <!-- Projekt & Accountability 14-18 -->
746
+ <div class="mech-chip" data-cat="project"><div class="mech-head"><span class="mech-id">#14</span><span class="mech-name">milestone_tracker</span></div><div class="mech-desc">Projekt-Fortschritt tracken</div><div class="mech-meta"><span class="cat-dot cat-project"></span>Projekt &middot; CD 5min</div></div>
747
+ <div class="mech-chip" data-cat="project"><div class="mech-head"><span class="mech-id">#15</span><span class="mech-name">commit_frequency</span></div><div class="mech-desc">Commit-Haeufigkeit ueberwachen</div><div class="mech-meta"><span class="cat-dot cat-project"></span>Projekt &middot; CD 10min</div></div>
748
+ <div class="mech-chip" data-cat="project"><div class="mech-head"><span class="mech-id">#16</span><span class="mech-name">test_coverage_nudge</span></div><div class="mech-desc">Tests nach Feature-Code anregen</div><div class="mech-meta"><span class="cat-dot cat-project"></span>Projekt &middot; CD 5min</div></div>
749
+ <div class="mech-chip" data-cat="project"><div class="mech-head"><span class="mech-id">#17</span><span class="mech-name">pr_review_reminder</span></div><div class="mech-desc">PR Review Erinnerung</div><div class="mech-meta"><span class="cat-dot cat-project"></span>Projekt &middot; CD 5min</div></div>
750
+ <div class="mech-chip" data-cat="project"><div class="mech-head"><span class="mech-id">#18</span><span class="mech-name">project_health_check</span></div><div class="mech-desc">Projekt-Gesundheits-Check</div><div class="mech-meta"><span class="cat-dot cat-project"></span>Projekt &middot; CD 5min</div></div>
751
+
752
+ <!-- Dopamin & Motivation 19-23 -->
753
+ <div class="mech-chip" data-cat="dopamine"><div class="mech-head"><span class="mech-id">#19</span><span class="mech-name">xp_award</span></div><div class="mech-desc">XP vergeben bei Erfolgen</div><div class="mech-meta"><span class="cat-dot cat-dopamine"></span>Dopamin &middot; CD 5min</div></div>
754
+ <div class="mech-chip" data-cat="dopamine"><div class="mech-head"><span class="mech-id">#20</span><span class="mech-name">streak_tracker</span></div><div class="mech-desc">Streak-Counter fuer taegliche Arbeit</div><div class="mech-meta"><span class="cat-dot cat-dopamine"></span>Dopamin &middot; CD 5min</div></div>
755
+ <div class="mech-chip" data-cat="dopamine"><div class="mech-head"><span class="mech-id">#21</span><span class="mech-name">small_win_celebration</span></div><div class="mech-desc">Kleine Erfolge feiern</div><div class="mech-meta"><span class="cat-dot cat-dopamine"></span>Dopamin &middot; CD 5min</div></div>
756
+ <div class="mech-chip" data-cat="dopamine"><div class="mech-head"><span class="mech-id">#22</span><span class="mech-name">progress_visualization</span></div><div class="mech-desc">Fortschritt visuell darstellen</div><div class="mech-meta"><span class="cat-dot cat-dopamine"></span>Dopamin &middot; CD 5min</div></div>
757
+ <div class="mech-chip" data-cat="dopamine"><div class="mech-head"><span class="mech-id">#23</span><span class="mech-name">challenge_mode</span></div><div class="mech-desc">Herausforderungen vorschlagen</div><div class="mech-meta"><span class="cat-dot cat-dopamine"></span>Dopamin &middot; CD 5min</div></div>
758
+
759
+ <!-- Awareness 24-28 -->
760
+ <div class="mech-chip" data-cat="awareness"><div class="mech-head"><span class="mech-id">#24</span><span class="mech-name">energy_level_check</span></div><div class="mech-desc">Energie-Level abfragen</div><div class="mech-meta"><span class="cat-dot cat-awareness"></span>Awareness &middot; CD 5min</div></div>
761
+ <div class="mech-chip" data-cat="awareness"><div class="mech-head"><span class="mech-id">#25</span><span class="mech-name">peak_time_optimizer</span></div><div class="mech-desc">Peak-Zeit optimal nutzen</div><div class="mech-meta"><span class="cat-dot cat-awareness"></span>Awareness &middot; CD 5min</div></div>
762
+ <div class="mech-chip" data-cat="awareness"><div class="mech-head"><span class="mech-id">#26</span><span class="mech-name">mood_tracker</span></div><div class="mech-desc">Stimmung tracken</div><div class="mech-meta"><span class="cat-dot cat-awareness"></span>Awareness &middot; CD 5min</div></div>
763
+ <div class="mech-chip" data-cat="awareness"><div class="mech-head"><span class="mech-id">#27</span><span class="mech-name">reflection_prompt</span></div><div class="mech-desc">Reflexions-Fragen stellen</div><div class="mech-meta"><span class="cat-dot cat-awareness"></span>Awareness &middot; CD 5min</div></div>
764
+ <div class="mech-chip" data-cat="awareness"><div class="mech-head"><span class="mech-id">#28</span><span class="mech-name">burnout_detector</span></div><div class="mech-desc">Burnout-Anzeichen erkennen</div><div class="mech-meta"><span class="cat-dot cat-awareness"></span>Awareness &middot; CD 5min</div></div>
765
+
766
+ <!-- Qualitaet 29-32 -->
767
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#29</span><span class="mech-name">safety_gate</span></div><div class="mech-desc">Gefaehrliche Befehle blockieren (str-only, kein MechanismResult)</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 5min</div></div>
768
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#30</span><span class="mech-name">code_review_nudge</span></div><div class="mech-desc">Code Review nach grossem Feature</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 5min</div></div>
769
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#31</span><span class="mech-name">refactor_detector</span></div><div class="mech-desc">Refactoring-Bedarf erkennen</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 5min</div></div>
770
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#32</span><span class="mech-name">dependency_check</span></div><div class="mech-desc">Dependency-Updates pruefen</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 5min</div></div>
771
+
772
+ <!-- ADHS 33-36 -->
773
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#33</span><span class="mech-name">hyperfocus_guard</span></div><div class="mech-desc">Hyperfokus nach 4h unterbrechen</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 30min</div></div>
774
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#34</span><span class="mech-name">direction_check</span></div><div class="mech-desc">Richtungs-Check nach 15 Min ohne Fortschritt</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 15min</div></div>
775
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#35</span><span class="mech-name">decision_support</span></div><div class="mech-desc">Entscheidungs-Hilfe bei A/B-Fragen</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 10min</div></div>
776
+ <div class="mech-chip" data-cat="learning"><div class="mech-head"><span class="mech-id">#36</span><span class="mech-name">depth_check</span></div><div class="mech-desc">Edge-Case Check alle 20 Edits</div><div class="mech-meta"><span class="cat-dot cat-learning"></span>Lernen &middot; CD 20min</div></div>
777
+
778
+ <!-- ADHS Live-Patterns 37-40 -->
779
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#37</span><span class="mech-name">big_bang_guard</span></div><div class="mech-desc">Warnung bei &gt;15 Edits ohne Test-Run</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 10min</div></div>
780
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#38</span><span class="mech-name">review_requires_test</span></div><div class="mech-desc">Review ohne vorherigen Test erkennen</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 15min</div></div>
781
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#39</span><span class="mech-name">bug_first</span></div><div class="mech-desc">Feature-Creep bei offenen Bugs erkennen</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 10min</div></div>
782
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#40</span><span class="mech-name">zombie_bug_detector</span></div><div class="mech-desc">Wiederkehrende Bugs ueber Sessions erkennen</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 30min</div></div>
783
+
784
+ <!-- Planning & Drift 41 -->
785
+ <div class="mech-chip" data-cat="focus"><div class="mech-head"><span class="mech-id">#41</span><span class="mech-name">planning_drift_detector</span></div><div class="mech-desc">Erkennt wenn Agents ohne Richtung arbeiten</div><div class="mech-meta"><span class="cat-dot cat-focus"></span>Fokus &middot; CD 15min</div></div>
786
+
787
+ <!-- Phase Contracts 42 -->
788
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#42</span><span class="mech-name">contract_compliance</span></div><div class="mech-desc">Prueft ob Phase-Contract-Obligations erfuellt werden</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 10min</div></div>
789
+
790
+ <!-- Workflow 43-48 (Grosse-Fehler-Analyse) -->
791
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#43</span><span class="mech-name">new_repo_guard</span></div><div class="mech-desc">Warnung bei neuem Repo ohne Analyse</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 30min</div></div>
792
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#44</span><span class="mech-name">skeleton_first</span></div><div class="mech-desc">Kein Test-Skeleton bei &gt;5 Edits</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 10min</div></div>
793
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#45</span><span class="mech-name">wiring_check</span></div><div class="mech-desc">Neues Feature gewired aber nicht getestet</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 15min</div></div>
794
+ <div class="mech-chip" data-cat="quality"><div class="mech-head"><span class="mech-id">#46</span><span class="mech-name">threshold_math</span></div><div class="mech-desc">Schwellwert-Aenderung ohne Erreichbarkeits-Check</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet &middot; CD 15min</div></div>
795
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#47</span><span class="mech-name">version_squared</span></div><div class="mech-desc">V(N+1) ohne V(N) Learnings</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 60min</div></div>
796
+ <div class="mech-chip" data-cat="adhs"><div class="mech-head"><span class="mech-id">#48</span><span class="mech-name">usage_proof</span></div><div class="mech-desc">Grosses Feature ohne Nutzungsnachweis</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS &middot; CD 30min</div></div>
797
+ </div>
798
+ </section>
799
+
800
+ <!-- ============================================================ -->
801
+ <!-- SEKTION 4: Samuel kennen lernen (Personalisierung) -->
802
+ <!-- ============================================================ -->
803
+ <section class="anim" style="--i:8">
804
+ <div class="section-label">Sektion 4</div>
805
+ <h2 class="section-title">Samuel kennen lernen</h2>
806
+ <p class="section-desc">Sidecar kennt dich &mdash; Rhythmus, Skills, Aengste, Projekte. Alles aus <code style="font-family:var(--font-mono);color:var(--accent)">samuel.toml</code> und der Digital Twin DB.</p>
807
+
808
+ <div class="profile-grid">
809
+ <!-- Rhythmus -->
810
+ <div class="card">
811
+ <h3>Rhythmus &amp; ADHS</h3>
812
+ <ul>
813
+ <li><span>Aufstehen</span><span class="skill-level sl-basics">11:00</span></li>
814
+ <li><span>Peak-Zeit</span><span class="skill-level sl-kann-bauen">13:00 &ndash; 17:00</span></li>
815
+ <li><span>Second Wave</span><span class="skill-level sl-kann-nutzen">18:00 &ndash; 20:00</span></li>
816
+ <li><span>Max Session</span><span class="skill-level sl-basics">90 min</span></li>
817
+ <li><span>Hyperfocus-Limit</span><span class="skill-level sl-angst">4h</span></li>
818
+ <li><span>Paralysis-Timeout</span><span class="skill-level sl-angst">20 min</span></li>
819
+ <li><span>Max Goals/Session</span><span class="skill-level sl-basics">2</span></li>
820
+ <li><span>Medikation</span><span class="skill-level sl-basics">Methylphenidat 10mg</span></li>
821
+ </ul>
822
+ </div>
823
+
824
+ <!-- Skills -->
825
+ <div class="card">
826
+ <h3>Skill-Matrix</h3>
827
+ <ul>
828
+ <li><span>Sockets / asyncio / Plugin-System</span><span class="skill-level sl-kann-bauen">kann-bauen</span></li>
829
+ <li><span>FastAPI / Prompt Engineering</span><span class="skill-level sl-kann-bauen">kann-bauen</span></li>
830
+ <li><span>Agent Design / Multi-Model</span><span class="skill-level sl-kann-bauen">kann-bauen</span></li>
831
+ <li><span>Context Engineering</span><span class="skill-level sl-kann-bauen">kann-bauen</span></li>
832
+ <li><span>Token Economics / Guardrails</span><span class="skill-level sl-kann-nutzen">kann-nutzen</span></li>
833
+ <li><span>Git / Testing / Caching</span><span class="skill-level sl-basics">basics</span></li>
834
+ <li><span>Cost Optimization</span><span class="skill-level sl-basics">basics</span></li>
835
+ <li><span>Databases / Deployment</span><span class="skill-level sl-angst">Angst</span></li>
836
+ <li><span>Fine-Tuning / MLOps</span><span class="skill-level sl-angst">Angst</span></li>
837
+ <li><span>RAG / Embeddings / Evals</span><span class="skill-level sl-keine">keine</span></li>
838
+ </ul>
839
+ </div>
840
+
841
+ <!-- Fear Map -->
842
+ <div class="card fear-card">
843
+ <h4>Fear Map &mdash; 4 Angst-Themen</h4>
844
+ <p style="font-size:0.88rem;color:var(--text-dim);margin-bottom:10px">Sidecar erkennt wenn du in einem Angst-Bereich arbeitest und bietet Micro-Offers an &mdash; keine Belehrung, sondern kleine Schritte.</p>
845
+ <ul style="list-style:none;padding:0">
846
+ <li style="padding:6px 0;border-bottom:1px solid rgba(243,139,168,0.1)"><strong>databases</strong> &mdash; SQL, Schema-Design, Migrations. Micro-Offer: "Ein SELECT reicht fuer heute."</li>
847
+ <li style="padding:6px 0;border-bottom:1px solid rgba(243,139,168,0.1)"><strong>deployment</strong> &mdash; Server, Docker, CI/CD. Micro-Offer: "Nur ein systemd Service. Kein K8s."</li>
848
+ <li style="padding:6px 0;border-bottom:1px solid rgba(243,139,168,0.1)"><strong>fine-tuning</strong> &mdash; Model Training, Datasets. Micro-Offer: "Erstmal nur API-Call testen."</li>
849
+ <li style="padding:6px 0"><strong>mlops</strong> &mdash; Pipelines, Monitoring, A/B-Tests. Micro-Offer: "Ein pytest reicht als Anfang."</li>
850
+ </ul>
851
+ </div>
852
+
853
+ <!-- Digital Twin -->
854
+ <div class="card">
855
+ <h3>Digital Twin (memory.db)</h3>
856
+ <p style="font-size:0.88rem;color:var(--text-dim);margin-bottom:10px">SQLite-Datenbank unter <code style="font-family:var(--font-mono);color:var(--accent)">~/.ubw/memory.db</code>. Shared mit V6.</p>
857
+ <ul style="list-style:none;padding:0">
858
+ <li style="padding:5px 0"><strong>observations</strong> &mdash; Tool-Events, Phase-Wechsel, Session-Metriken</li>
859
+ <li style="padding:5px 0"><strong>insights</strong> &mdash; Synthetisierte Erkenntnisse (5 Detektoren: repeated_error, phase_mismatch, time_patterns, skill_signals, session_quality)</li>
860
+ <li style="padding:5px 0"><strong>fear_log</strong> &mdash; Wann welches Angst-Thema getriggert wurde</li>
861
+ <li style="padding:5px 0"><strong>tts_ratings</strong> &mdash; Multi-dim Bewertungen (content/timing/form/dismiss_ms)</li>
862
+ <li style="padding:5px 0"><strong>knowledge</strong> &mdash; Explizite Fakten, Regeln, Hypothesen, Open Loops</li>
863
+ <li style="padding:5px 0"><strong>dynamic_memory</strong> &mdash; Automatisch synthetisiert, in Context Bridge injiziert</li>
864
+ </ul>
865
+ </div>
866
+ </div>
867
+
868
+ <!-- Projekte -->
869
+ <div style="margin-top:24px">
870
+ <h3 style="font-size:1.05rem;margin-bottom:12px">Projekt-Tracking</h3>
871
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px">
872
+ <div class="card" style="padding:14px">
873
+ <div style="font-weight:600;font-size:0.9rem">Sidecar V7</div>
874
+ <div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-dim)"><span>3/10 Milestones</span><span class="highlight hl-green">active</span></div>
875
+ <div style="height:4px;background:var(--surface1);border-radius:2px;margin-top:6px"><div style="height:100%;width:30%;background:var(--green);border-radius:2px"></div></div>
876
+ </div>
877
+ <div class="card" style="padding:14px">
878
+ <div style="font-weight:600;font-size:0.9rem">Sidecar V6</div>
879
+ <div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-dim)"><span>10/10 Milestones</span><span class="highlight hl-blue">done</span></div>
880
+ <div style="height:4px;background:var(--surface1);border-radius:2px;margin-top:6px"><div style="height:100%;width:100%;background:var(--blue);border-radius:2px"></div></div>
881
+ </div>
882
+ <div class="card" style="padding:14px">
883
+ <div style="font-weight:600;font-size:0.9rem">FireTracker</div>
884
+ <div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-dim)"><span>1/7 Milestones</span><span class="highlight hl-yellow">paused</span></div>
885
+ <div style="height:4px;background:var(--surface1);border-radius:2px;margin-top:6px"><div style="height:100%;width:14%;background:var(--yellow);border-radius:2px"></div></div>
886
+ </div>
887
+ <div class="card" style="padding:14px">
888
+ <div style="font-weight:600;font-size:0.9rem">StudyAgent</div>
889
+ <div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-dim)"><span>1/5 Milestones</span><span class="highlight hl-yellow">paused</span></div>
890
+ <div style="height:4px;background:var(--surface1);border-radius:2px;margin-top:6px"><div style="height:100%;width:20%;background:var(--yellow);border-radius:2px"></div></div>
891
+ </div>
892
+ <div class="card" style="padding:14px">
893
+ <div style="font-weight:600;font-size:0.9rem">TypoTuner</div>
894
+ <div style="display:flex;justify-content:space-between;font-size:0.82rem;color:var(--text-dim)"><span>1/5 Milestones</span><span class="highlight hl-blue">done</span></div>
895
+ <div style="height:4px;background:var(--surface1);border-radius:2px;margin-top:6px"><div style="height:100%;width:20%;background:var(--blue);border-radius:2px"></div></div>
896
+ </div>
897
+ </div>
898
+ </div>
899
+ </section>
900
+
901
+ <!-- ============================================================ -->
902
+ <!-- SEKTION 5: Sidecar lernt mit dir (Self-Tuning) -->
903
+ <!-- ============================================================ -->
904
+ <section class="anim" style="--i:9">
905
+ <div class="section-label">Sektion 5</div>
906
+ <h2 class="section-title">Sidecar lernt mit dir</h2>
907
+ <p class="section-desc">Das System tuned sich selbst. Gute Nudges feuern oefter, schlechte seltener, tote werden deaktiviert.</p>
908
+
909
+ <div class="tuning-grid">
910
+ <div class="card">
911
+ <h4>EWMA Ratings</h4>
912
+ <p>Jeder Mechanismus hat eine gewichtete Bewertung. <code style="font-family:var(--font-mono);color:var(--peach)">alpha=0.3</code> &mdash; 30% neues Signal, 70% Historie.</p>
913
+ <p style="margin-top:8px">Startwert: 3.0 (neutral). Erster Wert wird direkt uebernommen, danach gewichtet.</p>
914
+ </div>
915
+ <div class="card">
916
+ <h4>Auto-Rating Signale</h4>
917
+ <ul style="list-style:none;padding:0">
918
+ <li style="padding:4px 0"><span class="highlight hl-green">+0.5</span> Commit nach Nudge</li>
919
+ <li style="padding:4px 0"><span class="highlight hl-red">-0.5</span> Fehler-Burst nach Nudge</li>
920
+ <li style="padding:4px 0"><span class="highlight hl-blue">+1/-1</span> Manuelles TTS-Voting</li>
921
+ <li style="padding:4px 0"><span class="highlight hl-mauve">Multi-dim</span> Content + Timing + Form + Dismiss-Speed</li>
922
+ </ul>
923
+ </div>
924
+ <div class="card">
925
+ <h4>AdaptationEngine (alle 30min)</h4>
926
+ <p style="font-size:0.88rem;color:var(--text-dim)">Weighted Score = 0.4 &times; success_rate + 0.3 &times; trend + 0.3 &times; context_match</p>
927
+ <ul style="list-style:none;padding:0;margin-top:8px">
928
+ <li style="padding:4px 0"><span class="highlight hl-green">Score &gt; 0.8</span> Cooldown halbiert &mdash; "Top Performer"</li>
929
+ <li style="padding:4px 0"><span class="highlight hl-blue">0.3 &ndash; 0.8</span> Keine Aenderung &mdash; "Normal"</li>
930
+ <li style="padding:4px 0"><span class="highlight hl-yellow">Score &lt; 0.3</span> Cooldown verdoppelt &mdash; "Underperformer"</li>
931
+ <li style="padding:4px 0"><span class="highlight hl-red">Score &lt; 0.15</span> Deaktiviert &mdash; "Remove"</li>
932
+ <li style="padding:4px 0"><span class="highlight hl-peach">48h ohne Fire</span> Dormant auto-disable</li>
933
+ </ul>
934
+ </div>
935
+ <div class="card">
936
+ <h4>Spaced Repetition</h4>
937
+ <p style="font-size:0.88rem;color:var(--text-dim)">Ebbinghaus-Intervalle fuer gelernte Konzepte: <code style="font-family:var(--font-mono)">1d &rarr; 3d &rarr; 7d &rarr; 14d &rarr; 30d</code></p>
938
+ <ul style="list-style:none;padding:0;margin-top:8px">
939
+ <li style="padding:4px 0"><strong>Gap Detection</strong> &mdash; Gleicher Fehler 3+ mal = Knowledge Gap</li>
940
+ <li style="padding:4px 0"><strong>Max 2 Reviews/Session</strong> &mdash; Nicht ueberfluten</li>
941
+ <li style="padding:4px 0"><strong>Persist</strong> &mdash; JSON unter /tmp/sidecar-v7-learner.json</li>
942
+ </ul>
943
+ </div>
944
+ </div>
945
+ </section>
946
+
947
+ <!-- ============================================================ -->
948
+ <!-- SEKTION 6: So steuerst du Sidecar -->
949
+ <!-- ============================================================ -->
950
+ <section class="anim" style="--i:10">
951
+ <div class="section-label">Sektion 6</div>
952
+ <h2 class="section-title">So steuerst du Sidecar</h2>
953
+ <p class="section-desc">CLI, Dashboard und GTK Panel &mdash; drei Wege Sidecar zu kontrollieren.</p>
954
+
955
+ <h3 style="font-size:1.05rem;margin-bottom:14px">sidecar-v7-ctl</h3>
956
+ <div class="cli-grid">
957
+ <div class="cli-cmd"><code>sidecar-v7-ctl status</code><span>Session-Info, Phase, Prompts, Errors, Extensions</span></div>
958
+ <div class="cli-cmd"><code>sidecar-v7-ctl ranking</code><span>Alle 48 Mechanismen nach EWMA-Rating sortiert</span></div>
959
+ <div class="cli-cmd"><code>sidecar-v7-ctl vote &lt;id&gt; +/-</code><span>Mechanismus bewerten (thumbs up/down)</span></div>
960
+ <div class="cli-cmd"><code>sidecar-v7-ctl sessions</code><span>Alle getrackten Sessions auflisten</span></div>
961
+ <div class="cli-cmd"><code>sidecar-v7-ctl watch &lt;sid&gt;</code><span>Aktive Session wechseln</span></div>
962
+ <div class="cli-cmd"><code>sidecar-v7-ctl dashboard</code><span>Dashboard im Browser oeffnen (:5112)</span></div>
963
+ <div class="cli-cmd"><code>sidecar-v7-ctl feierabend</code><span>Florian spricht Feierabend-Summary (2-5 min)</span></div>
964
+ <div class="cli-cmd"><code>sidecar-v7-ctl proposals</code><span>Sidecar-Vorschlaege anzeigen + Lifecycle</span></div>
965
+ <div class="cli-cmd"><code>sidecar-v7-ctl knowledge add</code><span>Manuell Wissens-Objekt hinzufuegen</span></div>
966
+ <div class="cli-cmd"><code>sidecar-v7-ctl purge --days 90</code><span>Alte Memory-Rows aufraumen (dry-run moeglich)</span></div>
967
+ <div class="cli-cmd"><code>sidecar-v7-ctl skills</code><span>Skill-Matrix aus samuel.toml</span></div>
968
+ <div class="cli-cmd"><code>sidecar-v7-ctl ping</code><span>Daemon Health-Check (pong?)</span></div>
969
+ </div>
970
+
971
+ <div style="margin-top:32px;display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px">
972
+ <div class="card">
973
+ <h3>Dashboard (:5112)</h3>
974
+ <p>FastAPI + htmx + Tailwind. Live-Ansicht von:</p>
975
+ <ul style="list-style:none;padding:0;margin-top:8px">
976
+ <li style="padding:3px 0">&bull; Mechanismus-Ranking mit EWMA-Scores</li>
977
+ <li style="padding:3px 0">&bull; Session-Stats (Prompts, Errors, Phase)</li>
978
+ <li style="padding:3px 0">&bull; TTS-Log (Florian Spoken History)</li>
979
+ <li style="padding:3px 0">&bull; Alignment-Digest (Bookmarks, Projects, Gaps)</li>
980
+ <li style="padding:3px 0">&bull; Memory Explorer (Twin DB Eintraege)</li>
981
+ <li style="padding:3px 0">&bull; Explainer-History (alle generierten Erklaerungen)</li>
982
+ <li style="padding:3px 0">&bull; XP &amp; Streak-Tracker</li>
983
+ </ul>
984
+ </div>
985
+ <div class="card">
986
+ <h3>GTK3 Panel</h3>
987
+ <p>Kompaktes Desktop-Widget. Zeigt:</p>
988
+ <ul style="list-style:none;padding:0;margin-top:8px">
989
+ <li style="padding:3px 0">&bull; Aktuelle Phase + Workflow</li>
990
+ <li style="padding:3px 0">&bull; Letzte Nudge mit Rating-Widget (Emoji-Grid)</li>
991
+ <li style="padding:3px 0">&bull; TTS-Stop bei Downvote</li>
992
+ <li style="padding:3px 0">&bull; Bubble-Dismiss fuer unerwuenschte Popups</li>
993
+ <li style="padding:3px 0">&bull; Singleton PID (nur eine Instanz)</li>
994
+ </ul>
995
+ </div>
996
+ <div class="card">
997
+ <h3>Systemd Service</h3>
998
+ <p>User-Level Service:</p>
999
+ <ul style="list-style:none;padding:0;margin-top:8px">
1000
+ <li style="padding:3px 0"><code style="font-family:var(--font-mono);font-size:0.82rem;color:var(--green)">systemctl --user start sidecar-v7</code></li>
1001
+ <li style="padding:3px 0"><code style="font-family:var(--font-mono);font-size:0.82rem;color:var(--green)">journalctl --user -u sidecar-v7 -f</code></li>
1002
+ <li style="padding:3px 0">Auto-Restart, WatchdogSec, Socket-Cleanup</li>
1003
+ </ul>
1004
+ </div>
1005
+ </div>
1006
+ </section>
1007
+
1008
+ <footer>
1009
+ Sidecar V7 &middot; ~11K LOC &middot; core/ (MIT) + samuel/ (privat) &middot; Socket /tmp/sidecar-v7.sock &middot; Dashboard :5112 &middot; TTS :5050
1010
+ </footer>
1011
+
1012
+ </div><!-- /wrap -->
1013
+
1014
+ <script>
1015
+ // Tab switching for the 5 intervention paths
1016
+ function showTab(idx) {
1017
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
1018
+ document.querySelectorAll('.flow-out').forEach(el => el.classList.remove('active'));
1019
+ const tab = document.getElementById('tab-' + idx);
1020
+ const btn = document.getElementById('ftab-' + idx);
1021
+ if (tab) tab.classList.add('active');
1022
+ if (btn) btn.classList.add('active');
1023
+ }
1024
+
1025
+ // Mechanism filter
1026
+ function filterMechs(cat) {
1027
+ const chips = document.querySelectorAll('.mech-chip');
1028
+ let count = 0;
1029
+ chips.forEach(chip => {
1030
+ if (cat === 'all' || chip.dataset.cat === cat) {
1031
+ chip.style.display = '';
1032
+ count++;
1033
+ } else {
1034
+ chip.style.display = 'none';
1035
+ }
1036
+ });
1037
+ document.getElementById('mech-count').textContent = count + ' Mechanismen';
1038
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1039
+ btn.classList.toggle('active', btn.textContent.includes(
1040
+ cat === 'all' ? 'Alle' :
1041
+ cat === 'focus' ? 'Fokus' :
1042
+ cat === 'learning' ? 'Lernen' :
1043
+ cat === 'project' ? 'Projekt' :
1044
+ cat === 'dopamine' ? 'Dopamin' :
1045
+ cat === 'awareness' ? 'Awareness' :
1046
+ cat === 'quality' ? 'Qualitaet' :
1047
+ cat === 'adhs' ? 'ADHS' : ''
1048
+ ));
1049
+ });
1050
+ }
1051
+
1052
+ // Scroll-triggered fadeUp (IntersectionObserver)
1053
+ const obs = new IntersectionObserver((entries) => {
1054
+ entries.forEach(e => {
1055
+ if (e.isIntersecting) {
1056
+ e.target.style.animationPlayState = 'running';
1057
+ obs.unobserve(e.target);
1058
+ }
1059
+ });
1060
+ }, { threshold: 0.1 });
1061
+
1062
+ document.querySelectorAll('.anim').forEach(el => {
1063
+ el.style.animationPlayState = 'paused';
1064
+ obs.observe(el);
1065
+ });
1066
+ </script>
1067
+
1068
+ </body>
1069
+ </html>
sidecar-v7-demo/robots.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ User-agent: *
2
+ Disallow: /