graphite-code 0.3.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 (112) hide show
  1. graphite/__init__.py +41 -0
  2. graphite/__main__.py +7 -0
  3. graphite/_cleanup_worker.py +525 -0
  4. graphite/activation.py +164 -0
  5. graphite/agent_hooks.py +577 -0
  6. graphite/agent_settings.py +226 -0
  7. graphite/analyze.py +146 -0
  8. graphite/answer_contract.py +420 -0
  9. graphite/bootstrap.py +210 -0
  10. graphite/buildlock.py +99 -0
  11. graphite/cache.py +131 -0
  12. graphite/channel.py +1325 -0
  13. graphite/cli.py +3053 -0
  14. graphite/cluster.py +111 -0
  15. graphite/config.py +209 -0
  16. graphite/context.py +355 -0
  17. graphite/daemon.py +745 -0
  18. graphite/daemon_health.py +733 -0
  19. graphite/debt.py +118 -0
  20. graphite/dependency_install.py +1597 -0
  21. graphite/detach.py +33 -0
  22. graphite/doctor.py +678 -0
  23. graphite/doctor_probes.py +2100 -0
  24. graphite/engine_identity.py +238 -0
  25. graphite/export/__init__.py +6 -0
  26. graphite/export/html.py +244 -0
  27. graphite/export/json.py +39 -0
  28. graphite/export/md.py +68 -0
  29. graphite/extract/__init__.py +4 -0
  30. graphite/extract/ast.py +1964 -0
  31. graphite/freshness.py +127 -0
  32. graphite/git.py +406 -0
  33. graphite/graph.py +117 -0
  34. graphite/graph_io.py +188 -0
  35. graphite/health.py +147 -0
  36. graphite/hook_entry.py +68 -0
  37. graphite/hookinstall.py +224 -0
  38. graphite/hookshim.py +86 -0
  39. graphite/incident_ledger.py +247 -0
  40. graphite/ingest.py +279 -0
  41. graphite/init.py +791 -0
  42. graphite/io.py +32 -0
  43. graphite/listing.py +51 -0
  44. graphite/llm.py +518 -0
  45. graphite/llm_probe.py +157 -0
  46. graphite/mcp.py +7 -0
  47. graphite/mcp_server.py +450 -0
  48. graphite/natural_query.py +252 -0
  49. graphite/overlays.py +713 -0
  50. graphite/probe_process.py +879 -0
  51. graphite/probe_workspace.py +728 -0
  52. graphite/process_contracts.py +22 -0
  53. graphite/provider_observer.py +397 -0
  54. graphite/query.py +646 -0
  55. graphite/query_plan.py +97 -0
  56. graphite/replacement_audit.py +291 -0
  57. graphite/resolve.py +660 -0
  58. graphite/review.py +782 -0
  59. graphite/routing/__init__.py +5 -0
  60. graphite/routing/approval.py +362 -0
  61. graphite/routing/classifier.py +169 -0
  62. graphite/routing/claude_executor.py +419 -0
  63. graphite/routing/claude_probe.py +102 -0
  64. graphite/routing/cli_identity.py +84 -0
  65. graphite/routing/codex_executor.py +383 -0
  66. graphite/routing/codex_probe.py +93 -0
  67. graphite/routing/context_builder.py +327 -0
  68. graphite/routing/contracts.py +802 -0
  69. graphite/routing/diff_policy.py +468 -0
  70. graphite/routing/edit_apply.py +166 -0
  71. graphite/routing/effort.py +43 -0
  72. graphite/routing/lifecycle.py +771 -0
  73. graphite/routing/lifecycle_operator.py +227 -0
  74. graphite/routing/lifecycle_service.py +555 -0
  75. graphite/routing/lifecycle_storage.py +977 -0
  76. graphite/routing/ollama_executor.py +341 -0
  77. graphite/routing/ollama_probe.py +72 -0
  78. graphite/routing/openrouter_executor.py +338 -0
  79. graphite/routing/openrouter_probe.py +188 -0
  80. graphite/routing/policy.py +815 -0
  81. graphite/routing/probe_runner.py +543 -0
  82. graphite/routing/process_runner.py +523 -0
  83. graphite/routing/profiles.py +554 -0
  84. graphite/routing/prompt.py +58 -0
  85. graphite/routing/registry.py +444 -0
  86. graphite/routing/route_pool.py +629 -0
  87. graphite/routing/route_pool_execution.py +275 -0
  88. graphite/routing/schema_validation.py +169 -0
  89. graphite/routing/service.py +1263 -0
  90. graphite/routing/settings.py +99 -0
  91. graphite/routing/shadow.py +201 -0
  92. graphite/routing/storage.py +4001 -0
  93. graphite/routing/telemetry.py +346 -0
  94. graphite/routing/worktree.py +259 -0
  95. graphite/routing/zai_edit.py +113 -0
  96. graphite/routing/zai_executor.py +191 -0
  97. graphite/routing/zai_probe.py +126 -0
  98. graphite/savings.py +84 -0
  99. graphite/ts_bridge.py +142 -0
  100. graphite/ts_resolver.mjs +314 -0
  101. graphite/typescript_activation.py +1586 -0
  102. graphite/usage_ledger.py +156 -0
  103. graphite/validation.py +148 -0
  104. graphite/watch.py +167 -0
  105. graphite/windows_job.py +368 -0
  106. graphite/windows_startup.py +144 -0
  107. graphite/windows_task.py +212 -0
  108. graphite_code-0.3.0.dist-info/METADATA +743 -0
  109. graphite_code-0.3.0.dist-info/RECORD +112 -0
  110. graphite_code-0.3.0.dist-info/WHEEL +4 -0
  111. graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
  112. graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,346 @@
1
+ """Append-only verified evidence and typed privacy-safe aggregate export."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ from .contracts import (
11
+ EvidenceProvenance,
12
+ ExecutionReceipt,
13
+ Effort,
14
+ ProviderId,
15
+ RiskTier,
16
+ TaskCategory,
17
+ VerifiedOutcome,
18
+ )
19
+ from .registry import BUNDLED_PROFILES
20
+ from .storage import AggregateRecord, AggregateStore, RepositoryStore
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class EvidenceCorrelation:
25
+ task_id: str
26
+ decision_id: str
27
+ graph_fingerprint: str
28
+
29
+ def __post_init__(self) -> None:
30
+ if not self.task_id or not self.decision_id:
31
+ raise ValueError("evidence_correlation_invalid")
32
+ if len(self.graph_fingerprint) != 64 or any(
33
+ character not in "0123456789abcdef" for character in self.graph_fingerprint
34
+ ):
35
+ raise ValueError("evidence_correlation_invalid")
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class EvidenceSummary:
40
+ sample_count: int
41
+ success_count: int
42
+ severe_failure_count: int
43
+ human_count: int
44
+
45
+
46
+ class ReviewDefectClass(StrEnum):
47
+ CORRECTNESS = "correctness"
48
+ SECURITY = "security"
49
+ RELIABILITY = "reliability"
50
+ MAINTAINABILITY = "maintainability"
51
+ PERFORMANCE = "performance"
52
+ TEST_COVERAGE = "test_coverage"
53
+ REQUIREMENT_MISS = "requirement_miss"
54
+
55
+
56
+ class HumanVerdict(StrEnum):
57
+ ACCEPTED = "accepted"
58
+ REJECTED = "rejected"
59
+ REWORK = "rework"
60
+
61
+
62
+ _SAFE_MODEL = re.compile(
63
+ r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(/[A-Za-z0-9][A-Za-z0-9._:+-]{0,127})?$"
64
+ )
65
+ _HEX_64 = re.compile(r"^[0-9a-f]{64}$")
66
+ _VALIDATION_OUTCOMES = frozenset({"passed", "failed", "blocked", "not_run"})
67
+
68
+
69
+ def _bounded_optional(value: int | None, code: str, maximum: int) -> int | None:
70
+ if value is None:
71
+ return None
72
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
73
+ raise ValueError(code)
74
+ return value
75
+
76
+
77
+ def _bounded_required(value: object, code: str, maximum: int) -> int:
78
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= maximum:
79
+ raise ValueError(code)
80
+ return value
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class CliTelemetryRecord:
85
+ """The complete public telemetry schema; raw artifacts have no field to enter."""
86
+
87
+ provider: ProviderId
88
+ requested_model: str
89
+ effective_model: str
90
+ effort: Effort
91
+ capability_snapshot_digest: str
92
+ category: TaskCategory
93
+ risk: RiskTier
94
+ latency_ms: int | None
95
+ input_tokens: int | None
96
+ output_tokens: int | None
97
+ changed_file_count: int
98
+ changed_byte_count: int
99
+ validation_outcome: str
100
+ review_defect_classes: tuple[ReviewDefectClass, ...]
101
+ rework_count: int
102
+ human_verdict: HumanVerdict | None
103
+ provenance: EvidenceProvenance
104
+ observed_at: int
105
+ diff_sha256: str | None = None
106
+ cost_status: str = "unknown"
107
+
108
+ def __post_init__(self) -> None:
109
+ object.__setattr__(self, "provider", ProviderId(self.provider))
110
+ object.__setattr__(self, "effort", Effort(self.effort))
111
+ object.__setattr__(self, "category", TaskCategory(self.category))
112
+ object.__setattr__(self, "risk", RiskTier(self.risk))
113
+ object.__setattr__(self, "provenance", EvidenceProvenance(self.provenance))
114
+ for value in (self.requested_model, self.effective_model):
115
+ if not isinstance(value, str) or _SAFE_MODEL.fullmatch(value) is None:
116
+ raise ValueError("telemetry_model_invalid")
117
+ if (
118
+ not isinstance(self.capability_snapshot_digest, str)
119
+ or _HEX_64.fullmatch(self.capability_snapshot_digest) is None
120
+ ):
121
+ raise ValueError("telemetry_snapshot_invalid")
122
+ _bounded_optional(self.latency_ms, "telemetry_latency_invalid", 86_400_000)
123
+ _bounded_optional(self.input_tokens, "telemetry_usage_invalid", 100_000_000)
124
+ _bounded_optional(self.output_tokens, "telemetry_usage_invalid", 100_000_000)
125
+ for name, maximum in (
126
+ ("changed_file_count", 100_000),
127
+ ("changed_byte_count", 10**12),
128
+ ("rework_count", 1_000),
129
+ ("observed_at", 10**12),
130
+ ):
131
+ _bounded_required(getattr(self, name), f"telemetry_{name}_invalid", maximum)
132
+ if self.validation_outcome not in _VALIDATION_OUTCOMES:
133
+ raise ValueError("telemetry_validation_outcome_invalid")
134
+ if not isinstance(self.review_defect_classes, tuple):
135
+ raise ValueError("telemetry_defect_classes_invalid")
136
+ defects = tuple(ReviewDefectClass(item) for item in self.review_defect_classes)
137
+ if len(defects) > len(ReviewDefectClass) or len(set(defects)) != len(defects):
138
+ raise ValueError("telemetry_defect_classes_invalid")
139
+ object.__setattr__(self, "review_defect_classes", tuple(sorted(defects, key=str)))
140
+ if self.human_verdict is not None:
141
+ object.__setattr__(self, "human_verdict", HumanVerdict(self.human_verdict))
142
+ if self.cost_status != "unknown":
143
+ raise ValueError("telemetry_cost_status_invalid")
144
+ if self.diff_sha256 is not None and (
145
+ not isinstance(self.diff_sha256, str)
146
+ or _HEX_64.fullmatch(self.diff_sha256) is None
147
+ ):
148
+ raise ValueError("telemetry_diff_hash_invalid")
149
+
150
+ def to_dict(self) -> dict[str, Any]:
151
+ return {
152
+ "provider": self.provider.value,
153
+ "requested_model": self.requested_model,
154
+ "effective_model": self.effective_model,
155
+ "effort": self.effort.value,
156
+ "capability_snapshot_digest": self.capability_snapshot_digest,
157
+ "category": self.category.value,
158
+ "risk": self.risk.value,
159
+ "latency_ms": self.latency_ms,
160
+ "input_tokens": self.input_tokens,
161
+ "output_tokens": self.output_tokens,
162
+ "cost_status": self.cost_status,
163
+ "changed_file_count": self.changed_file_count,
164
+ "changed_byte_count": self.changed_byte_count,
165
+ "validation_outcome": self.validation_outcome,
166
+ "review_defect_classes": [item.value for item in self.review_defect_classes],
167
+ "rework_count": self.rework_count,
168
+ "human_verdict": None if self.human_verdict is None else self.human_verdict.value,
169
+ "provenance": self.provenance.value,
170
+ "observed_at": self.observed_at,
171
+ "diff_sha256": self.diff_sha256,
172
+ }
173
+
174
+
175
+ def record_cli_telemetry(store: RepositoryStore, record: CliTelemetryRecord) -> bool:
176
+ if not isinstance(record, CliTelemetryRecord):
177
+ raise ValueError("cli_telemetry_record_invalid")
178
+ return store.record_cli_telemetry(record.to_dict())
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class WeightedTelemetrySummary:
183
+ samples: int
184
+ weighted_samples_millis: int
185
+ weighted_success_millis: int
186
+ success_millis: int
187
+ latest_observed_at: int | None
188
+
189
+
190
+ def summarize_cli_telemetry(
191
+ records: tuple[CliTelemetryRecord, ...],
192
+ *,
193
+ now: int,
194
+ half_life_days: int = 30,
195
+ ) -> WeightedTelemetrySummary:
196
+ """Compute deterministic recency-weighted evidence; never infer missing cost."""
197
+ if isinstance(now, bool) or not isinstance(now, int) or now < 0:
198
+ raise ValueError("telemetry_time_invalid")
199
+ if isinstance(half_life_days, bool) or not isinstance(half_life_days, int) or half_life_days < 1:
200
+ raise ValueError("telemetry_half_life_invalid")
201
+ weighted_total = weighted_success = 0
202
+ latest: int | None = None
203
+ for record in records:
204
+ if not isinstance(record, CliTelemetryRecord) or record.observed_at > now:
205
+ raise ValueError("cli_telemetry_record_invalid")
206
+ age_days = (now - record.observed_at) // 86_400
207
+ weight = max(1, round(1_000 * (0.5 ** (age_days / half_life_days))))
208
+ weighted_total += weight
209
+ success = (
210
+ record.validation_outcome == "passed"
211
+ and not record.review_defect_classes
212
+ and record.human_verdict is not HumanVerdict.REJECTED
213
+ )
214
+ weighted_success += weight * int(success)
215
+ latest = record.observed_at if latest is None else max(latest, record.observed_at)
216
+ success_millis = 0 if weighted_total == 0 else weighted_success * 1_000 // weighted_total
217
+ return WeightedTelemetrySummary(
218
+ len(records), weighted_total, weighted_success, success_millis, latest
219
+ )
220
+
221
+
222
+ def record_verified_outcome(
223
+ store: RepositoryStore,
224
+ outcome: VerifiedOutcome,
225
+ correlation: EvidenceCorrelation,
226
+ ) -> bool:
227
+ """Append an outcome after binding it to the recorded execution identity."""
228
+ linked = store.execution_evidence(outcome.execution_id)
229
+ expected = {
230
+ "task_id": correlation.task_id,
231
+ "decision_id": correlation.decision_id,
232
+ "graph_fingerprint": correlation.graph_fingerprint,
233
+ }
234
+ if linked != expected:
235
+ raise ValueError("evidence_correlation_invalid")
236
+ if outcome.provenance in {
237
+ EvidenceProvenance.MACHINE_VERIFIED,
238
+ EvidenceProvenance.CI_IMPORTED,
239
+ } and any(
240
+ value is None
241
+ for value in (outcome.build_passed, outcome.tests_passed, outcome.security_passed)
242
+ ):
243
+ raise ValueError("evidence_verification_incomplete")
244
+ success = bool(
245
+ outcome.build_passed is True
246
+ and outcome.tests_passed is True
247
+ and outcome.security_passed is True
248
+ and not outcome.escalated
249
+ and not outcome.reverted
250
+ and not outcome.severe_failure
251
+ )
252
+ if outcome.provenance is EvidenceProvenance.HUMAN:
253
+ success = outcome.human_accepted is True and not outcome.reverted
254
+ if outcome.provenance is EvidenceProvenance.REVERSION:
255
+ success = False
256
+ return store.record_outcome(
257
+ outcome.outcome_id,
258
+ outcome.execution_id,
259
+ outcome.provenance.value,
260
+ success,
261
+ outcome.severe_failure,
262
+ outcome.recorded_at,
263
+ )
264
+
265
+
266
+ def evidence_summary(store: RepositoryStore) -> EvidenceSummary:
267
+ """Derive current evidence without mutating historical events."""
268
+ cutoff = store.latest_incident_review()
269
+ rows = [
270
+ row
271
+ for row in store.outcome_evidence_rows()
272
+ if cutoff is None or int(row["recorded_at"]) > cutoff
273
+ ]
274
+ human_count = sum(row["provenance"] == EvidenceProvenance.HUMAN.value for row in rows)
275
+ grouped: dict[str, list[dict[str, object]]] = {}
276
+ for row in rows:
277
+ grouped.setdefault(str(row["execution_id"]), []).append(row)
278
+ samples = successes = severe = 0
279
+ for events in grouped.values():
280
+ admitted = [
281
+ row for row in events
282
+ if row["provenance"] in {
283
+ EvidenceProvenance.MACHINE_VERIFIED.value,
284
+ EvidenceProvenance.CI_IMPORTED.value,
285
+ }
286
+ ]
287
+ if not admitted:
288
+ continue
289
+ samples += 1
290
+ reverted = any(
291
+ row["provenance"] == EvidenceProvenance.REVERSION.value for row in events
292
+ )
293
+ latest = admitted[-1]
294
+ successes += int(bool(latest["success"]) and not reverted)
295
+ severe += int(any(bool(row["severe_failure"]) for row in admitted))
296
+ return EvidenceSummary(samples, successes, severe, human_count)
297
+
298
+
299
+ def close_incident_review(
300
+ store: RepositoryStore,
301
+ execution_id: str,
302
+ *,
303
+ reviewed_at: int,
304
+ ) -> bool:
305
+ rows = store.outcome_evidence_rows()
306
+ if not any(
307
+ row["execution_id"] == execution_id and bool(row["severe_failure"])
308
+ for row in rows
309
+ ):
310
+ raise ValueError("incident_review_invalid")
311
+ return store.close_incident(execution_id, reviewed_at)
312
+
313
+
314
+ def _bucket(value: int | None) -> int:
315
+ if value is None or value <= 0:
316
+ return 0
317
+ return min(20, int(math.log2(value)) + 1)
318
+
319
+
320
+ def export_sanitized_aggregate(
321
+ aggregate: AggregateStore,
322
+ receipt: ExecutionReceipt,
323
+ *,
324
+ category: TaskCategory,
325
+ risk: RiskTier,
326
+ policy_version: str,
327
+ recorded_at: int,
328
+ ) -> bool:
329
+ """Build an aggregate solely from allowlisted typed fields."""
330
+ if receipt.model_id not in BUNDLED_PROFILES:
331
+ raise ValueError("model_id_invalid")
332
+ if isinstance(recorded_at, bool) or not isinstance(recorded_at, int) or recorded_at < 0:
333
+ raise ValueError("recorded_at_invalid")
334
+ record = AggregateRecord(
335
+ model_id=receipt.model_id,
336
+ effort=receipt.effort.value,
337
+ category=TaskCategory(category).value,
338
+ risk=RiskTier(risk).value,
339
+ outcome=receipt.outcome.value,
340
+ input_bucket=_bucket(receipt.input_tokens),
341
+ output_bucket=_bucket(receipt.output_tokens),
342
+ latency_bucket=_bucket(receipt.latency_ms),
343
+ policy_version=policy_version,
344
+ recorded_day=recorded_at // 86_400,
345
+ )
346
+ return aggregate.write(record)
@@ -0,0 +1,259 @@
1
+ """Approval-bound, isolated Git worktree preparation."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import stat
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Final
10
+
11
+ from graphite.git import (
12
+ GitError,
13
+ GitOutputLimitError,
14
+ GitResult,
15
+ GitRunner,
16
+ GitTimeoutError,
17
+ )
18
+
19
+ _TASK_ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
20
+ _COMMIT: Final = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
21
+ _REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
22
+ GIT_TIMEOUT_SECONDS: Final = 15.0
23
+ MAX_GIT_METADATA_BYTES: Final = 8 * 1024 * 1024
24
+
25
+
26
+ class WorktreeError(RuntimeError):
27
+ """Stable, path-free worktree preparation failure.
28
+
29
+ `cause` is a diagnostic carried in the MESSAGE only, and only ever an
30
+ exception class name. See `DiffPolicyError` for the reasoning; the two are
31
+ deliberately the same shape because the routing service catches them
32
+ together and cannot tell which one it is holding.
33
+ """
34
+
35
+ def __init__(self, code: str, cause: str | None = None) -> None:
36
+ self.code = code
37
+ self.cause = cause
38
+ super().__init__(f"{code} ({cause})" if cause else code)
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class TaskWorktree:
43
+ worktree_id: str
44
+ root: Path
45
+ git_common_dir: Path
46
+ baseline_commit: str
47
+ status: str = "prepared"
48
+
49
+
50
+ _CLEANUP_TERMINAL_STATES: Final = frozenset(
51
+ {"accepted", "rejected", "abandoned", "quarantined"}
52
+ )
53
+
54
+
55
+ def _is_reparse(metadata: os.stat_result) -> bool:
56
+ return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
57
+
58
+
59
+ def _canonical_directory(path: Path, code: str) -> Path:
60
+ if not isinstance(path, Path) or not path.is_absolute():
61
+ raise WorktreeError(code)
62
+ try:
63
+ metadata = path.lstat()
64
+ resolved = path.resolve(strict=True)
65
+ except OSError:
66
+ raise WorktreeError(code) from None
67
+ if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
68
+ raise WorktreeError(code)
69
+ return resolved
70
+
71
+
72
+ def _secure_directory(path: Path, code: str) -> Path:
73
+ try:
74
+ path.mkdir(parents=True, exist_ok=True, mode=0o700)
75
+ except OSError:
76
+ raise WorktreeError(code) from None
77
+ resolved = _canonical_directory(path, code)
78
+ if os.name != "nt":
79
+ try:
80
+ path.chmod(0o700)
81
+ except OSError:
82
+ raise WorktreeError(code) from None
83
+ return resolved
84
+
85
+
86
+ def _run(runner: GitRunner, arguments: list[str], *, maximum: int = MAX_GIT_METADATA_BYTES) -> GitResult:
87
+ try:
88
+ return runner.run(
89
+ arguments,
90
+ timeout_seconds=GIT_TIMEOUT_SECONDS,
91
+ max_stdout_bytes=maximum,
92
+ )
93
+ except GitTimeoutError:
94
+ raise WorktreeError("git_timeout") from None
95
+ except GitOutputLimitError:
96
+ raise WorktreeError("git_output_limit") from None
97
+ except GitError as exc:
98
+ # See the matching branch in `diff_policy._run_git` (graphite#37).
99
+ raise WorktreeError("git_unavailable", exc.diagnostic()) from None
100
+
101
+
102
+ def _decode_line(output: bytes, code: str) -> str:
103
+ try:
104
+ value = output.decode("utf-8").rstrip("\r\n")
105
+ except UnicodeDecodeError:
106
+ raise WorktreeError(code) from None
107
+ if not value or "\x00" in value or "\n" in value or "\r" in value:
108
+ raise WorktreeError(code)
109
+ return value
110
+
111
+
112
+ def _resolve_git_path(root: Path, raw: bytes, code: str) -> Path:
113
+ value = _decode_line(raw, code)
114
+ candidate = Path(value)
115
+ if not candidate.is_absolute():
116
+ candidate = root / candidate
117
+ try:
118
+ return candidate.resolve(strict=True)
119
+ except OSError:
120
+ raise WorktreeError(code) from None
121
+
122
+
123
+ def _validate_index(output: bytes) -> None:
124
+ if output and not output.endswith(b"\0"):
125
+ raise WorktreeError("git_protocol")
126
+ folded: dict[str, str] = {}
127
+ for record in output[:-1].split(b"\0") if output else ():
128
+ try:
129
+ header, raw_path = record.split(b"\t", 1)
130
+ mode, _object_id, stage = header.split(b" ")
131
+ path = raw_path.decode("utf-8")
132
+ except (ValueError, UnicodeDecodeError):
133
+ raise WorktreeError("git_protocol") from None
134
+ if stage != b"0" or mode in {b"120000", b"160000"}:
135
+ raise WorktreeError("source_special_file")
136
+ if not path or "\\" in path or "\x00" in path:
137
+ raise WorktreeError("git_protocol")
138
+ collision = path.casefold()
139
+ if collision in folded and folded[collision] != path:
140
+ raise WorktreeError("source_path_collision")
141
+ folded[collision] = path
142
+
143
+
144
+ def create_task_worktree(
145
+ *,
146
+ source_root: Path,
147
+ state_root: Path,
148
+ task_id: str,
149
+ approved_commit: str,
150
+ ) -> TaskWorktree:
151
+ """Create one detached worktree only from an exact clean approved commit."""
152
+ if not isinstance(task_id, str) or _TASK_ID.fullmatch(task_id) is None:
153
+ raise WorktreeError("task_id_invalid")
154
+ if not isinstance(approved_commit, str) or _COMMIT.fullmatch(approved_commit) is None:
155
+ raise WorktreeError("commit_invalid")
156
+ source = _canonical_directory(source_root, "source_root_invalid")
157
+ state_candidate = state_root.resolve(strict=False)
158
+ try:
159
+ state_candidate.relative_to(source)
160
+ except ValueError:
161
+ pass
162
+ else:
163
+ raise WorktreeError("state_root_invalid")
164
+ state = _secure_directory(state_root, "state_root_invalid")
165
+ tasks = _secure_directory(state / "tasks", "state_root_invalid")
166
+ target = tasks / task_id
167
+ if target.exists() or target.is_symlink():
168
+ raise WorktreeError("worktree_exists")
169
+
170
+ runner = GitRunner(source)
171
+ top = _run(runner, ["rev-parse", "--show-toplevel"])
172
+ if top.returncode != 0 or _resolve_git_path(source, top.stdout, "source_root_invalid") != source:
173
+ raise WorktreeError("source_root_invalid")
174
+ head = _run(runner, ["rev-parse", "HEAD"])
175
+ if head.returncode != 0 or _decode_line(head.stdout, "git_protocol") != approved_commit:
176
+ raise WorktreeError("source_commit_drift")
177
+ status_result = _run(runner, ["status", "--porcelain=v1", "-z", "--untracked-files=all"])
178
+ if status_result.returncode != 0:
179
+ raise WorktreeError("source_root_invalid")
180
+ if status_result.stdout:
181
+ raise WorktreeError("source_dirty")
182
+ index = _run(runner, ["ls-files", "-s", "-z"])
183
+ if index.returncode != 0:
184
+ raise WorktreeError("source_root_invalid")
185
+ _validate_index(index.stdout)
186
+ source_common_result = _run(runner, ["rev-parse", "--git-common-dir"])
187
+ if source_common_result.returncode != 0:
188
+ raise WorktreeError("source_root_invalid")
189
+ source_common = _resolve_git_path(source, source_common_result.stdout, "source_root_invalid")
190
+
191
+ added = _run(
192
+ runner,
193
+ ["worktree", "add", "--detach", "--no-checkout", str(target), approved_commit],
194
+ )
195
+ if added.returncode != 0:
196
+ raise WorktreeError("worktree_create_failed")
197
+ checkout_runner = GitRunner(target)
198
+ checkout = _run(checkout_runner, ["checkout", "--detach", approved_commit])
199
+ if checkout.returncode != 0:
200
+ raise WorktreeError("worktree_quarantined")
201
+ prepared_root = _canonical_directory(target, "worktree_quarantined")
202
+ prepared_head = _run(checkout_runner, ["rev-parse", "HEAD"])
203
+ if prepared_head.returncode != 0 or _decode_line(prepared_head.stdout, "git_protocol") != approved_commit:
204
+ raise WorktreeError("worktree_quarantined")
205
+ symbolic = _run(checkout_runner, ["symbolic-ref", "-q", "HEAD"])
206
+ if symbolic.returncode == 0:
207
+ raise WorktreeError("worktree_quarantined")
208
+ common_result = _run(checkout_runner, ["rev-parse", "--git-common-dir"])
209
+ if common_result.returncode != 0:
210
+ raise WorktreeError("worktree_quarantined")
211
+ common = _resolve_git_path(prepared_root, common_result.stdout, "worktree_quarantined")
212
+ if common != source_common:
213
+ raise WorktreeError("worktree_quarantined")
214
+ final_status = _run(
215
+ checkout_runner, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]
216
+ )
217
+ if final_status.returncode != 0 or final_status.stdout:
218
+ raise WorktreeError("worktree_quarantined")
219
+ return TaskWorktree(task_id, prepared_root, common, approved_commit)
220
+
221
+
222
+ def cleanup_task_worktree(
223
+ worktree: TaskWorktree,
224
+ *,
225
+ state_root: Path,
226
+ task_id: str,
227
+ terminal_status: str,
228
+ authority_granted: bool,
229
+ ) -> None:
230
+ """Remove a terminal task worktree only under explicit, rebound authority."""
231
+ if authority_granted is not True:
232
+ raise WorktreeError("cleanup_authority_required")
233
+ if (
234
+ not isinstance(worktree, TaskWorktree)
235
+ or task_id != worktree.worktree_id
236
+ or terminal_status not in _CLEANUP_TERMINAL_STATES
237
+ ):
238
+ raise WorktreeError("cleanup_state_invalid")
239
+ state = _canonical_directory(state_root, "state_root_invalid")
240
+ expected = state / "tasks" / task_id
241
+ root = _canonical_directory(worktree.root, "cleanup_containment_invalid")
242
+ try:
243
+ expected_resolved = expected.resolve(strict=True)
244
+ except OSError:
245
+ raise WorktreeError("cleanup_containment_invalid") from None
246
+ if root != expected_resolved or root != worktree.root:
247
+ raise WorktreeError("cleanup_containment_invalid")
248
+ checkout_runner = GitRunner(root)
249
+ common_result = _run(checkout_runner, ["rev-parse", "--git-common-dir"])
250
+ if common_result.returncode != 0:
251
+ raise WorktreeError("cleanup_containment_invalid")
252
+ common = _resolve_git_path(root, common_result.stdout, "cleanup_containment_invalid")
253
+ if common != worktree.git_common_dir or common.name != ".git":
254
+ raise WorktreeError("cleanup_containment_invalid")
255
+ source = _canonical_directory(common.parent, "cleanup_containment_invalid")
256
+ source_runner = GitRunner(source)
257
+ removed = _run(source_runner, ["worktree", "remove", "--force", str(root)])
258
+ if removed.returncode != 0 or root.exists() or root.is_symlink():
259
+ raise WorktreeError("cleanup_failed")