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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
graphite/windows_task.py
ADDED
|
@@ -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
|
+
}
|