pari-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. pari/__init__.py +3 -0
  2. pari/agent/__init__.py +14 -0
  3. pari/agent/loop.py +35 -0
  4. pari/agent/reflection.py +39 -0
  5. pari/agent/runtime.py +91 -0
  6. pari/agent/subagent.py +34 -0
  7. pari/cli.py +132 -0
  8. pari/config.py +23 -0
  9. pari/context/__init__.py +15 -0
  10. pari/context/cache.py +15 -0
  11. pari/context/chunks.py +40 -0
  12. pari/context/compaction.py +21 -0
  13. pari/context/epoch.py +32 -0
  14. pari/context/warmer.py +32 -0
  15. pari/edit/__init__.py +20 -0
  16. pari/edit/cascade.py +22 -0
  17. pari/edit/diff.py +8 -0
  18. pari/edit/dmp.py +15 -0
  19. pari/edit/exact.py +37 -0
  20. pari/edit/git_osr.py +14 -0
  21. pari/edit/indenter.py +12 -0
  22. pari/eval/__init__.py +14 -0
  23. pari/eval/judge.py +21 -0
  24. pari/eval/logging.py +19 -0
  25. pari/eval/metrics.py +22 -0
  26. pari/eval/runner.py +34 -0
  27. pari/fs/__init__.py +16 -0
  28. pari/fs/atomic.py +53 -0
  29. pari/fs/path.py +28 -0
  30. pari/fs/queue.py +22 -0
  31. pari/intelligence/__init__.py +15 -0
  32. pari/intelligence/ast.py +25 -0
  33. pari/intelligence/cache.py +21 -0
  34. pari/intelligence/ppr.py +21 -0
  35. pari/intelligence/repomap.py +21 -0
  36. pari/intelligence/search.py +33 -0
  37. pari/output/__init__.py +14 -0
  38. pari/output/parser.py +14 -0
  39. pari/output/spooler.py +33 -0
  40. pari/output/truncation.py +20 -0
  41. pari/providers/__init__.py +24 -0
  42. pari/providers/anthropic.py +23 -0
  43. pari/providers/base.py +56 -0
  44. pari/providers/deepseek.py +24 -0
  45. pari/providers/mock.py +46 -0
  46. pari/providers/ollama.py +25 -0
  47. pari/providers/openai.py +23 -0
  48. pari/routing/__init__.py +16 -0
  49. pari/routing/anti_verbosity.py +13 -0
  50. pari/routing/classifier.py +24 -0
  51. pari/routing/router.py +23 -0
  52. pari/sandbox/__init__.py +16 -0
  53. pari/sandbox/base.py +74 -0
  54. pari/sandbox/bwrap.py +39 -0
  55. pari/sandbox/dispatcher.py +31 -0
  56. pari/sandbox/e2b.py +32 -0
  57. pari/sandbox/process.py +38 -0
  58. pari/sandbox/seatbelt.py +32 -0
  59. pari/session/__init__.py +15 -0
  60. pari/session/checkpoint.py +24 -0
  61. pari/session/dag.py +34 -0
  62. pari/session/events.py +50 -0
  63. pari/session/logger.py +41 -0
  64. pari/session/undo.py +99 -0
  65. pari/tools/__init__.py +14 -0
  66. pari/tools/base.py +35 -0
  67. pari/tools/bash.py +24 -0
  68. pari/tools/fs.py +53 -0
  69. pari/tools/registry.py +36 -0
  70. pari/tools/search.py +38 -0
  71. pari/tui/__init__.py +28 -0
  72. pari/tui/app.py +355 -0
  73. pari/tui/commands.py +45 -0
  74. pari/tui/diff_view.py +17 -0
  75. pari/tui/status.py +65 -0
  76. pari_cli-0.1.0.dist-info/METADATA +167 -0
  77. pari_cli-0.1.0.dist-info/RECORD +81 -0
  78. pari_cli-0.1.0.dist-info/WHEEL +5 -0
  79. pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
  80. pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  81. pari_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,25 @@
1
+ """PARI Providers: Local Ollama Adapter.
2
+
3
+ Supports local models (Llama 3, Qwen 2.5 Coder, DeepSeek R1 distilled) via local HTTP endpoint.
4
+ Allows 100% offline, private coding agent execution without external cloud tokens.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+ from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
9
+
10
+
11
+ class OllamaProvider(LLMProvider):
12
+ """Adapter for locally hosted Ollama instances."""
13
+
14
+ def __init__(self, model_name: str = "qwen2.5-coder:7b", base_url: str = "http://localhost:11434/v1"):
15
+ super().__init__(model_name, api_key="ollama")
16
+ self.base_url = base_url
17
+
18
+ async def complete(
19
+ self,
20
+ messages: List[ProviderMessage],
21
+ tools: Optional[List[Dict[str, Any]]] = None,
22
+ temperature: float = 0.0,
23
+ ) -> ModelResponse:
24
+ """Call local Ollama endpoint."""
25
+ raise NotImplementedError
@@ -0,0 +1,23 @@
1
+ """PARI Providers: OpenAI Adapter.
2
+
3
+ Supports GPT-4o, o3-mini, and GPT-4.5 with function calling.
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional
7
+ from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
8
+
9
+
10
+ class OpenAIProvider(LLMProvider):
11
+ """Adapter for OpenAI Chat Completions API."""
12
+
13
+ def __init__(self, model_name: str = "gpt-4o", api_key: Optional[str] = None):
14
+ super().__init__(model_name, api_key)
15
+
16
+ async def complete(
17
+ self,
18
+ messages: List[ProviderMessage],
19
+ tools: Optional[List[Dict[str, Any]]] = None,
20
+ temperature: float = 0.0,
21
+ ) -> ModelResponse:
22
+ """Call OpenAI Chat Completions API."""
23
+ raise NotImplementedError
@@ -0,0 +1,16 @@
1
+ """PARI Layer 8: Dual-Model Routing Engine.
2
+
3
+ Architectural Purpose:
4
+ Harvested from Aider's architect_coder.py and closes Antigravity F1 (planning token burn).
5
+ Reduces per-turn output costs by ~85%:
6
+ - Architect Model (Claude 3.7 Sonnet / o3-mini): Ingests context and emits a dense,
7
+ high-signal 300-token plan.
8
+ - Editor Model (Claude 3.5 Haiku / DeepSeek V3): Ingests the plan and emits mechanical diffs.
9
+
10
+ Subsystems:
11
+ - router: Orchestrates the Architect-to-Editor pipeline
12
+ - classifier: Identifies simple/localized edits to bypass Architect overhead
13
+ - anti_verbosity: Enforces strict 4,096-token ceiling on reasoning to arrest planning loops
14
+ """
15
+
16
+ # Placeholder imports will be exported once implementations are added
@@ -0,0 +1,13 @@
1
+ """PARI Layer 8: Anti-Verbosity Guard.
2
+
3
+ Architectural Purpose:
4
+ Closes Google Antigravity F1 (200k-400k token burn on meta-planning).
5
+ Places a strict hard token ceiling (default 4,096 tokens) on the Architect model.
6
+ If the Architect fails to conclude its plan within 4k tokens, the output is cut
7
+ and dispatched immediately to the Editor model with instructions to execute
8
+ the partial plan, arresting recursive planning loops.
9
+ """
10
+
11
+ def enforce_planning_ceiling(plan_text: str, max_tokens: int = 4096) -> str:
12
+ """Enforce token ceiling on architect reasoning output."""
13
+ raise NotImplementedError
@@ -0,0 +1,24 @@
1
+ """PARI Layer 8: Task Classifier.
2
+
3
+ Architectural Purpose:
4
+ Evaluates incoming user prompts and active file scope.
5
+ If the task is simple (single file typo, localized test fix, doc update),
6
+ bypasses the Architect model entirely, saving both latency and token cost.
7
+ """
8
+
9
+ from enum import Enum
10
+ from typing import List
11
+
12
+
13
+ class TaskComplexity(str, Enum):
14
+ SIMPLE_EDIT = "simple_edit" # Bypass architect -> route directly to editor
15
+ ARCHITECTURAL = "architectural" # Dual-model: Architect plan -> Editor diff
16
+
17
+
18
+ class TaskClassifier:
19
+ """Classifies task complexity to select optimal routing pipeline."""
20
+
21
+ @staticmethod
22
+ def classify(prompt: str, active_files: List[str]) -> TaskComplexity:
23
+ """Classify task complexity based on intent keywords and scope."""
24
+ raise NotImplementedError
pari/routing/router.py ADDED
@@ -0,0 +1,23 @@
1
+ """PARI Layer 8: Dual-Model Router Pipeline.
2
+
3
+ Coordinates prompt classification, architect plan generation, and editor execution.
4
+ """
5
+
6
+ from typing import Any, Callable, Coroutine, List, Optional
7
+ from pari.routing.classifier import TaskClassifier, TaskComplexity
8
+
9
+
10
+ class DualModelRouter:
11
+ """Dispatches tasks across Architect and Editor model tiers."""
12
+
13
+ def __init__(
14
+ self,
15
+ architect_call: Optional[Callable[[str], Coroutine[Any, Any, str]]] = None,
16
+ editor_call: Optional[Callable[[str, Optional[str]], Coroutine[Any, Any, str]]] = None,
17
+ ):
18
+ self.architect_call = architect_call
19
+ self.editor_call = editor_call
20
+
21
+ async def route_and_execute(self, prompt: str, active_files: List[str]) -> str:
22
+ """Route prompt based on task complexity classification."""
23
+ raise NotImplementedError
@@ -0,0 +1,16 @@
1
+ """PARI Layer 0: Multi-Platform Kernel Sandboxing Engine.
2
+
3
+ Architectural Purpose:
4
+ Closes Claude Code F2, Pi F1, Aider F1, and OpenCode F1.
5
+ Prevents hallucinated or prompt-injected destructive commands (e.g., rm -rf /)
6
+ from executing with host-user privileges.
7
+
8
+ Subsystems:
9
+ - dispatcher: Automatically selects the most secure sandbox backend based on OS
10
+ - bwrap: Linux & Windows WSL2 bubblewrap container isolation (<5ms startup)
11
+ - seatbelt: macOS Apple Seatbelt (sandbox-exec + Scheme profile)
12
+ - e2b: Cloud microVM execution using Firecracker KVM
13
+ - process: Process-group supervision, die-with-parent lifecycle, and zombie reaping
14
+ """
15
+
16
+ # Placeholder imports will be exported once implementations are added
pari/sandbox/base.py ADDED
@@ -0,0 +1,74 @@
1
+ """PARI Layer 0: Base Sandbox Interface.
2
+
3
+ Defines the contract for all platform sandbox implementations (Bubblewrap, Seatbelt, E2B, ProcessGroup).
4
+ All commands executed by PARI must run through an implementation of SandboxRunner.
5
+
6
+ Guarantees:
7
+ 1. Working directory isolation: Writes restricted to authorized workspace tree.
8
+ 2. Die-with-parent lifecycle: Child processes killed when agent process terminates.
9
+ 3. Resource bounding: Execution constrained by timeout, max output bytes, and memory.
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Dict, Optional
16
+
17
+
18
+ @dataclass
19
+ class SandboxLimits:
20
+ """Resource constraints for sandboxed command execution.
21
+
22
+ Fields:
23
+ timeout_seconds: Maximum allowed wall-clock run duration.
24
+ max_output_bytes: Ceiling for stdout/stderr spooling.
25
+ memory_limit_mb: cgroup ceiling on Linux.
26
+ allow_network: Policy toggle for network interface access.
27
+ """
28
+ timeout_seconds: float = 30.0
29
+ max_output_bytes: int = 50 * 1024
30
+ memory_limit_mb: int = 2048
31
+ allow_network: bool = True
32
+
33
+
34
+ @dataclass
35
+ class SandboxResult:
36
+ """Structured result from sandboxed command execution.
37
+
38
+ Fields:
39
+ exit_code: Process return code.
40
+ stdout: Standard output string (truncated if exceeding limits).
41
+ stderr: Standard error string.
42
+ duration_seconds: Elapsed execution time.
43
+ timed_out: True if terminated by timeout.
44
+ truncated: True if output exceeded max_output_bytes.
45
+ """
46
+ exit_code: int
47
+ stdout: str
48
+ stderr: str
49
+ duration_seconds: float
50
+ timed_out: bool = False
51
+ truncated: bool = False
52
+
53
+
54
+ class SandboxRunner(ABC):
55
+ """Abstract base class for all PARI kernel and cloud sandboxes."""
56
+
57
+ def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
58
+ # TODO: Initialize workspace path and enforce resource limits
59
+ pass
60
+
61
+ @abstractmethod
62
+ def run_command(
63
+ self,
64
+ command: str,
65
+ env: Optional[Dict[str, str]] = None,
66
+ timeout: Optional[float] = None,
67
+ ) -> SandboxResult:
68
+ """Execute a shell command inside the sandbox."""
69
+ raise NotImplementedError
70
+
71
+ @abstractmethod
72
+ def cleanup(self) -> None:
73
+ """Terminate any lingering child processes, virtual mounts, or microVM instances."""
74
+ raise NotImplementedError
pari/sandbox/bwrap.py ADDED
@@ -0,0 +1,39 @@
1
+ """PARI Layer 0: Linux / WSL2 Bubblewrap Sandbox Runner.
2
+
3
+ Architectural Purpose:
4
+ Harvested from Codex codex-bwrap.
5
+ Provides sub-5ms unprivileged user namespace isolation (zero root required).
6
+ Mounts root / as read-only, workspace as read-write, masks ~/.ssh, ~/.gnupg, and
7
+ drops network namespaces when network isolation is requested.
8
+
9
+ Kernel Namespaces:
10
+ - CLONE_NEWUSER, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWIPC, CLONE_NEWUTS.
11
+ - Die-with-parent via bwrap --die-with-parent flag.
12
+ """
13
+
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional
16
+ from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
17
+
18
+
19
+ class BwrapSandboxRunner(SandboxRunner):
20
+ """Runs commands in an unprivileged Linux Bubblewrap (bwrap) sandbox."""
21
+
22
+ def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
23
+ super().__init__(workspace, limits)
24
+ # TODO: Verify 'bwrap' executable exists in PATH
25
+
26
+ def run_command(
27
+ self,
28
+ command: str,
29
+ env: Optional[Dict[str, str]] = None,
30
+ timeout: Optional[float] = None,
31
+ ) -> SandboxResult:
32
+ # TODO: Assemble bwrap CLI arguments:
33
+ # --die-with-parent, --ro-bind / /, --bind workspace workspace,
34
+ # mask sensitive directories (~/.ssh, ~/.gnupg), and spawn child process
35
+ raise NotImplementedError("Bwrap sandbox runner placeholder.")
36
+
37
+ def cleanup(self) -> None:
38
+ # TODO: Clean up any ephemeral mounts
39
+ pass
@@ -0,0 +1,31 @@
1
+ """PARI Layer 0: Sandbox Selection Dispatcher.
2
+
3
+ Architectural Purpose:
4
+ Intelligently inspects the host platform and environment configuration
5
+ to select the most secure available sandbox engine:
6
+ 1. Cloud mode -> E2B Firecracker MicroVM
7
+ 2. Linux / WSL2 with bwrap -> Bubblewrap runner
8
+ 3. macOS -> Apple Seatbelt runner
9
+ 4. Fallback / Native Windows -> Supervised Process Group runner
10
+ """
11
+
12
+ from pathlib import Path
13
+ from typing import Optional
14
+ from pari.sandbox.base import SandboxLimits, SandboxRunner
15
+
16
+
17
+ def get_sandbox_runner(
18
+ workspace: Path,
19
+ limits: Optional[SandboxLimits] = None,
20
+ force_mode: Optional[str] = None,
21
+ ) -> SandboxRunner:
22
+ """Select and instantiate the appropriate sandbox runner for the current platform.
23
+
24
+ TODO:
25
+ - Check force_mode or PARI_SANDBOX_MODE env var.
26
+ - If cloud -> E2BCloudSandboxRunner
27
+ - If Linux and bwrap in PATH -> BwrapSandboxRunner
28
+ - If macOS and sandbox-exec in PATH -> MacOSSeatbeltSandboxRunner
29
+ - Otherwise -> SupervisedProcessRunner fallback
30
+ """
31
+ raise NotImplementedError("Sandbox selection dispatcher placeholder.")
pari/sandbox/e2b.py ADDED
@@ -0,0 +1,32 @@
1
+ """PARI Layer 0: Cloud E2B Firecracker Sandbox Runner.
2
+
3
+ Architectural Purpose:
4
+ Satisfies cloud microVM sandboxing using E2B Firecracker hardware KVM microVMs.
5
+ Bypasses weeks of custom hypervisor / vsock plumbing while guaranteeing full
6
+ kernel-level hardware virtualization for remote evaluation and cloud runs.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Dict, Optional
11
+ from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
12
+
13
+
14
+ class E2BCloudSandboxRunner(SandboxRunner):
15
+ """Executes commands inside an ephemeral E2B Firecracker Cloud microVM."""
16
+
17
+ def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None, api_key: Optional[str] = None):
18
+ super().__init__(workspace, limits)
19
+ # TODO: Initialize E2B Sandbox client with api_key
20
+
21
+ def run_command(
22
+ self,
23
+ command: str,
24
+ env: Optional[Dict[str, str]] = None,
25
+ timeout: Optional[float] = None,
26
+ ) -> SandboxResult:
27
+ # TODO: Execute command remotely inside Firecracker microVM
28
+ raise NotImplementedError("E2B cloud sandbox runner placeholder.")
29
+
30
+ def cleanup(self) -> None:
31
+ # TODO: Close remote microVM instance
32
+ pass
@@ -0,0 +1,38 @@
1
+ """PARI Layer 0: Process Group Supervisor & Fallback Runner.
2
+
3
+ Architectural Purpose:
4
+ Closes Codex F5 (Orphaned exec-server processes) and provides the supervised
5
+ fallback runner when kernel sandbox utilities (bwrap/seatbelt) are absent.
6
+
7
+ Features:
8
+ - Creates independent process groups using os.setsid() so child process trees
9
+ can be terminated cleanly as a unit (killpg).
10
+ - Sets PR_SET_PDEATHSIG on Linux so child processes terminate if the parent crashes.
11
+ - Prevents zombie process accumulation via signal handling and reaping.
12
+ """
13
+
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional
16
+ from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
17
+
18
+
19
+ class SupervisedProcessRunner(SandboxRunner):
20
+ """Supervised process runner using process-group tracking and path-guard boundaries."""
21
+
22
+ def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
23
+ super().__init__(workspace, limits)
24
+ # TODO: Initialize active PID tracking list
25
+
26
+ def run_command(
27
+ self,
28
+ command: str,
29
+ env: Optional[Dict[str, str]] = None,
30
+ timeout: Optional[float] = None,
31
+ ) -> SandboxResult:
32
+ # TODO: Execute command using subprocess.Popen with preexec_fn=os.setsid
33
+ # and enforce die-with-parent lifecycle + timeout handling
34
+ raise NotImplementedError("Supervised process runner placeholder.")
35
+
36
+ def cleanup(self) -> None:
37
+ # TODO: Terminate all recorded active process groups
38
+ pass
@@ -0,0 +1,32 @@
1
+ """PARI Layer 0: macOS Apple Seatbelt Sandbox Runner.
2
+
3
+ Architectural Purpose:
4
+ Uses macOS sandbox-exec with an explicit TinyScheme profile.
5
+ Confines filesystem write operations strictly to the workspace directory.
6
+ Note: Expands explicit user home path since Seatbelt TinyScheme does not expand tilde (~).
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Dict, Optional
11
+ from pari.sandbox.base import SandboxLimits, SandboxResult, SandboxRunner
12
+
13
+
14
+ class MacOSSeatbeltSandboxRunner(SandboxRunner):
15
+ """Runs commands in an Apple Seatbelt sandbox via sandbox-exec."""
16
+
17
+ def __init__(self, workspace: Path, limits: Optional[SandboxLimits] = None):
18
+ super().__init__(workspace, limits)
19
+ # TODO: Verify 'sandbox-exec' exists in PATH
20
+
21
+ def run_command(
22
+ self,
23
+ command: str,
24
+ env: Optional[Dict[str, str]] = None,
25
+ timeout: Optional[float] = None,
26
+ ) -> SandboxResult:
27
+ # TODO: Generate TinyScheme profile restricting writes to workspace and /tmp,
28
+ # denying ~/.ssh, ~/.aws, ~/.gnupg access, and execute via sandbox-exec -f
29
+ raise NotImplementedError("macOS Seatbelt sandbox runner placeholder.")
30
+
31
+ def cleanup(self) -> None:
32
+ pass
@@ -0,0 +1,15 @@
1
+ """PARI Layer 1: Durable Session Engine (Merkle DAG).
2
+
3
+ Architectural Purpose:
4
+ Closes Claude Code F1 (No ACID state durability; 0-byte corrupt session file on SIGINT)
5
+ and Pi F4 / F5 (O(N) JSONL replay and mid-append torn-write truncation).
6
+
7
+ Subsystems:
8
+ - events: Structured session event records (SESSION_START, USER_PROMPT, TOOL_CALL, etc.)
9
+ - logger: Append-only JSONL writer with CRC32 integrity check & torn-write recovery
10
+ - dag: Merkle DAG node tracking, parentId linking, and branch reconstruction
11
+ - checkpoint: Periodic index snapshots (every 50 turns) for O(1) state recovery
12
+ - undo: Bounded, guarded file rollback + child process termination + side effect report
13
+ """
14
+
15
+ # Placeholder imports will be exported once implementations are added
@@ -0,0 +1,24 @@
1
+ """PARI Layer 1: Checkpoint Index Manager.
2
+
3
+ Architectural Purpose:
4
+ Closes Pi F4 (O(N) JSONL replay latency on deep 500+ turn sessions).
5
+ Periodically writes a CHECKPOINT index event (every 50 turns) containing the
6
+ active context state, file snapshot manifests, and token metrics.
7
+ Allows resuming a session by reading backward to the most recent checkpoint.
8
+ """
9
+
10
+ from typing import Any, Dict, Optional
11
+ from pari.session.logger import SessionLogger
12
+
13
+
14
+ class CheckpointManager:
15
+ """Manages periodic checkpoint intervals for fast session resumption."""
16
+
17
+ def __init__(self, logger: SessionLogger, interval_turns: int = 50):
18
+ self.logger = logger
19
+ self.interval_turns = interval_turns
20
+ # TODO: Track turn count
21
+
22
+ def record_turn(self, state_snapshot: Dict[str, Any]) -> Optional[str]:
23
+ """Increment turn counter and trigger checkpoint write if interval is met."""
24
+ raise NotImplementedError
pari/session/dag.py ADDED
@@ -0,0 +1,34 @@
1
+ """PARI Layer 1: Merkle DAG Session Graph.
2
+
3
+ Architectural Purpose:
4
+ Harvested from Pi's session-manager.ts.
5
+ Maintains session history as a Directed Acyclic Graph (DAG) using parentId references.
6
+ Enables non-linear branching, checkpoints, and causal traversal without in-memory state loss.
7
+ """
8
+
9
+ from typing import Dict, List, Optional
10
+ from pari.session.events import SessionEvent
11
+
12
+
13
+ class DAGNode:
14
+ """Represents a node in the causal event Merkle DAG."""
15
+ def __init__(self, event: SessionEvent):
16
+ self.event = event
17
+ self.parent: Optional["DAGNode"] = None
18
+ self.children: List["DAGNode"] = []
19
+
20
+
21
+ class SessionDAG:
22
+ """In-memory DAG index over the immutable session event stream."""
23
+
24
+ def __init__(self):
25
+ # TODO: Initialize node map and head/root pointers
26
+ pass
27
+
28
+ def add_event(self, event: SessionEvent) -> DAGNode:
29
+ """Insert event into DAG and link parent/child pointers."""
30
+ raise NotImplementedError
31
+
32
+ def get_lineage(self, leaf_id: Optional[str] = None) -> List[SessionEvent]:
33
+ """Traverse backwards from leaf to root to reconstruct the linear event thread."""
34
+ raise NotImplementedError
pari/session/events.py ADDED
@@ -0,0 +1,50 @@
1
+ """PARI Layer 1: Structured Session Event Types.
2
+
3
+ Defines all immutable event records logged into the append-only Merkle DAG.
4
+ Each event possesses a unique record ID, timestamp, and an optional parent_id
5
+ forming an unbroken causal Merkle chain.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ class SessionEventType(str, Enum):
14
+ SESSION_START = "session_start"
15
+ USER_PROMPT = "user_prompt"
16
+ ASSISTANT_RESPONSE = "assistant_response"
17
+ TOOL_CALL = "tool_call"
18
+ TOOL_RESULT = "tool_result"
19
+ FILE_SNAPSHOT = "file_snapshot"
20
+ CHECKPOINT = "checkpoint"
21
+ SESSION_END = "session_end"
22
+
23
+
24
+ @dataclass
25
+ class SessionEvent:
26
+ """An individual event entry in the session Merkle DAG.
27
+
28
+ Fields:
29
+ type: Event category
30
+ payload: Event data dictionary
31
+ id: Unique UUID identifier
32
+ parent_id: ID of preceding causal event
33
+ timestamp: Epoch timestamp
34
+ crc32: Integrity checksum
35
+ """
36
+ type: SessionEventType
37
+ payload: Dict[str, Any]
38
+ id: Optional[str] = None
39
+ parent_id: Optional[str] = None
40
+ timestamp: float = 0.0
41
+ crc32: Optional[int] = None
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ """Convert event to serializable dictionary."""
45
+ raise NotImplementedError
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: Dict[str, Any]) -> "SessionEvent":
49
+ """Reconstruct event from serialized dictionary."""
50
+ raise NotImplementedError
pari/session/logger.py ADDED
@@ -0,0 +1,41 @@
1
+ """PARI Layer 1: Append-Only JSONL Session Logger.
2
+
3
+ Architectural Purpose:
4
+ Closes Claude Code F1 & Pi F5.
5
+ Writes every session event with an explicit CRC32 checksum, flushes to OS buffer,
6
+ and calls os.fsync() on critical milestones.
7
+ On startup, scans backward from EOF to automatically detect and truncate torn writes
8
+ caused by power loss or sudden SIGKILL.
9
+ """
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Iterator, List, Optional
13
+ from pari.session.events import SessionEvent, SessionEventType
14
+
15
+
16
+ class SessionLogger:
17
+ """Manages an append-only JSONL session log with CRC32 integrity verification."""
18
+
19
+ def __init__(self, session_file: Path):
20
+ self.session_file = session_file
21
+ # TODO: Initialize file path and perform backward recovery scan for torn writes
22
+
23
+ def _recover_and_index(self) -> None:
24
+ """Scan session file, recover from torn writes at EOF, and locate the last record ID."""
25
+ raise NotImplementedError("Torn write recovery placeholder.")
26
+
27
+ def log(self, event_type: SessionEventType, payload: Dict[str, Any]) -> SessionEvent:
28
+ """Append a new verified session event to disk with CRC32 checksum."""
29
+ raise NotImplementedError("Session logging placeholder.")
30
+
31
+ def append_event(self, event_type: str, data: Dict[str, Any]) -> str:
32
+ """Legacy append API."""
33
+ raise NotImplementedError
34
+
35
+ def read_events(self) -> List[Dict[str, Any]]:
36
+ """Read all verified records from the session file."""
37
+ raise NotImplementedError
38
+
39
+ def iter_events(self) -> Iterator[SessionEvent]:
40
+ """Iterate over all valid events in chronological order."""
41
+ raise NotImplementedError
pari/session/undo.py ADDED
@@ -0,0 +1,99 @@
1
+ """PARI Layer 1: Guarded Undo Manager.
2
+
3
+ Architectural Purpose:
4
+ Implements a safe, bounded /undo mechanism:
5
+ 1. Restores files modified by the most recent turn.
6
+ 2. Guarded check: If the developer manually edited the file after PARI touched it,
7
+ aborts or prompts to prevent accidental overwrite of user work.
8
+ 3. Terminates background processes started during the turn.
9
+ 4. Explicit disclosure: Informs user that external side effects (API calls, remote DBs,
10
+ pip install) are logged but not automatically reversible.
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import List
16
+
17
+
18
+ @dataclass
19
+ class FileSnapshot:
20
+ """Pre- and post-edit file hash snapshot."""
21
+ rel_path: str
22
+ pre_edit_content: str
23
+ pre_edit_hash: str
24
+ post_edit_hash: str
25
+
26
+
27
+ @dataclass
28
+ class UndoResult:
29
+ """Outcome of guarded undo execution."""
30
+ reverted_files: List[str]
31
+ conflicted_files: List[str]
32
+ terminated_processes: int
33
+ disclosed_side_effects: List[str]
34
+
35
+
36
+ import hashlib
37
+ from typing import Dict, List
38
+ from pari.fs.atomic import atomic_write, read_file
39
+ from pari.fs.path import resolve_safe_path
40
+
41
+
42
+ class GuardedUndoManager:
43
+ """Provides guarded rollback for physical file changes and process termination."""
44
+
45
+ def __init__(self, workspace: Path):
46
+ self.workspace = workspace.resolve()
47
+ self.snapshots: Dict[str, FileSnapshot] = {}
48
+ self.side_effects: List[str] = []
49
+
50
+ def _hash(self, content: str) -> str:
51
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
52
+
53
+ def record_snapshot(self, rel_path: str, pre_content: str, post_content: str):
54
+ """Record file state prior to write mutation."""
55
+ self.snapshots[rel_path] = FileSnapshot(
56
+ rel_path=rel_path,
57
+ pre_edit_content=pre_content,
58
+ pre_edit_hash=self._hash(pre_content),
59
+ post_edit_hash=self._hash(post_content),
60
+ )
61
+
62
+ def record_side_effect(self, description: str):
63
+ """Log external irreversible side effect (e.g. package install, DB migration)."""
64
+ self.side_effects.append(description)
65
+
66
+ def undo(self, force: bool = False) -> UndoResult:
67
+ """Execute guarded rollback verifying files haven't drifted."""
68
+ reverted: List[str] = []
69
+ conflicts: List[str] = []
70
+
71
+ for rel_path, snapshot in list(self.snapshots.items()):
72
+ file_path = resolve_safe_path(self.workspace, rel_path)
73
+ if not file_path.exists():
74
+ atomic_write(file_path, snapshot.pre_edit_content)
75
+ reverted.append(rel_path)
76
+ continue
77
+
78
+ current_content = read_file(file_path)
79
+ current_hash = self._hash(current_content)
80
+
81
+ if current_hash == snapshot.post_edit_hash or force:
82
+ atomic_write(file_path, snapshot.pre_edit_content)
83
+ reverted.append(rel_path)
84
+ else:
85
+ conflicts.append(rel_path)
86
+
87
+ # Clear reverted snapshots
88
+ for rel in reverted:
89
+ self.snapshots.pop(rel, None)
90
+
91
+ result = UndoResult(
92
+ reverted_files=reverted,
93
+ conflicted_files=conflicts,
94
+ terminated_processes=0,
95
+ disclosed_side_effects=list(self.side_effects),
96
+ )
97
+ self.side_effects.clear()
98
+ return result
99
+