Initial public upload from Projekte/Sidecar-Workspace (part 17)
Browse files- codex-sidecarV1/.gitignore +3 -0
- codex-sidecarV1/.pytest_cache/.gitignore +2 -0
- codex-sidecarV1/.pytest_cache/CACHEDIR.TAG +4 -0
- codex-sidecarV1/.pytest_cache/README.md +8 -0
- codex-sidecarV1/.pytest_cache/v/cache/lastfailed +5 -0
- codex-sidecarV1/.pytest_cache/v/cache/nodeids +15 -0
- codex-sidecarV1/README.md +53 -0
- codex-sidecarV1/pyproject.toml +21 -0
- codex-sidecarV1/src/codex_sidecar/__init__.py +21 -0
- codex-sidecarV1/src/codex_sidecar/__main__.py +7 -0
- codex-sidecarV1/src/codex_sidecar/capabilities.py +43 -0
- codex-sidecarV1/src/codex_sidecar/cli.py +190 -0
- codex-sidecarV1/src/codex_sidecar/governor.py +428 -0
- codex-sidecarV1/src/codex_sidecar/models.py +126 -0
- codex-sidecarV1/src/codex_sidecar/state.py +259 -0
- codex-sidecarV1/src/codex_sidecar/watcher.py +249 -0
- codex-sidecarV1/tests/fixtures/build_without_checks.jsonl +7 -0
- codex-sidecarV1/tests/fixtures/risky_shell.jsonl +6 -0
- codex-sidecarV1/tests/fixtures/subagent_session.jsonl +8 -0
- codex-sidecarV1/tests/test_cli.py +68 -0
- codex-sidecarV1/tests/test_governor.py +60 -0
- codex-sidecarV1/tests/test_state.py +42 -0
- codex-sidecarV1/tests/test_watcher.py +28 -0
- sidecar-v7-demo/.codex/config.toml +31 -0
- sidecar-v7-demo/.nojekyll +0 -0
- sidecar-v7-demo/index.html +1069 -0
- sidecar-v7-demo/robots.txt +2 -0
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 — 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 · ~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"><5ms</span> <strong>Fast Path</strong> — 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> — 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 — 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">→</div>
|
| 582 |
+
<div class="flow-node">Prompt</div>
|
| 583 |
+
<div class="flow-arrow">→</div>
|
| 584 |
+
<div class="flow-node sidecar">Sidecar V7</div>
|
| 585 |
+
<div class="flow-arrow">→</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">💬</span> Context Injection — 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> — 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> — Abgeleitet aus der Activity-Phase: <span class="highlight hl-teal">planning → build → test → deploy</span></li>
|
| 605 |
+
<li><strong>Contract-Tracking</strong> — Pflichten pro Phase (must/should). Score 0-100%. Warnung wenn Obligations nicht erfuellt.</li>
|
| 606 |
+
<li><strong>Anti-Pattern-Warnungen</strong> — Erkennt Hypothesen-lose Delegation, Silent Long-Running, Vibe-Coding ohne Verstaendnis.</li>
|
| 607 |
+
<li><strong>Skill-Empfehlungen</strong> — 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> — Hyperfocus-Warnung, Session-Sprawl-Erkennung, Paralysis-Detection nach 20min.</li>
|
| 609 |
+
<li><strong>Personal Context</strong> — Energie-Level (Peak 13-17h), Stimmung (Keyword-Analyse), Tageszeit, Angst-Themen.</li>
|
| 610 |
+
<li><strong>Twin Memory</strong> — Relevante Beobachtungen aus der Memory-DB (Insights, Fear-Log, gelernte Konzepte).</li>
|
| 611 |
+
</ul>
|
| 612 |
+
<div class="meta">Hook: UserPromptSubmit → bridge.py → daemon.py → ContextBridge.enrich() → 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">🎯</span> Coaching-Nudges — 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> — <span class="highlight hl-mauve">ADHS (Tier 0)</span> > <span class="highlight hl-blue">Fokus/Qualitaet (1)</span> > <span class="highlight hl-peach">Projekt (2)</span> > <span class="highlight hl-green">Lernen (3)</span> > <span class="highlight hl-yellow">Dopamin/Awareness (4)</span></li>
|
| 623 |
+
<li><strong>Cooldown-System</strong> — Jeder Mechanismus hat einen eigenen Cooldown (60s bis 3600s). Verhindert Spam.</li>
|
| 624 |
+
<li><strong>EWMA-Ratings</strong> — Gewichtete Bewertung (alpha=0.3). Top-Performer feuern oefter, Underperformer seltener.</li>
|
| 625 |
+
<li><strong>MechanismResult</strong> — 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> — 47/48 Mechanismen sprechen dich direkt an. Nur safety_gate (#29) blockiert sofort ohne Anrede.</li>
|
| 627 |
+
<li><strong>8 Kategorien</strong> — 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 → coach.py wire_all() → 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">🗣</span> Florian spricht — 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 — du hoerst Coaching, ohne auf den Bildschirm schauen zu muessen.</p>
|
| 638 |
+
<ul>
|
| 639 |
+
<li><strong>TTS-Server</strong> — Systemd Service auf Port :5050. Health-Check bei Start, Auto-Restart wenn down.</li>
|
| 640 |
+
<li><strong>Was gesprochen wird</strong> — Fokus/ADHS-Nudges (sofort), Slow-Path-Ergebnisse (zusammengefasst), Commit-Digests, Feierabend-Summary.</li>
|
| 641 |
+
<li><strong>Cooldowns pro Typ</strong> — 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> — Ring-Buffer der letzten 10 gesprochenen Texte. Keine Wiederholungen.</li>
|
| 643 |
+
<li><strong>STT-Antworten</strong> — Whisper STT. Samuel kann auf gesprochene Nudges antworten (max 3 Runden Dialog).</li>
|
| 644 |
+
<li><strong>TTS Voting</strong> — 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> — Backticks entfernt, Pfade gekuerzt, Abkuerzungen expandiert (PR→Pull Request, LOC→Lines of Code), Zahlen 0-12 als Woerter.</li>
|
| 646 |
+
</ul>
|
| 647 |
+
<div class="meta">samuel/florian.py → Edge TTS :5050 → 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">🛑</span> Safety Gates — 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 — 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> — Loeschen von Home/Root/CWD</li>
|
| 662 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">git push --force</code> — Force Push</li>
|
| 663 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">git reset --hard</code> — Hard Reset</li>
|
| 664 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">DROP TABLE / DROP DATABASE</code> — SQL Drop</li>
|
| 665 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">chmod 777</code> — Unsichere Berechtigungen</li>
|
| 666 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">mkfs.*</code> — Dateisystem formatieren</li>
|
| 667 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">sudo / su</code> — Kein TTY in Claude Code</li>
|
| 668 |
+
<li><code style="font-family:var(--font-mono);color:var(--red)">> /dev/sd*</code> — 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 — 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 → safety.py check_safety() + check_secret_exposure() → 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">🧠</span> Deep Analysis — 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> — <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> — 10min (clean), 5min (1+ Fehler), 3min (3+ Fehler)</li>
|
| 697 |
+
<li><strong>Event-Trigger</strong> — Error-Burst, Phase-Change, Stuck-Detection loesen sofortige Analyse aus</li>
|
| 698 |
+
<li><strong>Budget</strong> — $0.50/Tag, ~$0.0003/Analyse. BudgetTracker verhindert Ueberschreitung.</li>
|
| 699 |
+
<li><strong>Ergebnis-Pipeline</strong> — Analyse-Ergebnis wird auf den naechsten Prompt geprepended + optional von Florian vorgelesen.</li>
|
| 700 |
+
<li><strong>Coaching Rules</strong> — 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 → MiniMax M2.5 Agent Loop → Function Calling → write_protocol → 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 — 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 · 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 >15 Edits ohne Test-Run</div><div class="mech-meta"><span class="cat-dot cat-adhs"></span>ADHS · 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 · 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 · 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 · 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 · 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 · 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 · 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 >5 Edits</div><div class="mech-meta"><span class="cat-dot cat-quality"></span>Qualitaet · 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 · 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 · 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 · 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 · 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 — 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 & 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 – 17:00</span></li>
|
| 815 |
+
<li><span>Second Wave</span><span class="skill-level sl-kann-nutzen">18:00 – 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 — 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 — 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> — 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> — 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> — Model Training, Datasets. Micro-Offer: "Erstmal nur API-Call testen."</li>
|
| 849 |
+
<li style="padding:6px 0"><strong>mlops</strong> — 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> — Tool-Events, Phase-Wechsel, Session-Metriken</li>
|
| 859 |
+
<li style="padding:5px 0"><strong>insights</strong> — 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> — Wann welches Angst-Thema getriggert wurde</li>
|
| 861 |
+
<li style="padding:5px 0"><strong>tts_ratings</strong> — Multi-dim Bewertungen (content/timing/form/dismiss_ms)</li>
|
| 862 |
+
<li style="padding:5px 0"><strong>knowledge</strong> — Explizite Fakten, Regeln, Hypothesen, Open Loops</li>
|
| 863 |
+
<li style="padding:5px 0"><strong>dynamic_memory</strong> — 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> — 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 × success_rate + 0.3 × trend + 0.3 × 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 > 0.8</span> Cooldown halbiert — "Top Performer"</li>
|
| 929 |
+
<li style="padding:4px 0"><span class="highlight hl-blue">0.3 – 0.8</span> Keine Aenderung — "Normal"</li>
|
| 930 |
+
<li style="padding:4px 0"><span class="highlight hl-yellow">Score < 0.3</span> Cooldown verdoppelt — "Underperformer"</li>
|
| 931 |
+
<li style="padding:4px 0"><span class="highlight hl-red">Score < 0.15</span> Deaktiviert — "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 → 3d → 7d → 14d → 30d</code></p>
|
| 938 |
+
<ul style="list-style:none;padding:0;margin-top:8px">
|
| 939 |
+
<li style="padding:4px 0"><strong>Gap Detection</strong> — Gleicher Fehler 3+ mal = Knowledge Gap</li>
|
| 940 |
+
<li style="padding:4px 0"><strong>Max 2 Reviews/Session</strong> — Nicht ueberfluten</li>
|
| 941 |
+
<li style="padding:4px 0"><strong>Persist</strong> — 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 — 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 <id> +/-</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 <sid></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">• Mechanismus-Ranking mit EWMA-Scores</li>
|
| 977 |
+
<li style="padding:3px 0">• Session-Stats (Prompts, Errors, Phase)</li>
|
| 978 |
+
<li style="padding:3px 0">• TTS-Log (Florian Spoken History)</li>
|
| 979 |
+
<li style="padding:3px 0">• Alignment-Digest (Bookmarks, Projects, Gaps)</li>
|
| 980 |
+
<li style="padding:3px 0">• Memory Explorer (Twin DB Eintraege)</li>
|
| 981 |
+
<li style="padding:3px 0">• Explainer-History (alle generierten Erklaerungen)</li>
|
| 982 |
+
<li style="padding:3px 0">• XP & 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">• Aktuelle Phase + Workflow</li>
|
| 990 |
+
<li style="padding:3px 0">• Letzte Nudge mit Rating-Widget (Emoji-Grid)</li>
|
| 991 |
+
<li style="padding:3px 0">• TTS-Stop bei Downvote</li>
|
| 992 |
+
<li style="padding:3px 0">• Bubble-Dismiss fuer unerwuenschte Popups</li>
|
| 993 |
+
<li style="padding:3px 0">• 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 · ~11K LOC · core/ (MIT) + samuel/ (privat) · Socket /tmp/sidecar-v7.sock · Dashboard :5112 · 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: /
|