algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,806 @@
1
+ """Run-scoped execution evidence and fail-closed file-write guardrails.
2
+
3
+ The ledger deliberately stores only structural metadata. File contents, shell
4
+ commands, and command output are never retained here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import os
11
+ import re
12
+ import shlex
13
+ from contextvars import ContextVar, Token
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Callable, Iterable, Literal, Sequence
17
+
18
+
19
+ EvidenceKind = Literal["read", "mutation", "verification"]
20
+ VerificationKind = Literal["same_path_reread", "git_diff", "test", "lint"]
21
+
22
+ _PATH_MUTATION_OPERATIONS = frozenset({"write_file", "edit_file", "batch_edit"})
23
+ _MUTATION_OPERATIONS = frozenset({*_PATH_MUTATION_OPERATIONS, "run_shell"})
24
+ _READ_OPERATIONS = frozenset({"read_file"})
25
+ _VERIFICATION_KINDS = frozenset({"git_diff", "test", "lint"})
26
+
27
+ _SENSITIVE_COMPONENTS = frozenset(
28
+ {
29
+ ".git",
30
+ ".hg",
31
+ ".svn",
32
+ ".ssh",
33
+ ".gnupg",
34
+ ".aws",
35
+ ".azure",
36
+ ".kube",
37
+ ".docker",
38
+ }
39
+ )
40
+ _SENSITIVE_FILENAMES = frozenset(
41
+ {
42
+ ".env",
43
+ ".netrc",
44
+ "_netrc",
45
+ "credentials",
46
+ "credentials.json",
47
+ "secrets.json",
48
+ "service-account.json",
49
+ "service_account.json",
50
+ "token.json",
51
+ "id_rsa",
52
+ "id_dsa",
53
+ "id_ecdsa",
54
+ "id_ed25519",
55
+ }
56
+ )
57
+ _SENSITIVE_SUFFIXES = frozenset(
58
+ {".pem", ".key", ".p12", ".pfx", ".jks", ".keystore", ".kdbx"}
59
+ )
60
+ _CONTROL_OPERATOR_RE = re.compile(
61
+ r"(?:\r|\n|;|&&|\|\||(?<!\|)\|(?!\|)|(?<!&)\&(?!&)|`|\$\(|[<>])"
62
+ )
63
+ _ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$", re.DOTALL)
64
+ _RUNNER_OPTIONS_WITH_VALUE = frozenset(
65
+ {
66
+ "--directory",
67
+ "--env-file",
68
+ "--extra",
69
+ "--group",
70
+ "--index",
71
+ "--project",
72
+ "--python",
73
+ "--with",
74
+ }
75
+ )
76
+ _VERIFIER_SCRIPT_RE = re.compile(
77
+ r"^(?:(?:health|smoke)[_-]?check|check(?:[_-].*)?|verify(?:[_-].*)?|"
78
+ r"test(?:[_-].*)?|.*[_-]test)\.(?:py|sh|ps1|js|ts)$",
79
+ re.IGNORECASE,
80
+ )
81
+
82
+
83
+ class ExecutionGuardrailError(RuntimeError):
84
+ """Raised when execution evidence cannot be handled safely."""
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class EvidenceEvent:
89
+ """One successful, content-free event in execution order."""
90
+
91
+ sequence: int
92
+ kind: EvidenceKind
93
+ operation: str
94
+ relative_path: str | None = None
95
+ verification_kind: str | None = None
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class PathDecision:
100
+ """Decision for a prospective model-controlled file write."""
101
+
102
+ allowed: bool
103
+ reason: str
104
+ resolved_path: Path | None = None
105
+ relative_path: str | None = None
106
+ sensitive: bool = False
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ReadBeforeEditDecision:
111
+ """Whether an edit has a fresh successful read in the current run."""
112
+
113
+ allowed: bool
114
+ reason: str
115
+ read_sequence: int | None = None
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class VerificationCommand:
120
+ """Content-free classification of a shell verification command."""
121
+
122
+ qualifies: bool
123
+ kind: str | None
124
+ reason: str
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class CompletionDecision:
129
+ """Whether execution evidence supports a successful completion claim."""
130
+
131
+ allowed: bool
132
+ reason: str
133
+ last_mutation_sequence: int | None = None
134
+ verifier_sequence: int | None = None
135
+ verifier_kind: str | None = None
136
+
137
+
138
+ @dataclass
139
+ class _ExecutionLedger:
140
+ workspace: Path
141
+ events: list[EvidenceEvent] = field(default_factory=list)
142
+ closed: bool = False
143
+
144
+ def append(
145
+ self,
146
+ kind: EvidenceKind,
147
+ operation: str,
148
+ *,
149
+ relative_path: str | None = None,
150
+ verification_kind: str | None = None,
151
+ ) -> EvidenceEvent:
152
+ if self.closed:
153
+ raise ExecutionGuardrailError("execution scope is closed")
154
+ event = EvidenceEvent(
155
+ sequence=len(self.events) + 1,
156
+ kind=kind,
157
+ operation=operation,
158
+ relative_path=relative_path,
159
+ verification_kind=verification_kind,
160
+ )
161
+ self.events.append(event)
162
+ return event
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class ExecutionScope:
167
+ """Opaque handle returned by :func:`begin_execution_scope`."""
168
+
169
+ workspace: Path
170
+ _ledger: _ExecutionLedger
171
+ _token: Token[_ExecutionLedger | None]
172
+
173
+
174
+ _ACTIVE_LEDGER: ContextVar[_ExecutionLedger | None] = ContextVar(
175
+ "algo_cli_execution_evidence_ledger",
176
+ default=None,
177
+ )
178
+
179
+
180
+ def begin_execution_scope(workspace: str | Path) -> ExecutionScope:
181
+ """Begin a run-local evidence scope rooted at an existing directory."""
182
+
183
+ try:
184
+ resolved_workspace = Path(workspace).expanduser().resolve(strict=True)
185
+ except (OSError, RuntimeError, TypeError, ValueError) as exc:
186
+ raise ExecutionGuardrailError("workspace cannot be resolved safely") from exc
187
+ if not resolved_workspace.is_dir():
188
+ raise ExecutionGuardrailError("workspace must be an existing directory")
189
+ if resolved_workspace.parent == resolved_workspace:
190
+ raise ExecutionGuardrailError("filesystem root cannot be used as a workspace")
191
+ ledger = _ExecutionLedger(workspace=resolved_workspace)
192
+ token = _ACTIVE_LEDGER.set(ledger)
193
+ return ExecutionScope(resolved_workspace, ledger, token)
194
+
195
+
196
+ def end_execution_scope(scope: ExecutionScope) -> tuple[EvidenceEvent, ...]:
197
+ """Close *scope*, restore any parent scope, and return immutable evidence."""
198
+
199
+ active = _ACTIVE_LEDGER.get()
200
+ if active is not scope._ledger or active.closed:
201
+ raise ExecutionGuardrailError("execution scopes must be ended once and in order")
202
+ snapshot = tuple(active.events)
203
+ try:
204
+ _ACTIVE_LEDGER.reset(scope._token)
205
+ except (RuntimeError, ValueError) as exc:
206
+ raise ExecutionGuardrailError("execution scope belongs to a different context") from exc
207
+ active.closed = True
208
+ return snapshot
209
+
210
+
211
+ def evidence_snapshot() -> tuple[EvidenceEvent, ...]:
212
+ """Return an immutable snapshot of the active scope's evidence."""
213
+
214
+ ledger = _ACTIVE_LEDGER.get()
215
+ return tuple(ledger.events) if ledger is not None and not ledger.closed else ()
216
+
217
+
218
+ def active_workspace() -> Path | None:
219
+ """Return the active canonical workspace, if a scope is open."""
220
+
221
+ ledger = _ACTIVE_LEDGER.get()
222
+ return ledger.workspace if ledger is not None and not ledger.closed else None
223
+
224
+
225
+ def is_sensitive_path(path: str | Path) -> bool:
226
+ """Return whether a path names credential, VCS, or private key material."""
227
+
228
+ try:
229
+ normalized = str(path).replace("\\", "/")
230
+ parts = tuple(part.casefold() for part in Path(normalized).parts if part not in {"", "."})
231
+ except (OSError, RuntimeError, TypeError, ValueError):
232
+ return True
233
+ if not parts:
234
+ return True
235
+ if any(part in _SENSITIVE_COMPONENTS for part in parts):
236
+ return True
237
+ if any(part in _SENSITIVE_FILENAMES or part.startswith(".env.") for part in parts):
238
+ return True
239
+ name = parts[-1]
240
+ return any(name.endswith(suffix) for suffix in _SENSITIVE_SUFFIXES)
241
+
242
+
243
+ def assess_resolved_write_path(
244
+ resolved_workspace: Path,
245
+ resolved_target: Path,
246
+ *,
247
+ target_is_directory: bool = False,
248
+ ) -> PathDecision:
249
+ """Pure containment decision for paths already resolved by a caller/probe."""
250
+
251
+ if not isinstance(resolved_workspace, Path) or not isinstance(resolved_target, Path):
252
+ return PathDecision(False, "resolved paths are invalid")
253
+ if not resolved_workspace.is_absolute() or not resolved_target.is_absolute():
254
+ return PathDecision(False, "resolved paths must be absolute")
255
+ if resolved_workspace.parent == resolved_workspace:
256
+ return PathDecision(False, "filesystem root cannot be used as a workspace")
257
+ try:
258
+ relative = resolved_target.relative_to(resolved_workspace)
259
+ except (TypeError, ValueError):
260
+ return PathDecision(False, "target escapes the workspace")
261
+ relative_text = relative.as_posix()
262
+ if relative_text in {"", "."}:
263
+ return PathDecision(False, "target must name a file")
264
+ if ".." in relative.parts:
265
+ return PathDecision(False, "target escapes the workspace")
266
+ if target_is_directory:
267
+ return PathDecision(False, "target is a directory")
268
+ if is_sensitive_path(relative):
269
+ return PathDecision(
270
+ False,
271
+ "target is a sensitive path",
272
+ resolved_path=resolved_target,
273
+ relative_path=relative_text,
274
+ sensitive=True,
275
+ )
276
+ return PathDecision(
277
+ True,
278
+ "target is contained in the workspace",
279
+ resolved_path=resolved_target,
280
+ relative_path=relative_text,
281
+ )
282
+
283
+
284
+ def assess_write_path(
285
+ workspace: str | Path,
286
+ path: str | Path,
287
+ *,
288
+ resolve_probe: Callable[[Path, bool], Path] | None = None,
289
+ is_directory_probe: Callable[[Path], bool] | None = None,
290
+ ) -> PathDecision:
291
+ """Resolve and assess a prospective write without performing the write.
292
+
293
+ The optional probes make resolution and directory checks deterministic in
294
+ tests or alternate runtimes. Resolution failures always deny the write.
295
+ """
296
+
297
+ def default_resolve(candidate: Path, strict: bool) -> Path:
298
+ return candidate.resolve(strict=strict)
299
+
300
+ resolver = resolve_probe or default_resolve
301
+ directory_probe = is_directory_probe or Path.is_dir
302
+ try:
303
+ workspace_input = Path(workspace).expanduser()
304
+ resolved_workspace = resolver(workspace_input, True)
305
+ if not directory_probe(resolved_workspace):
306
+ return PathDecision(False, "workspace is not a directory")
307
+ if resolved_workspace.parent == resolved_workspace:
308
+ return PathDecision(False, "filesystem root cannot be used as a workspace")
309
+ target_input = Path(path).expanduser()
310
+ if not target_input.is_absolute():
311
+ target_input = resolved_workspace / target_input
312
+ resolved_target = resolver(target_input, False)
313
+ target_is_directory = directory_probe(resolved_target)
314
+ except (OSError, RuntimeError, TypeError, ValueError):
315
+ return PathDecision(False, "path cannot be resolved safely")
316
+ return assess_resolved_write_path(
317
+ resolved_workspace,
318
+ resolved_target,
319
+ target_is_directory=target_is_directory,
320
+ )
321
+
322
+
323
+ def assess_active_write_path(path: str | Path) -> PathDecision:
324
+ """Assess a path against the active run workspace, failing closed."""
325
+
326
+ workspace = active_workspace()
327
+ if workspace is None:
328
+ return PathDecision(False, "no active execution scope")
329
+ return assess_write_path(workspace, path)
330
+
331
+
332
+ def _valid_evidence(events: Sequence[EvidenceEvent]) -> bool:
333
+ try:
334
+ expected = 1
335
+ for event in events:
336
+ if event.sequence != expected:
337
+ return False
338
+ expected += 1
339
+ if event.kind == "read":
340
+ if event.operation not in _READ_OPERATIONS or not event.relative_path or event.verification_kind:
341
+ return False
342
+ elif event.kind == "mutation":
343
+ if event.operation not in _MUTATION_OPERATIONS or not event.relative_path or event.verification_kind:
344
+ return False
345
+ elif event.kind == "verification":
346
+ if (
347
+ event.operation != "verification"
348
+ or event.relative_path
349
+ or event.verification_kind not in _VERIFICATION_KINDS
350
+ ):
351
+ return False
352
+ else:
353
+ return False
354
+ except (AttributeError, TypeError):
355
+ return False
356
+ return True
357
+
358
+
359
+ def evaluate_read_before_edit(
360
+ relative_path: str,
361
+ events: Sequence[EvidenceEvent],
362
+ ) -> ReadBeforeEditDecision:
363
+ """Pure decision requiring a read newer than any mutation of the file."""
364
+
365
+ if not relative_path or not _valid_evidence(events):
366
+ return ReadBeforeEditDecision(False, "execution evidence is invalid")
367
+ reads = [event for event in events if event.kind == "read" and event.relative_path == relative_path]
368
+ if not reads:
369
+ return ReadBeforeEditDecision(False, "edit requires a successful same-file read")
370
+ last_read = reads[-1]
371
+ mutations = [
372
+ event
373
+ for event in events
374
+ if event.kind == "mutation" and event.relative_path in {relative_path, "."}
375
+ ]
376
+ if mutations and mutations[-1].sequence > last_read.sequence:
377
+ return ReadBeforeEditDecision(False, "edit requires a fresh read after the previous mutation")
378
+ return ReadBeforeEditDecision(True, "same-file read evidence is fresh", last_read.sequence)
379
+
380
+
381
+ def read_before_edit_decision(path: str | Path) -> ReadBeforeEditDecision:
382
+ """Assess active-scope read evidence for a prospective edit."""
383
+
384
+ path_decision = assess_active_write_path(path)
385
+ if not path_decision.allowed or not path_decision.relative_path:
386
+ return ReadBeforeEditDecision(False, path_decision.reason)
387
+ return evaluate_read_before_edit(path_decision.relative_path, evidence_snapshot())
388
+
389
+
390
+ def _append_path_event(
391
+ kind: Literal["read", "mutation"],
392
+ path: str | Path,
393
+ *,
394
+ operation: str,
395
+ success: bool,
396
+ ) -> EvidenceEvent | None:
397
+ if not success:
398
+ return None
399
+ ledger = _ACTIVE_LEDGER.get()
400
+ if ledger is None or ledger.closed:
401
+ return None
402
+ allowed_operations = _READ_OPERATIONS if kind == "read" else _PATH_MUTATION_OPERATIONS
403
+ if operation not in allowed_operations:
404
+ return None
405
+ decision = assess_write_path(ledger.workspace, path)
406
+ if not decision.allowed or not decision.relative_path:
407
+ return None
408
+ return ledger.append(kind, operation, relative_path=decision.relative_path)
409
+
410
+
411
+ def record_read(
412
+ path: str | Path,
413
+ *,
414
+ success: bool,
415
+ operation: str = "read_file",
416
+ ) -> EvidenceEvent | None:
417
+ """Record only a successful, contained, non-sensitive file read."""
418
+
419
+ return _append_path_event("read", path, operation=operation, success=success)
420
+
421
+
422
+ def record_mutation(
423
+ path: str | Path,
424
+ *,
425
+ success: bool,
426
+ operation: str,
427
+ ) -> EvidenceEvent | None:
428
+ """Record only a successful allowed model file mutation."""
429
+
430
+ return _append_path_event("mutation", path, operation=operation, success=success)
431
+
432
+
433
+ def record_workspace_mutation(
434
+ *,
435
+ success: bool,
436
+ operation: str = "run_shell",
437
+ ) -> EvidenceEvent | None:
438
+ """Record a successful workspace-wide mutation without retaining its command."""
439
+
440
+ if not success or operation != "run_shell":
441
+ return None
442
+ ledger = _ACTIVE_LEDGER.get()
443
+ if ledger is None or ledger.closed:
444
+ return None
445
+ return ledger.append("mutation", operation, relative_path=".")
446
+
447
+
448
+ def _shell_split(command: str, *, platform_name: str | None = None) -> list[str]:
449
+ """Split one shell command using the quoting rules of the target platform."""
450
+
451
+ platform = os.name if platform_name is None else platform_name
452
+ tokens = shlex.split(command, posix=platform != "nt")
453
+ if platform == "nt":
454
+ # ``cmd.exe`` uses double quotes for grouping and removes the outer
455
+ # pair before invoking Python. Single quotes are literal characters.
456
+ tokens = [
457
+ token[1:-1]
458
+ if len(token) >= 2 and token.startswith('"') and token.endswith('"')
459
+ else token
460
+ for token in tokens
461
+ ]
462
+ return tokens
463
+
464
+
465
+ def _command_tokens(command: str) -> list[str] | None:
466
+ if not command.strip() or _CONTROL_OPERATOR_RE.search(command):
467
+ return None
468
+ try:
469
+ tokens = _shell_split(command)
470
+ except ValueError:
471
+ return None
472
+ while os.name != "nt" and tokens and _ENV_ASSIGNMENT_RE.match(tokens[0]):
473
+ tokens.pop(0)
474
+ return tokens or None
475
+
476
+
477
+ def _unwrap_verification_shell(command: str) -> str:
478
+ """Remove narrowly safe shell framing around a verifier command.
479
+
480
+ Models commonly express a working directory in-band even though
481
+ ``run_shell`` also has a structured ``cwd`` argument. Accept one leading
482
+ ``cd PATH &&`` and one trailing ``2>&1`` while leaving every other shell
483
+ operator for the fail-closed parser to reject.
484
+ """
485
+
486
+ candidate = command.strip()
487
+ candidate = re.sub(r"\s+2\s*>\s*&\s*1\s*$", "", candidate).strip()
488
+ if "&&" not in candidate:
489
+ return candidate
490
+ parts = candidate.split("&&")
491
+ if len(parts) != 2:
492
+ return candidate
493
+ try:
494
+ prefix = _shell_split(parts[0].strip())
495
+ except ValueError:
496
+ return candidate
497
+ if len(prefix) == 2 and _executable_name(prefix[0]) == "cd" and prefix[1]:
498
+ return parts[1].strip()
499
+ if len(prefix) == 3 and _executable_name(prefix[0]) == "cd" and prefix[1] == "--" and prefix[2]:
500
+ return parts[1].strip()
501
+ return candidate
502
+
503
+
504
+ def _executable_name(token: str) -> str:
505
+ name = token.strip("\"'").replace("\\", "/").rsplit("/", 1)[-1].casefold()
506
+ for suffix in (".exe", ".cmd", ".bat", ".ps1"):
507
+ if name.endswith(suffix):
508
+ return name[: -len(suffix)]
509
+ return name
510
+
511
+
512
+ def _strip_runner(tokens: list[str]) -> list[str]:
513
+ first = _executable_name(tokens[0])
514
+ if first == "env":
515
+ tokens = tokens[1:]
516
+ while tokens and (_ENV_ASSIGNMENT_RE.match(tokens[0]) or tokens[0].startswith("-")):
517
+ tokens = tokens[1:]
518
+ return _strip_runner(tokens) if tokens else []
519
+ if first in {"uv", "poetry", "pipenv"}:
520
+ try:
521
+ run_index = tokens.index("run")
522
+ except ValueError:
523
+ return []
524
+ nested = tokens[run_index + 1 :]
525
+ while nested and nested[0].startswith("-"):
526
+ raw_option = nested.pop(0)
527
+ option = raw_option.split("=", 1)[0]
528
+ if "=" not in raw_option and option in _RUNNER_OPTIONS_WITH_VALUE:
529
+ if not nested:
530
+ return []
531
+ nested.pop(0)
532
+ elif option not in _RUNNER_OPTIONS_WITH_VALUE and option not in {
533
+ "--all-extras",
534
+ "--exact",
535
+ "--frozen",
536
+ "--isolated",
537
+ "--locked",
538
+ "--no-project",
539
+ "--no-sync",
540
+ "--offline",
541
+ }:
542
+ return []
543
+ return _strip_runner(nested) if nested else []
544
+ return tokens
545
+
546
+
547
+ def _assertion_script_qualifies(source: str) -> bool:
548
+ """Return whether inline Python contains a fail-on-error check."""
549
+
550
+ try:
551
+ tree = ast.parse(source)
552
+ except SyntaxError:
553
+ return False
554
+ for node in ast.walk(tree):
555
+ if isinstance(node, ast.Assert):
556
+ return True
557
+ if (
558
+ isinstance(node, ast.Call)
559
+ and isinstance(node.func, ast.Attribute)
560
+ and isinstance(node.func.value, ast.Name)
561
+ and node.func.value.id == "sys"
562
+ and node.func.attr == "exit"
563
+ and node.args
564
+ and not (
565
+ isinstance(node.args[0], ast.Constant)
566
+ and node.args[0].value in {0, None}
567
+ )
568
+ ):
569
+ return True
570
+ return False
571
+
572
+
573
+ def _looks_like_verifier_script(token: str) -> bool:
574
+ return bool(_VERIFIER_SCRIPT_RE.fullmatch(_executable_name(token)))
575
+
576
+
577
+ def _inline_python_verification(
578
+ command: str,
579
+ *,
580
+ platform_name: str | None = None,
581
+ ) -> VerificationCommand | None:
582
+ """Classify a standalone Python ``-c`` check before newline rejection.
583
+
584
+ ``shlex`` preserves newlines inside the quoted source as one argument, so
585
+ multiline assertion scripts remain unambiguous while extra shell segments
586
+ produce additional tokens and are rejected.
587
+ """
588
+
589
+ platform = os.name if platform_name is None else platform_name
590
+ try:
591
+ tokens = _shell_split(command, platform_name=platform)
592
+ except ValueError:
593
+ return None
594
+ while platform != "nt" and tokens and _ENV_ASSIGNMENT_RE.match(tokens[0]):
595
+ tokens.pop(0)
596
+ if len(tokens) != 3 or not re.fullmatch(
597
+ r"python(?:\d+(?:\.\d+)*)?|py", _executable_name(tokens[0])
598
+ ) or tokens[1].casefold() != "-c":
599
+ return None
600
+ if _assertion_script_qualifies(tokens[2]):
601
+ return VerificationCommand(True, "test", "inline assertions execute a fail-on-error check")
602
+ return VerificationCommand(False, None, "inline Python lacks fail-on-error assertions")
603
+
604
+
605
+ def classify_verification_command(command: str) -> VerificationCommand:
606
+ """Classify one shell command without retaining its text.
607
+
608
+ Compound/piped commands and discovery-only flags are rejected because a
609
+ zero shell status would not reliably prove the verifier itself passed. A
610
+ single working-directory wrapper and stderr-to-stdout redirect are allowed
611
+ because they preserve the verifier's exit status.
612
+ """
613
+
614
+ unwrapped = _unwrap_verification_shell(command)
615
+ inline_python = _inline_python_verification(unwrapped)
616
+ if inline_python is not None:
617
+ return inline_python
618
+ tokens = _command_tokens(unwrapped)
619
+ if tokens is None:
620
+ return VerificationCommand(False, None, "command is empty, compound, or ambiguous")
621
+ tokens = _strip_runner(tokens)
622
+ if not tokens:
623
+ return VerificationCommand(False, None, "command runner does not invoke a verifier")
624
+ lowered = [token.casefold() for token in tokens]
625
+ if any(
626
+ flag in lowered
627
+ for flag in {
628
+ "--help",
629
+ "-h",
630
+ "--version",
631
+ "--collect-only",
632
+ "--co",
633
+ "--list-tests",
634
+ "--list-sessions",
635
+ "--notest",
636
+ "--showconfig",
637
+ "--dry-run",
638
+ "--print-config",
639
+ }
640
+ ):
641
+ return VerificationCommand(False, None, "command does not execute verification")
642
+ if any(
643
+ flag in lowered
644
+ for flag in {
645
+ "--exit-zero",
646
+ "--no-error-on-unmatched-pattern",
647
+ "--pass-with-no-tests",
648
+ }
649
+ ):
650
+ return VerificationCommand(False, None, "command disables verifier failure status")
651
+
652
+ executable = _executable_name(tokens[0])
653
+ if re.fullmatch(r"python(?:\d+(?:\.\d+)*)?|py", executable):
654
+ if len(tokens) >= 2 and _looks_like_verifier_script(tokens[1]):
655
+ return VerificationCommand(True, "test", "command executes a verifier script")
656
+ if len(lowered) < 3 or lowered[1] != "-m":
657
+ return VerificationCommand(False, None, "python command does not invoke a verifier module")
658
+ tokens = tokens[2:]
659
+ lowered = lowered[2:]
660
+ executable = _executable_name(tokens[0])
661
+ elif executable == "git":
662
+ if len(lowered) >= 3 and lowered[1] == "diff" and "--check" in lowered[2:]:
663
+ return VerificationCommand(True, "git_diff", "git diff --check is structural verification")
664
+ return VerificationCommand(False, None, "git command is not a fail-on-error git diff check")
665
+
666
+ if _looks_like_verifier_script(tokens[0]):
667
+ return VerificationCommand(True, "test", "command executes a verifier script")
668
+
669
+ if executable in {"pytest", "py.test", "unittest", "tox", "nox"}:
670
+ return VerificationCommand(True, "test", "command executes a test runner")
671
+ if executable in {"go", "cargo", "dotnet"}:
672
+ if executable == "go" and (len(lowered) < 2 or lowered[1] != "test"):
673
+ return VerificationCommand(False, None, "go command is not a test")
674
+ if executable == "cargo" and len(lowered) >= 2 and lowered[1] == "clippy":
675
+ return VerificationCommand(True, "lint", "command executes a lint check")
676
+ if executable == "cargo" and (len(lowered) < 2 or lowered[1] != "test"):
677
+ return VerificationCommand(False, None, "cargo command is not a test or lint check")
678
+ if executable == "dotnet" and (len(lowered) < 2 or lowered[1] != "test"):
679
+ return VerificationCommand(False, None, "dotnet command is not a test")
680
+ return VerificationCommand(True, "test", "command executes a test runner")
681
+ if executable in {"mvn", "mvnw"}:
682
+ goals = {token for token in lowered[1:] if not token.startswith("-")}
683
+ if goals & {"test", "verify"}:
684
+ return VerificationCommand(True, "test", "build goal executes tests")
685
+ return VerificationCommand(False, None, "build command lacks a test goal")
686
+ if executable in {"gradle", "gradlew"}:
687
+ tasks = {token for token in lowered[1:] if not token.startswith("-")}
688
+ if tasks & {"test", "check"}:
689
+ return VerificationCommand(True, "test", "build task executes tests")
690
+ return VerificationCommand(False, None, "build command lacks a test task")
691
+
692
+ if executable in {"npm", "pnpm", "yarn", "bun"}:
693
+ scripts = {"test", "lint", "check", "typecheck", "type-check"}
694
+ requested = next((token for token in lowered[1:] if not token.startswith("-")), "")
695
+ if requested == "run" and len(lowered) >= 3:
696
+ requested = lowered[2]
697
+ if requested in scripts:
698
+ kind = "test" if requested == "test" else "lint"
699
+ return VerificationCommand(True, kind, "package script executes verification")
700
+ return VerificationCommand(False, None, "package command is not a test or lint script")
701
+
702
+ if executable in {"make", "just"}:
703
+ targets = {token for token in lowered[1:] if not token.startswith("-")}
704
+ if "test" in targets:
705
+ return VerificationCommand(True, "test", "build target executes tests")
706
+ if targets & {"lint", "check", "typecheck", "type-check"}:
707
+ return VerificationCommand(True, "lint", "build target executes lint checks")
708
+ return VerificationCommand(False, None, "build command lacks a verification target")
709
+
710
+ if executable in {
711
+ "ruff",
712
+ "mypy",
713
+ "pyright",
714
+ "pylint",
715
+ "flake8",
716
+ "eslint",
717
+ "biome",
718
+ "shellcheck",
719
+ "golangci-lint",
720
+ }:
721
+ if executable in {"ruff", "biome"} and (len(lowered) < 2 or lowered[1] != "check"):
722
+ return VerificationCommand(False, None, "command does not execute a lint check")
723
+ if any(flag in lowered for flag in {"--fix", "--write"}):
724
+ return VerificationCommand(False, None, "mutating lint commands are not verification")
725
+ return VerificationCommand(True, "lint", "command executes a lint or type check")
726
+ if executable == "tsc" and "--noemit" in lowered:
727
+ return VerificationCommand(True, "lint", "command executes a no-emit type check")
728
+ return VerificationCommand(False, None, "command is not a recognized verifier")
729
+
730
+
731
+ _STATUS_MASKING_ECHO_RE = re.compile(
732
+ r"(?is)^(?P<base>.+?);\s*echo\s+(?:[\"'])?[^\n;]*\$\?[^\n;]*(?:[\"'])?\s*$"
733
+ )
734
+
735
+
736
+ def masks_verification_exit_status(command: str) -> bool:
737
+ """Return whether a verifier is followed by an echo that masks its status."""
738
+
739
+ match = _STATUS_MASKING_ECHO_RE.match(str(command or "").strip())
740
+ if match is None:
741
+ return False
742
+ return classify_verification_command(match.group("base").strip()).qualifies
743
+
744
+
745
+ def record_verification(
746
+ verification_kind: str,
747
+ *,
748
+ success: bool,
749
+ ) -> EvidenceEvent | None:
750
+ """Record a successful recognized verifier without command/output data."""
751
+
752
+ if not success or verification_kind not in _VERIFICATION_KINDS:
753
+ return None
754
+ ledger = _ACTIVE_LEDGER.get()
755
+ if ledger is None or ledger.closed:
756
+ return None
757
+ return ledger.append("verification", "verification", verification_kind=verification_kind)
758
+
759
+
760
+ def record_shell_verification(command: str, *, returncode: int) -> EvidenceEvent | None:
761
+ """Classify a shell invocation and record it only when it passed."""
762
+
763
+ if returncode != 0:
764
+ return None
765
+ classification = classify_verification_command(command)
766
+ if not classification.qualifies or classification.kind is None:
767
+ return None
768
+ return record_verification(classification.kind, success=True)
769
+
770
+
771
+ def evaluate_completion(events: Sequence[EvidenceEvent]) -> CompletionDecision:
772
+ """Pure completion decision over an immutable or probe-supplied ledger."""
773
+
774
+ if not _valid_evidence(events):
775
+ return CompletionDecision(False, "execution evidence is invalid")
776
+ mutations = [event for event in events if event.kind == "mutation"]
777
+ if not mutations:
778
+ return CompletionDecision(True, "no successful file mutation requires verification")
779
+ last_mutation = mutations[-1]
780
+ for event in events:
781
+ if event.sequence <= last_mutation.sequence:
782
+ continue
783
+ if event.kind == "verification" and event.verification_kind in _VERIFICATION_KINDS:
784
+ return CompletionDecision(
785
+ True,
786
+ "last mutation was followed by successful verification",
787
+ last_mutation.sequence,
788
+ event.sequence,
789
+ event.verification_kind,
790
+ )
791
+ return CompletionDecision(
792
+ False,
793
+ "successful verification is required after the last mutation",
794
+ last_mutation.sequence,
795
+ )
796
+
797
+
798
+ def completion_decision(events: Iterable[EvidenceEvent] | None = None) -> CompletionDecision:
799
+ """Evaluate completion for supplied evidence or the active run scope."""
800
+
801
+ if events is not None:
802
+ return evaluate_completion(tuple(events))
803
+ ledger = _ACTIVE_LEDGER.get()
804
+ if ledger is None or ledger.closed:
805
+ return CompletionDecision(False, "no active execution scope")
806
+ return evaluate_completion(tuple(ledger.events))