calcifier 0.1.1__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 (69) hide show
  1. calcifier/__init__.py +73 -0
  2. calcifier/agent_config.py +263 -0
  3. calcifier/agent_events.py +450 -0
  4. calcifier/agent_providers.py +342 -0
  5. calcifier/agents.py +227 -0
  6. calcifier/artifact.py +313 -0
  7. calcifier/atomic_io.py +31 -0
  8. calcifier/baseline.py +159 -0
  9. calcifier/budget.py +63 -0
  10. calcifier/calcify.py +51 -0
  11. calcifier/catalog.py +139 -0
  12. calcifier/cli.py +787 -0
  13. calcifier/cli_options.py +108 -0
  14. calcifier/clock.py +79 -0
  15. calcifier/compiler_activity.py +108 -0
  16. calcifier/compiler_resources.py +98 -0
  17. calcifier/composition.py +347 -0
  18. calcifier/diagnostics.py +172 -0
  19. calcifier/dry_run.py +500 -0
  20. calcifier/effect_methods.py +39 -0
  21. calcifier/exceptions.py +28 -0
  22. calcifier/filesystem_contract.py +88 -0
  23. calcifier/gate.py +996 -0
  24. calcifier/graph.py +112 -0
  25. calcifier/graph_build.py +241 -0
  26. calcifier/graph_edges.py +34 -0
  27. calcifier/hashing.py +9 -0
  28. calcifier/instruction_context.py +197 -0
  29. calcifier/json_config.py +16 -0
  30. calcifier/metadata.py +235 -0
  31. calcifier/mutations.py +318 -0
  32. calcifier/native_events.py +299 -0
  33. calcifier/output_annotations.py +149 -0
  34. calcifier/outputs.py +550 -0
  35. calcifier/parallel_contract.py +61 -0
  36. calcifier/path_syntax.py +30 -0
  37. calcifier/portable.py +373 -0
  38. calcifier/prepared_context.py +82 -0
  39. calcifier/prompts.py +370 -0
  40. calcifier/py.typed +0 -0
  41. calcifier/recipe.py +1217 -0
  42. calcifier/recipe_ast.py +577 -0
  43. calcifier/recipe_draft.py +218 -0
  44. calcifier/registry.py +83 -0
  45. calcifier/repair.py +534 -0
  46. calcifier/run_result.py +119 -0
  47. calcifier/runtime_conformance.py +135 -0
  48. calcifier/side_effects.py +91 -0
  49. calcifier/skill_frontmatter.py +89 -0
  50. calcifier/skill_package.py +271 -0
  51. calcifier/source_quality.py +392 -0
  52. calcifier/subprocesses.py +407 -0
  53. calcifier/taint.py +603 -0
  54. calcifier/tools.py +456 -0
  55. calcifier/trace-schema.json +768 -0
  56. calcifier/trace.py +532 -0
  57. calcifier/trace_export.py +203 -0
  58. calcifier/trace_profile.py +83 -0
  59. calcifier/trust.py +483 -0
  60. calcifier/type_check.py +105 -0
  61. calcifier/update.py +65 -0
  62. calcifier/usage.py +61 -0
  63. calcifier/validation.py +29 -0
  64. calcifier/windows_job.py +202 -0
  65. calcifier-0.1.1.dist-info/METADATA +127 -0
  66. calcifier-0.1.1.dist-info/RECORD +69 -0
  67. calcifier-0.1.1.dist-info/WHEEL +4 -0
  68. calcifier-0.1.1.dist-info/entry_points.txt +2 -0
  69. calcifier-0.1.1.dist-info/licenses/LICENSE +21 -0
calcifier/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """Calcifier: typed Python recipes for LLM-assisted skills."""
2
+
3
+ from .agents import agent
4
+ from .budget import Budget, BudgetExceeded
5
+ from .composition import (
6
+ SubrecipeError,
7
+ SubrecipeResult,
8
+ call_catalog_recipe,
9
+ call_recipe,
10
+ )
11
+ from .filesystem_contract import TransientAgentError
12
+ from .mutations import (
13
+ CommandResult,
14
+ ensure_directory,
15
+ run_command,
16
+ verify_artifact,
17
+ write_json,
18
+ write_text,
19
+ )
20
+ from .outputs import output_type, word_count
21
+ from .prepared_context import PreparedContext, prepare_context
22
+ from .recipe import (
23
+ Recipe,
24
+ RecipeError,
25
+ RecipeFailure,
26
+ RecipeRun,
27
+ emit,
28
+ fail,
29
+ load_recipe,
30
+ parallel,
31
+ recipe,
32
+ resource,
33
+ step,
34
+ )
35
+ from .side_effects import record_side_effect
36
+ from .tools import tool
37
+
38
+ __version__ = "0.1.1"
39
+
40
+ __all__ = [
41
+ "Budget",
42
+ "BudgetExceeded",
43
+ "CommandResult",
44
+ "PreparedContext",
45
+ "Recipe",
46
+ "RecipeError",
47
+ "RecipeFailure",
48
+ "RecipeRun",
49
+ "SubrecipeError",
50
+ "SubrecipeResult",
51
+ "TransientAgentError",
52
+ "__version__",
53
+ "agent",
54
+ "call_catalog_recipe",
55
+ "call_recipe",
56
+ "emit",
57
+ "ensure_directory",
58
+ "fail",
59
+ "load_recipe",
60
+ "output_type",
61
+ "parallel",
62
+ "prepare_context",
63
+ "recipe",
64
+ "record_side_effect",
65
+ "resource",
66
+ "run_command",
67
+ "step",
68
+ "tool",
69
+ "verify_artifact",
70
+ "word_count",
71
+ "write_json",
72
+ "write_text",
73
+ ]
@@ -0,0 +1,263 @@
1
+ """Strict role-to-provider configuration for host agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass, field, replace
9
+ from pathlib import Path
10
+ from typing import Any, cast
11
+
12
+ from .exceptions import CalcifierError, ErrorFamily
13
+ from .hashing import sha256_hex
14
+ from .json_config import load_json_config
15
+ from .validation import is_positive_number
16
+
17
+
18
+ class AgentConfigError(CalcifierError, ValueError):
19
+ """Raised when an agent configuration is invalid."""
20
+
21
+ family = ErrorFamily.SETUP
22
+
23
+
24
+ ROLE_RE = re.compile(r"[A-Za-z][A-Za-z0-9._-]*")
25
+ _MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]*")
26
+ BUILTIN_AGENTS = frozenset({"claude", "codex"})
27
+ # Claude's whole tool allowlist: agents read and search, and never write or run commands.
28
+ _CLAUDE_READ_TOOLS = "Read,Glob,Grep"
29
+ _CODEX_REASONING_EFFORTS = frozenset(
30
+ {"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"}
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class AgentRuntime:
36
+ kind: str
37
+
38
+ def as_dict(self) -> dict[str, str]:
39
+ return {"kind": self.kind}
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class AgentRoute:
44
+ role: str
45
+ builtin: str | None
46
+ command: tuple[str, ...] | None
47
+ runtime: AgentRuntime
48
+ timeout_seconds: int | None = None
49
+ remediation: str | None = None
50
+ model: str | None = None
51
+ reasoning_effort: str | None = None
52
+
53
+ @property
54
+ def target_type(self) -> str:
55
+ return "builtin" if self.builtin is not None else "command"
56
+
57
+ @property
58
+ def target_name(self) -> str:
59
+ if self.builtin is not None:
60
+ return self.builtin
61
+ if self.command is None: # defensive; parser enforces exactly one target
62
+ raise AgentConfigError(f"agent {self.role!r} has no target")
63
+ return self.command[0]
64
+
65
+ def as_dict(self) -> dict[str, Any]:
66
+ target: dict[str, Any]
67
+ if self.builtin is not None:
68
+ target = {"builtin": self.builtin}
69
+ else:
70
+ target = {"command": list(self.command or ())}
71
+ result: dict[str, Any] = {
72
+ "target": target,
73
+ "runtime": self.runtime.as_dict(),
74
+ }
75
+ if self.timeout_seconds is not None:
76
+ result["timeout_seconds"] = self.timeout_seconds
77
+ if self.remediation is not None:
78
+ result["remediation"] = self.remediation
79
+ if self.model is not None:
80
+ result["model"] = self.model
81
+ if self.reasoning_effort is not None:
82
+ result["reasoning_effort"] = self.reasoning_effort
83
+ return result
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class AgentConfiguration:
88
+ routes: Mapping[str, AgentRoute] = field(default_factory=dict)
89
+
90
+ def fingerprint(self, roles: tuple[str, ...] | None = None) -> str:
91
+ selected = sorted(self.routes) if roles is None else sorted(set(roles))
92
+ payload = {
93
+ "version": 1,
94
+ "agents": {
95
+ role: self.routes[role].as_dict() for role in selected if role in self.routes
96
+ },
97
+ }
98
+ # gremlin: pardon[equivalent] the payload is already built in sorted, fixed order.
99
+ material = json.dumps(payload, sort_keys=True, separators=(",", ":"))
100
+ return sha256_hex(material.encode())
101
+
102
+
103
+ def override_agent_timeouts(
104
+ configuration: AgentConfiguration,
105
+ values: list[str] | tuple[str, ...] | None,
106
+ ) -> AgentConfiguration:
107
+ """Apply strict ``ROLE=SECONDS`` overrides to a version-1 configuration."""
108
+ routes = dict(configuration.routes)
109
+ for value in values or ():
110
+ if "=" not in value:
111
+ raise AgentConfigError("--agent-timeout expects ROLE=SECONDS")
112
+ role, raw_seconds = value.split("=", 1)
113
+ if role not in routes:
114
+ raise AgentConfigError(f"--agent-timeout names unknown role {role!r}")
115
+ try:
116
+ seconds = int(raw_seconds)
117
+ except ValueError as exc:
118
+ raise AgentConfigError("--agent-timeout seconds must be an integer") from exc
119
+ if not is_positive_number(seconds, integral=True):
120
+ raise AgentConfigError("--agent-timeout seconds must be positive")
121
+ routes[role] = replace(routes[role], timeout_seconds=seconds)
122
+ return AgentConfiguration(routes=routes)
123
+
124
+
125
+ def load_agent_config(path: str | Path) -> AgentConfiguration:
126
+ """Load a strict version-1 role-to-subprocess configuration."""
127
+ config_path = Path(path)
128
+ try:
129
+ raw = load_json_config(config_path, object_pairs_hook=_unique_object)
130
+ except (OSError, json.JSONDecodeError) as exc:
131
+ raise AgentConfigError(f"failed to load agent config {config_path}: {exc}") from exc
132
+ root = _mapping(raw, "agent config")
133
+ _reject_unknown(root, {"version", "agents"}, "agent config")
134
+ if root.get("version") != 1:
135
+ raise AgentConfigError("agent config version must be 1")
136
+ raw_agents = _mapping(root.get("agents"), "agent config agents")
137
+ routes: dict[str, AgentRoute] = {}
138
+ for role, value in raw_agents.items():
139
+ if not isinstance(role, str) or ROLE_RE.fullmatch(role) is None:
140
+ raise AgentConfigError(f"invalid agent role name: {role!r}")
141
+ if role in BUILTIN_AGENTS:
142
+ raise AgentConfigError(f"agent role {role!r} is reserved")
143
+ routes[role] = _parse_route(role, value)
144
+ return AgentConfiguration(routes=routes)
145
+
146
+
147
+ def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
148
+ result: dict[str, Any] = {}
149
+ for key, value in pairs:
150
+ if key in result:
151
+ raise AgentConfigError(f"agent config contains duplicate key {key!r}")
152
+ result[key] = value
153
+ return result
154
+
155
+
156
+ def _parse_route(role: str, raw: object) -> AgentRoute:
157
+ route = _mapping(raw, f"agent {role!r}")
158
+ _reject_unknown(
159
+ route,
160
+ {
161
+ "target",
162
+ "runtime",
163
+ "timeout_seconds",
164
+ "remediation",
165
+ "model",
166
+ "reasoning_effort",
167
+ },
168
+ f"agent {role!r}",
169
+ )
170
+ target = _mapping(route.get("target"), f"agent {role!r} target")
171
+ _reject_unknown(target, {"builtin", "command"}, f"agent {role!r} target")
172
+ has_builtin = "builtin" in target
173
+ has_command = "command" in target
174
+ if has_builtin == has_command:
175
+ raise AgentConfigError(
176
+ f"agent {role!r} target must contain exactly one of builtin or command"
177
+ )
178
+ builtin: str | None = None
179
+ command: tuple[str, ...] | None = None
180
+ if has_builtin:
181
+ builtin_value = target["builtin"]
182
+ if builtin_value not in BUILTIN_AGENTS:
183
+ raise AgentConfigError(
184
+ f"agent {role!r} builtin must be one of {sorted(BUILTIN_AGENTS)}"
185
+ )
186
+ builtin = str(builtin_value)
187
+ else:
188
+ command_value = target["command"]
189
+ if not isinstance(command_value, list) or not command_value:
190
+ raise AgentConfigError(f"agent {role!r} command must be a non-empty argv array")
191
+ if not all(isinstance(part, str) and part for part in command_value):
192
+ raise AgentConfigError(f"agent {role!r} command entries must be non-empty strings")
193
+ command = tuple(command_value)
194
+ runtime = _parse_runtime(role, route.get("runtime"))
195
+ timeout = _parse_timeout(role, route.get("timeout_seconds"))
196
+ remediation = route.get("remediation")
197
+ if remediation is not None and not isinstance(remediation, str):
198
+ raise AgentConfigError(f"agent {role!r} remediation must be a string")
199
+ model = _parse_model(role, route.get("model"), builtin)
200
+ reasoning_effort = _parse_codex_reasoning_effort(role, route.get("reasoning_effort"), builtin)
201
+ return AgentRoute(
202
+ role,
203
+ builtin,
204
+ command,
205
+ runtime,
206
+ timeout,
207
+ remediation,
208
+ model,
209
+ reasoning_effort,
210
+ )
211
+
212
+
213
+ def _parse_model(role: str, value: object, builtin: str | None) -> str | None:
214
+ if value is None:
215
+ return None
216
+ if builtin not in {"claude", "codex"}:
217
+ raise AgentConfigError(f"agent {role!r} model is only valid for builtin claude or codex")
218
+ if not isinstance(value, str) or _MODEL_RE.fullmatch(value) is None:
219
+ raise AgentConfigError(f"agent {role!r} model must be a non-empty model name")
220
+ return value
221
+
222
+
223
+ def _parse_codex_reasoning_effort(role: str, value: object, builtin: str | None) -> str | None:
224
+ if value is None:
225
+ return None
226
+ if builtin != "codex":
227
+ raise AgentConfigError(f"agent {role!r} reasoning_effort is only valid for builtin codex")
228
+ if value not in _CODEX_REASONING_EFFORTS:
229
+ raise AgentConfigError(
230
+ f"agent {role!r} reasoning_effort must be one of {sorted(_CODEX_REASONING_EFFORTS)}"
231
+ )
232
+ return str(value)
233
+
234
+
235
+ def _parse_timeout(role: str, value: object) -> int:
236
+ if value is None:
237
+ raise AgentConfigError(f"agent {role!r} requires timeout_seconds")
238
+ if not is_positive_number(value, integral=True):
239
+ raise AgentConfigError(f"agent {role!r} timeout_seconds must be a positive integer")
240
+ return cast("int", value)
241
+
242
+
243
+ def _parse_runtime(role: str, raw: object) -> AgentRuntime:
244
+ runtime = _mapping(raw, f"agent {role!r} runtime")
245
+ if runtime.get("kind") != "host":
246
+ raise AgentConfigError(f"agent {role!r} runtime kind must be host")
247
+ if set(runtime) != {"kind"}:
248
+ raise AgentConfigError(f"host runtime for agent {role!r} accepts only kind")
249
+ return AgentRuntime(kind="host")
250
+
251
+
252
+ def _mapping(raw: object, context: str) -> Mapping[str, Any]:
253
+ if not isinstance(raw, Mapping):
254
+ raise AgentConfigError(f"{context} must be a JSON object")
255
+ if not all(isinstance(key, str) for key in raw):
256
+ raise AgentConfigError(f"{context} keys must be strings")
257
+ return cast("Mapping[str, Any]", raw)
258
+
259
+
260
+ def _reject_unknown(raw: Mapping[str, Any], allowed: set[str], context: str) -> None:
261
+ unknown = sorted(set(raw) - allowed)
262
+ if unknown:
263
+ raise AgentConfigError(f"{context} has unknown fields: {unknown}")