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,212 @@
1
+ """Windows Scheduled Task helpers for the Graphite daemon."""
2
+ from __future__ import annotations
3
+
4
+ import csv
5
+ import platform
6
+ import subprocess
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Sequence
11
+
12
+ DEFAULT_TASK_NAME = "GraphiteDaemon-FProjects"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class TaskCommand:
17
+ executable: Path
18
+ arguments: tuple[str, ...]
19
+ working_dir: Path
20
+
21
+ @property
22
+ def task_run(self) -> str:
23
+ quoted_exe = f'"{self.executable}"'
24
+ rendered_args = " ".join(_quote_arg(arg) for arg in self.arguments)
25
+ return f"{quoted_exe} {rendered_args}".strip()
26
+
27
+
28
+ def _quote_arg(arg: str) -> str:
29
+ if not arg:
30
+ return '""'
31
+ if any(ch.isspace() for ch in arg) or any(ch in arg for ch in '"&()[]{}^=;!,`'):
32
+ return '"' + arg.replace('"', '\\"') + '"'
33
+ return arg
34
+
35
+
36
+ def require_windows() -> None:
37
+ if platform.system().lower() != "windows":
38
+ raise RuntimeError("Windows Scheduled Task integration is only available on Windows")
39
+
40
+
41
+ #: Console scripts a generated launcher must never invoke. Matched on the stem,
42
+ #: so `graphite`, `graphite.cmd` and `graphite.exe` are all refused.
43
+ _CONSOLE_SCRIPT_STEMS = frozenset({"graphite", "graphite-mcp"})
44
+
45
+
46
+ def resolve_launcher_interpreter(explicit: str | None = None) -> Path:
47
+ """The Python interpreter a generated launcher runs. Never a console script.
48
+
49
+ The hazard is not the console script itself. Running a SCRIPT puts the
50
+ script's own directory on `sys.path[0]`; only `-m` puts the CWD there, and
51
+ that is the entire mechanism -- so a pip-generated entry point is not
52
+ cwd-shadowable. The hazard is the `-m` a wrapper may run internally, which
53
+ the generator cannot see and cannot add `-P` to.
54
+
55
+ That is not hypothetical. The resolver this replaced returned
56
+ `shutil.which("graphite")` or `~/.local/bin/graphite.cmd`, and on the
57
+ machine this was found on there is no pip console script at all -- what
58
+ `graphite` names there is a hand-written `.cmd` running
59
+ `python -B -m graphite`. This launcher starts hidden at every login with a
60
+ projects root as its working directory, so a `graphite.py` -- or a
61
+ `graphite/` directory -- dropped at that root beat the installed package,
62
+ and a module-shaped shadow RUNS before it errors. Measured, cwd holding a
63
+ hostile `graphite.py`:
64
+
65
+ python -m graphite -> shadow ran
66
+ python -P -m graphite -> real graphite
67
+ .cmd -> python -B -m graphite -> shadow ran
68
+ .cmd -> python -B -P -m graphite -> real graphite
69
+
70
+ So the launcher runs the interpreter directly and passes `-P` itself: the
71
+ only shape it can guarantee without auditing someone else's wrapper.
72
+
73
+ An explicit override stays supported and is honoured, but one naming a
74
+ console script is refused -- the argument vector built below begins
75
+ `-P -m graphite`, which only an interpreter can accept.
76
+ """
77
+ if explicit:
78
+ path = Path(explicit).expanduser().resolve()
79
+ if not path.exists():
80
+ raise FileNotFoundError(path)
81
+ if path.stem.lower() in _CONSOLE_SCRIPT_STEMS:
82
+ raise ValueError(
83
+ f"{path.name} is a console script and cannot carry `-P`, so a launcher "
84
+ "built from it would be shadowable by a `graphite.py` in its working "
85
+ "directory. Pass a Python interpreter instead."
86
+ )
87
+ return path
88
+ return Path(sys.executable)
89
+
90
+
91
+ def daemon_task_command(
92
+ base_path: Path,
93
+ *,
94
+ graphite_executable: str | None = None,
95
+ scan_interval: float = 15.0,
96
+ discover_interval: float = 90.0,
97
+ max_projects: int = 128,
98
+ max_depth: int = 6,
99
+ max_builds_per_cycle: int = 1,
100
+ build_timeout: float = 240.0,
101
+ debounce: float = 1.0,
102
+ ) -> TaskCommand:
103
+ base = base_path.resolve()
104
+ # `-P` is the whole point: it keeps the working directory off `sys.path[0]`.
105
+ # `-B` is deliberately NOT included -- it only suppresses bytecode and does
106
+ # nothing to `sys.path`, and pairing them here would re-suggest the reading
107
+ # that caused this defect.
108
+ args = (
109
+ "-P",
110
+ "-m",
111
+ "graphite",
112
+ "daemon",
113
+ str(base),
114
+ "--scan-interval",
115
+ _fmt_number(scan_interval),
116
+ "--discover-interval",
117
+ _fmt_number(discover_interval),
118
+ "--max-projects",
119
+ str(max_projects),
120
+ "--max-depth",
121
+ str(max_depth),
122
+ "--max-builds-per-cycle",
123
+ str(max_builds_per_cycle),
124
+ "--build-timeout",
125
+ _fmt_number(build_timeout),
126
+ "--debounce",
127
+ _fmt_number(debounce),
128
+ )
129
+ return TaskCommand(
130
+ executable=resolve_launcher_interpreter(graphite_executable),
131
+ arguments=args,
132
+ working_dir=base,
133
+ )
134
+
135
+
136
+ def _fmt_number(value: float) -> str:
137
+ return str(int(value)) if float(value).is_integer() else str(value)
138
+
139
+
140
+ def create_daemon_task(
141
+ task_name: str,
142
+ command: TaskCommand,
143
+ *,
144
+ force: bool = True,
145
+ start_now: bool = False,
146
+ run: callable = subprocess.run,
147
+ ) -> dict[str, object]:
148
+ require_windows()
149
+ cmd = [
150
+ "schtasks.exe",
151
+ "/Create",
152
+ "/TN",
153
+ task_name,
154
+ "/TR",
155
+ command.task_run,
156
+ "/SC",
157
+ "ONLOGON",
158
+ "/RL",
159
+ "LIMITED",
160
+ ]
161
+ if force:
162
+ cmd.append("/F")
163
+ result = run(cmd, capture_output=True, text=True, check=False)
164
+ payload = _result_payload(result, command=cmd)
165
+ if result.returncode == 0 and start_now:
166
+ started = start_daemon_task(task_name, run=run)
167
+ payload["started"] = started
168
+ return payload
169
+
170
+
171
+ def start_daemon_task(task_name: str, *, run: callable = subprocess.run) -> dict[str, object]:
172
+ require_windows()
173
+ cmd = ["schtasks.exe", "/Run", "/TN", task_name]
174
+ result = run(cmd, capture_output=True, text=True, check=False)
175
+ return _result_payload(result, command=cmd)
176
+
177
+
178
+ def delete_daemon_task(task_name: str, *, run: callable = subprocess.run) -> dict[str, object]:
179
+ require_windows()
180
+ cmd = ["schtasks.exe", "/Delete", "/TN", task_name, "/F"]
181
+ result = run(cmd, capture_output=True, text=True, check=False)
182
+ return _result_payload(result, command=cmd)
183
+
184
+
185
+ def query_daemon_task(task_name: str, *, run: callable = subprocess.run) -> dict[str, object]:
186
+ require_windows()
187
+ cmd = ["schtasks.exe", "/Query", "/TN", task_name, "/FO", "CSV", "/V"]
188
+ result = run(cmd, capture_output=True, text=True, check=False)
189
+ payload = _result_payload(result, command=cmd)
190
+ if result.returncode != 0:
191
+ payload["exists"] = False
192
+ return payload
193
+ payload["exists"] = True
194
+ payload["task"] = _parse_schtasks_csv(result.stdout)
195
+ return payload
196
+
197
+
198
+ def _parse_schtasks_csv(stdout: str) -> dict[str, str]:
199
+ rows = list(csv.DictReader(stdout.splitlines()))
200
+ if not rows:
201
+ return {}
202
+ return {str(k): str(v) for k, v in rows[0].items()}
203
+
204
+
205
+ def _result_payload(result: subprocess.CompletedProcess[str], *, command: Sequence[str]) -> dict[str, object]:
206
+ return {
207
+ "ok": result.returncode == 0,
208
+ "returncode": result.returncode,
209
+ "stdout": (result.stdout or "").strip(),
210
+ "stderr": (result.stderr or "").strip(),
211
+ "command": list(command),
212
+ }