habit-hooks-python 1.0.1__tar.gz → 1.2.0__tar.gz
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.
- habit_hooks_python-1.2.0/.gitignore +26 -0
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/PKG-INFO +2 -2
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/docs/python-plugin.spec.md +134 -33
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/pyproject.toml +1 -1
- habit_hooks_python-1.2.0/src/habit_hooks_python/config.toml +16 -0
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/guides/high-complexity.md +2 -1
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/guides/swallowed-exception.md +2 -1
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/sensors/deptry_sensor.py +16 -5
- habit_hooks_python-1.2.0/tests/test_a_deptry_nobody_installed_is_named.py +35 -0
- habit_hooks_python-1.2.0/tests/test_the_python_files_leave_installed_packages_alone.py +51 -0
- habit_hooks_python-1.0.1/.gitignore +0 -20
- habit_hooks_python-1.0.1/src/habit_hooks_python/config.toml +0 -5
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/__init__.py +0 -0
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/ruff.toml +0 -0
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/sensors/deptry.toml +0 -0
- {habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/sensors/ruff.toml +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Per-plugin Node tool deps (installed via pnpm install; see pnpm-workspace.yaml)
|
|
2
|
+
node_modules/
|
|
3
|
+
dist
|
|
4
|
+
coverage
|
|
5
|
+
.DS_Store
|
|
6
|
+
.idea
|
|
7
|
+
.claude-channel/
|
|
8
|
+
*.tgz
|
|
9
|
+
*.log
|
|
10
|
+
.vscode/
|
|
11
|
+
.venv/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.spec-runs/
|
|
16
|
+
|
|
17
|
+
# Workflow orchestration script (run from ~/.claude, never a repo deliverable)
|
|
18
|
+
.claude/workflows-build-overnight.js
|
|
19
|
+
|
|
20
|
+
# Agent worktrees (created by the harness inside the checkout)
|
|
21
|
+
.claude/worktrees/
|
|
22
|
+
/scratches/
|
|
23
|
+
|
|
24
|
+
# unsupervised-issues run signals (live mode switch + stop flag)
|
|
25
|
+
.unsupervised-issues.mode
|
|
26
|
+
.unsupervised-issues.stop
|
|
@@ -65,6 +65,112 @@ habit-sensors --all | jq 'sort_by(.smell)[] | {smell, language, key: (.issues[0]
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## An empty scope runs no sensor, so ruff never scans the whole tree
|
|
69
|
+
|
|
70
|
+
A scoped run that resolves to **zero** files measured nothing, and no sensor may
|
|
71
|
+
run over it. `ruff` handed no paths falls back to its own default — scan the
|
|
72
|
+
current directory — so without this guard a non-source-only change reports every
|
|
73
|
+
legacy smell in the tree and fails the run (#93). Here `[files]` counts only
|
|
74
|
+
`**/*.py` as source (the plugin default) and `--file README.md` names a doc, so
|
|
75
|
+
the scope is empty even though `legacy.py` carries three real smells. The run
|
|
76
|
+
emits `[]` and exits 0, and the scope layer still says on stderr that it scanned
|
|
77
|
+
nothing.
|
|
78
|
+
|
|
79
|
+
📄.habit-hooks/config.toml
|
|
80
|
+
```toml
|
|
81
|
+
plugins = ["python"]
|
|
82
|
+
|
|
83
|
+
[sensors.deptry]
|
|
84
|
+
disabled = true
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
📄ruff.toml @plugins/python/src/habit_hooks_python/ruff.toml
|
|
88
|
+
|
|
89
|
+
📄pyproject.toml
|
|
90
|
+
```toml
|
|
91
|
+
[project]
|
|
92
|
+
name = "demo"
|
|
93
|
+
version = "0.0.0"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
📄legacy.py
|
|
97
|
+
```python
|
|
98
|
+
import os
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def charge(a, b, c, d):
|
|
102
|
+
unused = 1
|
|
103
|
+
return a + b + c + d
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
📄README.md
|
|
107
|
+
```text
|
|
108
|
+
# docs
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
habit-sensors --file README.md
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
🖥️ ✅
|
|
116
|
+
```json
|
|
117
|
+
[]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
🚨
|
|
121
|
+
```text
|
|
122
|
+
habit-sensors: --file 'README.md' is outside [files]; nothing scanned
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## A sensor narrowed to no files is as empty a scope as a run with none
|
|
126
|
+
|
|
127
|
+
`[sensors.<name>] files` narrows the run's scope for that sensor alone, and it
|
|
128
|
+
can leave that sensor nothing while the run as a whole still measured something.
|
|
129
|
+
Its scope is then as empty as the case above's, with the same consequence — so
|
|
130
|
+
the guard is asked per sensor, not once for the run. Here `legacy.py` is in
|
|
131
|
+
scope and carries three real smells, but `ruff` is narrowed to `src/**/*.py`,
|
|
132
|
+
which holds none of them: ruff never runs, and never falls back to scanning the
|
|
133
|
+
tree it was steered away from.
|
|
134
|
+
|
|
135
|
+
📄.habit-hooks/config.toml
|
|
136
|
+
```toml
|
|
137
|
+
plugins = ["python"]
|
|
138
|
+
|
|
139
|
+
[sensors.ruff]
|
|
140
|
+
files = ["src/**/*.py"]
|
|
141
|
+
|
|
142
|
+
[sensors.deptry]
|
|
143
|
+
disabled = true
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
📄ruff.toml @plugins/python/src/habit_hooks_python/ruff.toml
|
|
147
|
+
|
|
148
|
+
📄pyproject.toml
|
|
149
|
+
```toml
|
|
150
|
+
[project]
|
|
151
|
+
name = "demo"
|
|
152
|
+
version = "0.0.0"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
📄legacy.py
|
|
156
|
+
```python
|
|
157
|
+
import os
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def charge(a, b, c, d):
|
|
161
|
+
unused = 1
|
|
162
|
+
return a + b + c + d
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
habit-sensors --all
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
🖥️ ✅
|
|
170
|
+
```json
|
|
171
|
+
[]
|
|
172
|
+
```
|
|
173
|
+
|
|
68
174
|
## ruff maps a syntax error to parse-error
|
|
69
175
|
|
|
70
176
|
A file ruff cannot parse surfaces as `parse-error` (ruff reports it as
|
|
@@ -152,7 +258,13 @@ deptry needs a `pyproject.toml` to analyse; without one it exits non-zero
|
|
|
152
258
|
instead of emitting findings. The sensor must surface that as a failure — a
|
|
153
259
|
crashed tool is never a clean run. The sensor exits with a code outside the
|
|
154
260
|
findings range, so `habit-sensors` raises, names the sensor on stderr, and exits
|
|
155
|
-
1 rather than printing an empty (false-clean) result.
|
|
261
|
+
1 rather than printing an empty (false-clean) result. The failed run carries only
|
|
262
|
+
the reserved `incomplete-run` marker on stdout
|
|
263
|
+
([habit-sensors.spec.md](../../../docs/habit-sensors.spec.md)).
|
|
264
|
+
|
|
265
|
+
The notice carries deptry's own diagnosis after that first line. That text is
|
|
266
|
+
deptry's to word and names absolute paths, so only the line naming the sensor is
|
|
267
|
+
asserted here.
|
|
156
268
|
|
|
157
269
|
📄.habit-hooks/config.toml
|
|
158
270
|
```toml
|
|
@@ -169,15 +281,19 @@ def fetch(url):
|
|
|
169
281
|
```
|
|
170
282
|
|
|
171
283
|
```bash
|
|
172
|
-
habit-sensors --all
|
|
284
|
+
habit-sensors --all | jq -c '[.[].smell]'
|
|
173
285
|
```
|
|
174
286
|
|
|
175
287
|
🖥️ ❌ 1
|
|
176
288
|
```json
|
|
177
|
-
[]
|
|
289
|
+
["incomplete-run"]
|
|
178
290
|
```
|
|
179
291
|
|
|
180
|
-
|
|
292
|
+
```bash
|
|
293
|
+
habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
🖥️ ❌ 1
|
|
181
297
|
```text
|
|
182
298
|
habit-sensors: sensor 'deptry' failed: ${python} ${dir}/deptry_sensor.py
|
|
183
299
|
```
|
|
@@ -189,7 +305,8 @@ The `ruff` sensor pipes the tool into `jq`. A crashing `ruff` (here, a malformed
|
|
|
189
305
|
naive pipe would let `jq` succeed on empty input and mask the crash as a false-
|
|
190
306
|
clean run. The command sets `pipefail` so the tool's failing exit propagates
|
|
191
307
|
through the pipe; `habit-sensors` then raises, names the sensor on stderr, and
|
|
192
|
-
exits 1
|
|
308
|
+
exits 1, carrying only the reserved `incomplete-run` marker on stdout
|
|
309
|
+
([habit-sensors.spec.md](../../../docs/habit-sensors.spec.md)).
|
|
193
310
|
|
|
194
311
|
📄.habit-hooks/config.toml
|
|
195
312
|
```toml
|
|
@@ -210,40 +327,24 @@ import os
|
|
|
210
327
|
```
|
|
211
328
|
|
|
212
329
|
```bash
|
|
213
|
-
habit-sensors --all
|
|
330
|
+
habit-sensors --all | jq -c '[.[].smell]'
|
|
214
331
|
```
|
|
215
332
|
|
|
216
333
|
🖥️ ❌ 1
|
|
217
334
|
```json
|
|
218
|
-
[]
|
|
335
|
+
["incomplete-run"]
|
|
219
336
|
```
|
|
220
337
|
|
|
221
|
-
|
|
338
|
+
The notice quotes the sensor's whole command — multi-line here, so its first
|
|
339
|
+
line shows only where that command starts — and then ruff's own diagnosis, which
|
|
340
|
+
names the absolute path of the config it could not parse. Only the line naming
|
|
341
|
+
the sensor is stable enough to assert.
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
habit-sensors --all 2>&1 >/dev/null | sed -n 1p
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
🖥️ ❌ 1
|
|
222
348
|
```text
|
|
223
349
|
habit-sensors: sensor 'ruff' failed: set -o pipefail
|
|
224
|
-
ruff check --output-format=json --select=C901,PLR0913,PLR0915,F841,F401,BLE001 ${files} | jq '
|
|
225
|
-
map(. + {smell: ({
|
|
226
|
-
"C901": "high-complexity",
|
|
227
|
-
"PLR0913": "too-many-parameters",
|
|
228
|
-
"PLR0915": "oversized-function",
|
|
229
|
-
"F841": "unused-variable",
|
|
230
|
-
"F401": "unused-import",
|
|
231
|
-
"BLE001": "swallowed-exception",
|
|
232
|
-
"invalid-syntax": "parse-error"
|
|
233
|
-
}[.code])})
|
|
234
|
-
| group_by(.smell)
|
|
235
|
-
| map({
|
|
236
|
-
smell: .[0].smell,
|
|
237
|
-
details: {},
|
|
238
|
-
issues: map({
|
|
239
|
-
key: .filename,
|
|
240
|
-
details: {
|
|
241
|
-
file: .filename,
|
|
242
|
-
line: .location.row,
|
|
243
|
-
column: .location.column,
|
|
244
|
-
message: .message,
|
|
245
|
-
source: ("ruff:" + .code)
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
})'
|
|
249
350
|
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Python plugin defaults.
|
|
2
|
+
language = "python"
|
|
3
|
+
# A project naming no `files` of its own scans what its plugins declare, so this
|
|
4
|
+
# is the first run for anyone `habit-hooks init` set up. An installed package is
|
|
5
|
+
# never the project's source; `site-packages` is where one lands whatever the
|
|
6
|
+
# environment is called, so it answers for a `venv/` as well as a `.venv/`.
|
|
7
|
+
files = ["**/*.py", "!**/site-packages/**", "!**/.venv/**", "!**/venv/**"]
|
|
8
|
+
sensors = ["ruff", "deptry"]
|
|
9
|
+
transformers = []
|
|
10
|
+
|
|
11
|
+
# The ruff sensor pipes its JSON through jq, so a python-only project needs it.
|
|
12
|
+
detectors = [
|
|
13
|
+
{ name = "ruff", kind = "command", install = "pip install ruff" },
|
|
14
|
+
{ name = "deptry", kind = "command", install = "pip install deptry" },
|
|
15
|
+
{ name = "jq", kind = "command", install = "brew install jq" },
|
|
16
|
+
]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
High cyclomatic complexity means one function is making too many decisions at once. The smell is not the number, it is that the function has quietly taken on more than one job. The count is the symptom, tangled responsibilities are the cause.
|
|
2
2
|
|
|
3
|
-
{% include "includes/line_level_issues.md" %}
|
|
4
3
|
Work through it in order:
|
|
5
4
|
|
|
6
5
|
1. **Name what each branch is for.** Give every branch a one-sentence description of the responsibility it handles. If two branches describe the same thing, they belong together. If a branch has no clean name, that path probably wants its own function.
|
|
@@ -10,3 +9,5 @@ Work through it in order:
|
|
|
10
9
|
Reducing the number without reducing the tangle is not a fix. Do not merge conditions with `and`/`or` just to drop a branch, and do not rewrite `if` statements as ternaries to slip under the check. ruff's mccabe does not count ternary expressions, so that trick lowers the score while a human still has to hold every condition in their head.
|
|
11
10
|
|
|
12
11
|
The test is not whether the number dropped. It is whether someone reading the function for the first time can hold it in their head at once. If not, it is still doing too much.
|
|
12
|
+
|
|
13
|
+
{% include "includes/line_level_issues.md" %}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
A broad `except` (`except:`, `except Exception`, `except BaseException`) that discards the error silently is hiding a failure you have not understood, not handling one you planned for. Before you write it, name the specific error you expect and why. That one sentence is usually the fix.
|
|
2
2
|
|
|
3
|
-
{% include "includes/line_level_issues.md" %}
|
|
4
3
|
Work through it in order:
|
|
5
4
|
|
|
6
5
|
1. **Catch only what you can name.** `ValueError`, `KeyError`, `TimeoutError`, whatever the call really raises. If you cannot name it, you are guessing, and every other error should stay free to surface where someone can see it.
|
|
@@ -10,3 +9,5 @@ Work through it in order:
|
|
|
10
9
|
Narrowing the type or adding `# noqa` only to quiet ruff is not a fix if the error is still discarded.
|
|
11
10
|
|
|
12
11
|
If you are unsure whether this catch is a real decision or a reflex, check with a human before you keep it.
|
|
12
|
+
|
|
13
|
+
{% include "includes/line_level_issues.md" %}
|
|
@@ -15,11 +15,22 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def run_deptry(report: Path) -> subprocess.CompletedProcess[str]:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
"""What deptry said, or what a shell says about a deptry nobody installed.
|
|
19
|
+
|
|
20
|
+
``pip install habit-hooks-python`` brings neither detector with it, so this
|
|
21
|
+
is the ordinary state of a machine that has just enabled the plugin — and an
|
|
22
|
+
absent tool raised a ``FileNotFoundError`` out of here, making twenty lines
|
|
23
|
+
of Python internals the sensor's diagnosis (#114). This wrapper is what looks
|
|
24
|
+
for deptry, so it answers the way the shell would have, and that phrase is
|
|
25
|
+
what the run recognises to name the missing tool.
|
|
26
|
+
"""
|
|
27
|
+
command = ["deptry", ".", "--json-output", str(report)]
|
|
28
|
+
try:
|
|
29
|
+
return subprocess.run(command, capture_output=True, text=True)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
return subprocess.CompletedProcess(
|
|
32
|
+
command, 127, "", "deptry: command not found\n"
|
|
33
|
+
)
|
|
23
34
|
|
|
24
35
|
|
|
25
36
|
def deptry_crashed(result: subprocess.CompletedProcess[str], report: Path) -> bool:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""A detector this plugin does not bring with it must still answer in one line.
|
|
2
|
+
|
|
3
|
+
``pip install habit-hooks-python`` installs neither ruff nor deptry, so a machine
|
|
4
|
+
that has just enabled the plugin is the ordinary case, not the edge one. The ruff
|
|
5
|
+
sensor is a shell command, so the shell answers for it; deptry is spawned from
|
|
6
|
+
Python, where an absent tool is a ``FileNotFoundError`` and twenty lines of
|
|
7
|
+
internals would otherwise become the sensor's diagnosis (#114).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
SENSOR = (
|
|
17
|
+
Path(__file__).resolve().parents[1]
|
|
18
|
+
/ "src/habit_hooks_python/sensors/deptry_sensor.py"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_a_deptry_nobody_installed_answers_the_way_a_shell_does(
|
|
23
|
+
tmp_path: Path,
|
|
24
|
+
) -> None:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
[sys.executable, str(SENSOR)],
|
|
27
|
+
cwd=tmp_path,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
env={"PATH": "/nonexistent"},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert result.returncode != 0
|
|
34
|
+
assert result.stdout.strip() == ""
|
|
35
|
+
assert result.stderr.strip() == "deptry: command not found"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""What this plugin means when it says which files are its language's.
|
|
2
|
+
|
|
3
|
+
A project that names no ``files`` of its own scans what its plugins declare, and
|
|
4
|
+
``habit-hooks init`` writes exactly such a config — so what is declared here *is*
|
|
5
|
+
the first run for anyone init set up. A bare ``**/*.py`` reaches into the
|
|
6
|
+
project's virtualenv and reports on every installed package, which buries the
|
|
7
|
+
project's own findings under thousands that are nobody's to fix.
|
|
8
|
+
|
|
9
|
+
``site-packages`` is where an installed package lands whatever the environment is
|
|
10
|
+
called, so it answers for a ``venv/`` as well as a ``.venv/``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import tomllib
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pathspec
|
|
19
|
+
|
|
20
|
+
PACKAGE = Path(__file__).resolve().parents[1] / "src" / "habit_hooks_python"
|
|
21
|
+
|
|
22
|
+
PROJECT_SOURCE = ("src/billing.py", "tests/test_billing.py", "conftest.py")
|
|
23
|
+
DEPENDENCIES = (
|
|
24
|
+
".venv/lib/python3.12/site-packages/attrs/__init__.py",
|
|
25
|
+
"venv/lib/python3.11/site-packages/jinja2/environment.py",
|
|
26
|
+
".tox/py312/lib/python3.12/site-packages/pytest/__init__.py",
|
|
27
|
+
# Not under site-packages, so only the environment's own name excludes these
|
|
28
|
+
# — and it is spelled both ways about equally often.
|
|
29
|
+
".venv/bin/activate_this.py",
|
|
30
|
+
"venv/bin/activate_this.py",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _declared_files() -> pathspec.PathSpec:
|
|
35
|
+
config = tomllib.loads((PACKAGE / "config.toml").read_text(encoding="utf-8"))
|
|
36
|
+
return pathspec.PathSpec.from_lines("gitignore", config["files"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_the_project_s_own_python_is_source() -> None:
|
|
40
|
+
spec = _declared_files()
|
|
41
|
+
|
|
42
|
+
assert [path for path in PROJECT_SOURCE if spec.match_file(path)] == list(
|
|
43
|
+
PROJECT_SOURCE
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_an_installed_package_is_not_this_project_s_source() -> None:
|
|
48
|
+
"""Under either name a virtualenv usually goes by."""
|
|
49
|
+
spec = _declared_files()
|
|
50
|
+
|
|
51
|
+
assert [path for path in DEPENDENCIES if spec.match_file(path)] == []
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Per-plugin Node tool deps (installed via npm ci; see plugins/*/package.json)
|
|
2
|
-
node_modules/
|
|
3
|
-
dist
|
|
4
|
-
coverage
|
|
5
|
-
.DS_Store
|
|
6
|
-
.idea
|
|
7
|
-
.claude-channel/
|
|
8
|
-
*.tgz
|
|
9
|
-
*.log
|
|
10
|
-
.vscode/
|
|
11
|
-
.venv/
|
|
12
|
-
__pycache__/
|
|
13
|
-
*.pyc
|
|
14
|
-
.pytest_cache/
|
|
15
|
-
.spec-runs/
|
|
16
|
-
|
|
17
|
-
/ruff.toml
|
|
18
|
-
|
|
19
|
-
# Workflow orchestration script (run from ~/.claude, never a repo deliverable)
|
|
20
|
-
.claude/workflows-build-overnight.js
|
|
File without changes
|
|
File without changes
|
{habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/sensors/deptry.toml
RENAMED
|
File without changes
|
{habit_hooks_python-1.0.1 → habit_hooks_python-1.2.0}/src/habit_hooks_python/sensors/ruff.toml
RENAMED
|
File without changes
|