gitlogstats 1.0.5__tar.gz → 1.0.7__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.
Files changed (35) hide show
  1. gitlogstats-1.0.7/.claude/CLAUDE.md +83 -0
  2. gitlogstats-1.0.7/.claude/agents/developer-agent.md +40 -0
  3. gitlogstats-1.0.7/.claude/agents/spec-reviewer.md +38 -0
  4. gitlogstats-1.0.7/.claude/hooks/format-edited.sh +44 -0
  5. gitlogstats-1.0.7/.claude/hooks/guard-paths.sh +111 -0
  6. gitlogstats-1.0.7/.claude/hooks/guard-paths.test.mjs +100 -0
  7. gitlogstats-1.0.7/.claude/hooks/verify.sh +55 -0
  8. gitlogstats-1.0.7/.claude/settings.json +36 -0
  9. gitlogstats-1.0.7/.claude/skills/phase-handoff/SKILL.md +65 -0
  10. gitlogstats-1.0.7/.claude/skills/stale-check/SKILL.md +38 -0
  11. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/.gitignore +3 -0
  12. gitlogstats-1.0.7/.prettierrc.json +5 -0
  13. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/PKG-INFO +34 -8
  14. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/Pipfile +1 -0
  15. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/Pipfile.lock +51 -51
  16. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/README.md +31 -6
  17. gitlogstats-1.0.7/docs/SPEC.md +185 -0
  18. gitlogstats-1.0.7/package-lock.json +43 -0
  19. gitlogstats-1.0.7/package.json +19 -0
  20. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/pyproject.toml +2 -2
  21. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/requirements.txt +1 -0
  22. gitlogstats-1.0.7/scripts/board/config.mjs +89 -0
  23. gitlogstats-1.0.7/scripts/board/derive.mjs +224 -0
  24. gitlogstats-1.0.7/scripts/board/derive.test.mjs +111 -0
  25. gitlogstats-1.0.7/scripts/board/manifest.yaml +282 -0
  26. gitlogstats-1.0.7/scripts/board/sync.mjs +431 -0
  27. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/src/gitlogstats/__main__.py +77 -26
  28. gitlogstats-1.0.7/src/gitlogstats/git_logs_parser.py +286 -0
  29. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/tests/test_git_logs_parser.py +288 -60
  30. gitlogstats-1.0.7/tests/test_integration.py +592 -0
  31. gitlogstats-1.0.5/src/gitlogstats/git_logs_parser.py +0 -221
  32. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/.github/workflows/build.yaml +0 -0
  33. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/LICENSE +0 -0
  34. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/repos.txt +0 -0
  35. {gitlogstats-1.0.5 → gitlogstats-1.0.7}/src/gitlogstats/__init__.py +0 -0
@@ -0,0 +1,83 @@
1
+ # Instructions to Claude
2
+
3
+ `gitlogstats` is a Python command-line tool (`src/gitlogstats/`) that reports developers' contributions to git repositories. The repository also holds JavaScript project tooling: the project-board scripts (`scripts/board/`) and the Claude Code hook tests (`.claude/hooks/`). Each language has its own conventions below.
4
+
5
+ ## Testing
6
+
7
+ Before finalizing major changes to code, write thorough unit tests and integration tests, aiming for 100% coverage of `src/gitlogstats/`. Statistics must be checked against **real git repositories** built in the tests (see `tests/test_integration.py`), not only against hand-written `git log` text: a mocked log can only confirm the format the test author expected. All passing tests should be reproducible during regression testing as we develop new code.
8
+
9
+ The checks, all run by the Stop hook (`.claude/hooks/verify.sh`):
10
+
11
+ | Area | Command |
12
+ | ------------------------- | ----------------------------------------------------------- |
13
+ | Python tests | `python -m pytest -q` (or `npm test`) |
14
+ | Python coverage | `python -m pytest --cov` |
15
+ | Formatting (Python + JS) | `npm run format:check` (Black and Prettier) |
16
+ | Board script tests | `npm run test:scripts` |
17
+ | Hook tests | `npm run test:hooks` |
18
+
19
+ ### Reading a green result
20
+
21
+ A passing check is not automatically evidence. Before treating one as proof, ask:
22
+
23
+ - **What would this value look like if the thing that sets it had never happened?** If the answer is "the same", it is not evidence. A count of zero failures, a field read off an object that does not have it, a measurement that never ran, and a package built before the change all report exactly what success reports.
24
+ - **Does this pass mean "nothing broke" or "the thing works"?** They are indistinguishable from outside. A green suite proves nothing about a scenario the data under test never exercises — e.g. a parser tested only on single-line commit messages says nothing about multi-line ones.
25
+ - **Which tests did this green actually run?** `pytest` covers the Python tool only; the board scripts and hooks have their own `node --test` suites. A green from one says nothing about the others.
26
+ - **Would the new test fail without the change?** Where it is cheap, run it against the old code (e.g. `git show HEAD:<file>` into a scratch directory) and confirm it fails.
27
+
28
+ The first question you can and should ask about your own work: whether an artifact is what you think it is is checkable from the inside. **A prediction is not** — when you expect a particular answer, the expectation is what is doing the asking, so that check has to come from someone with no stake in the outcome. Where work is split across people or sessions, spend one question there rather than on general review.
29
+
30
+ Fix the defect, not the instrument. If a check reports something correct as a fault, the rule is measuring the wrong thing — loosening its threshold hides the real fault it was built to catch.
31
+
32
+ ## Python conventions
33
+
34
+ - Follow **PEP 8**, formatted with **Black** (default settings, 88-character lines). Run `black src tests` (or `npm run format`); the PostToolUse hook formats edited `.py` files automatically, and `black --check src tests` must pass.
35
+ - Naming per PEP 8: `snake_case` for functions, methods and variables; `PascalCase` for classes; `UPPER_SNAKE_CASE` for module constants.
36
+ - Imports at the top of the module: standard library, then third party, then local, one group each.
37
+ - Support the Python versions in `pyproject.toml` (`requires-python`) and CI's matrix; avoid syntax newer than they allow.
38
+ - Run git through `subprocess` with an **argument list**, never a shell string, so names, paths and patterns containing spaces or quotes are passed intact.
39
+ - Never parse human-oriented `git log` output (commit messages, `Author:` lines) with regular expressions. Ask git for a machine-readable `--format` instead.
40
+ - Results go to standard output; diagnostics (`--verbose`) go to standard error.
41
+ - Runtime code uses the standard library only; test and build tools belong in the `dev` extra / `[dev-packages]`.
42
+
43
+ ## JavaScript conventions
44
+
45
+ - JavaScript in this repository is project tooling only (`scripts/`, `.claude/hooks/`), run with Node 20+.
46
+ - Format with **Prettier** (`.prettierrc.json`: no semicolons, single quotes, arrow parentheses avoided). Run `npm run format`; the PostToolUse hook formats edited files automatically.
47
+ - Use ES Modules (`import`/`export`, `.mjs` files) and the Node standard library (`node:fs`, `node:test`, …). Add dependencies only when the standard library cannot do the job.
48
+ - Test with the built-in `node:test` runner in `*.test.mjs` files next to the code they test. Scripts that run on execution must guard that with an "is main module" check so tests can import them.
49
+
50
+ ## Code comments
51
+
52
+ Leave standard docstring comments for all modules, functions, and for large blocks of complicated code — Python docstrings, JSDoc blocks in JavaScript. Avoid jargon and keep comments concise.
53
+
54
+ ## Specification
55
+
56
+ The specification is in `docs/SPEC.md`. It describes what the tool does today and what is planned, and it is the source of the GitHub project board. Update it in the same change whenever behaviour changes or a requirement is completed.
57
+
58
+ The project-board scripts (`scripts/board`) parse `docs/SPEC.md`, so keep these formats when editing it:
59
+
60
+ - **Sections are `### <N>. <Title>` headings** (e.g. `### 2. Contribution Statistics`); they label a requirement's section.
61
+ - **Requirements are `#### <ID> <Title>` subheadings** — `<ID>` is `FAMILY-N` (uppercase family letters, hyphen, number; e.g. `STAT-5`, `REPO-2`). The prose beneath the heading, up to the next heading, becomes the issue body. Families: `REPO`, `STAT`, `FILT`, `OUT`, `CLI`, `PKG` (labels in `scripts/board/config.mjs`).
62
+ - **Each requirement has a `**Status:** <status>` line**, one of `Todo`, `In Progress`, `Done`. An unknown status fails `board:derive`.
63
+ - **Future Work items are `-` bullets** under `### <N>. Future Work`; they become phase-2 issues keyed by a slug of their first words, so rewording a bullet's start re-keys it.
64
+ - **Never change an existing ID** — it keys the issue; renaming or renumbering orphans the old issue and creates a new one.
65
+
66
+ ## Git & project-board workflow
67
+
68
+ Changes are tracked on the GitHub project board (https://github.com/users/bloombar/projects/4, linked to this repository) and follow a branch → PR flow. Follow this **by default**:
69
+
70
+ - **Do not commit code changes directly to the default branch (`master`).** Create a feature branch named `feat/<REQ-ID>-<slug>` (e.g. `feat/STAT-6-mailmap`; use a short descriptive slug when no requirement id applies). **Exception:** documentation (Markdown and anything under `docs/`) and project tooling under `scripts/` and `.claude/` may be committed directly to the default branch without a PR.
71
+ - **Open a pull request** whose description includes `Closes #N` — `N` is the board issue's number — so the issue closes on merge. Commit and push only when asked, and run the checks above first.
72
+ - **Do not hand-create board issues.** The board is generated from the SPEC: `npm run board:derive` regenerates `scripts/board/manifest.yaml`, then `npm run board:sync` creates or updates the issues (`-- --dry-run` previews; `-- --reconcile` forces SPEC statuses back onto the board). A requirement marked `Done` in the SPEC is always closed and moved to Done; other status moves are left to the board unless `--reconcile` is passed.
73
+ - Pushing to `master` publishes a release to PyPI (see `.github/workflows/build.yaml`), so bump `version` in `pyproject.toml` when a change to the package should be released.
74
+
75
+ If asked to use a different workflow (e.g. commit code straight to the default branch), **remind the user that it diverges from this flow and confirm before proceeding** — then honor the confirmed request.
76
+
77
+ ## Protected paths
78
+
79
+ **IMPORTANT:** `.pypirc` holds the PyPI publishing token and `.env*` files hold any other local credentials. A `PreToolUse` hook (`.claude/hooks/guard-paths.sh`, tested by `npm run test:hooks`) blocks writes to them. A block is a signal to stop and report — never route around it.
80
+
81
+ ## Agent workflow
82
+
83
+ Larger work can run as a supervisor/developer split: `.claude/agents/developer-agent.md` implements a scoped slice against a brief, `.claude/agents/spec-reviewer.md` reviews the diff in fresh context, and the agent doing the work is never the one grading it. The brief template and definition of done are in the `phase-handoff` skill; run the `stale-check` skill before starting or reviewing a slice.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: developer-agent
3
+ description: Implements one scoped slice of gitlogstats work against a self-contained brief, then reports the evidence that it works. Invoked by the supervisor, one invocation per slice.
4
+ tools: Read, Edit, Write, Grep, Glob, Bash
5
+ model: sonnet
6
+ ---
7
+
8
+ You are the implementer on gitlogstats, a Python command-line tool that reports developers' contributions to git repositories. A supervisor scopes the work and reviews it; you write the code. The repository also has JavaScript tooling (`scripts/board/`, `.claude/hooks/`); follow the conventions for whichever language you touch, as set out in `.claude/CLAUDE.md`.
9
+
10
+ ## What you receive
11
+
12
+ A brief that names the files and interfaces involved, the SPEC requirement ids it satisfies, what is explicitly out of scope, and **the exact command that proves it works**. Treat the brief as the contract.
13
+
14
+ ## How you work
15
+
16
+ 0. **Check for stale branches and PRs first** — run the `stale-check` skill before touching anything. If an open PR touches files in your slice, or your branch is behind its base, **stop and report it** rather than editing in parallel.
17
+ 1. **Read before writing.** Match the surrounding code — its comment density, naming, and idiom. Python follows PEP 8 and is formatted with Black; JavaScript is ES Modules formatted with Prettier (no semicolons).
18
+ 2. **Stay inside the slice.** Do not refactor code the brief did not name, do not add abstraction for a future the brief did not describe, and do not fix unrelated problems you notice. Note them in your report instead. Scope creep makes the diff unreviewable.
19
+ 3. **Write the test first where it is cheap to.** New behaviour needs a test that fails without your change. For anything that affects the statistics, that means an integration test that builds a real git repository (see `tests/test_integration.py`) — a hand-written `git log` string only confirms the format you already expected.
20
+ 4. **Run the brief's check.** Then run the project checks: `python -m pytest -q` and `black --check src tests` for Python; `npm run format:check`, `npm run test:scripts` (when the slice touches `scripts/`) and `npm run test:hooks` (when it touches `.claude/hooks/`) for JavaScript. Iterate until green.
21
+ 5. **Keep the SPEC in step.** If the slice completes or changes a requirement, update its text and `**Status:**` line in `docs/SPEC.md` and run `npm run board:derive`.
22
+ 6. **Cite requirement ids in comments** where you implement them (`# STAT-5`, `// REPO-2`), so the SPEC and the code stay traceable in both directions.
23
+
24
+ ## What you must not do
25
+
26
+ - **Never write to `.pypirc` or `.env*`.** They hold the PyPI publishing token and other credentials. A hook blocks these; if you hit that block, stop and report it — do not find another route around it.
27
+ - Never `git push`, never merge, never open a PR, never publish to PyPI. The supervisor owns the git history and releases.
28
+ - Never claim a check passed without having run it.
29
+
30
+ ## What you return
31
+
32
+ Keep it short and factual:
33
+
34
+ - **What changed** — the files, one line each.
35
+ - **Evidence** — the exact commands you ran and their output. Paste the real output, including test counts. If something failed and you fixed it, say what it was.
36
+ - **Decisions** — any judgment call you made that the brief did not settle, and why.
37
+ - **Out of scope** — anything you noticed but deliberately left alone.
38
+ - **Blocked** — anything you could not finish, and precisely what it needs.
39
+
40
+ An honest "this part does not work yet, here is the failing output" is worth far more than a confident summary that turns out to be wrong two slices later.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: spec-reviewer
3
+ description: Adversarially reviews a slice's diff against its brief and docs/SPEC.md in a fresh context. Read-only. Checks conformance, scope, test quality, and independently re-runs the brief's verification.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: opus
6
+ ---
7
+
8
+ You review a diff you did not write, against the brief that requested it. You have not seen the reasoning that produced the change, and that is the point: you judge the result on its own terms.
9
+
10
+ You are **read-only**. Never edit a file, never commit, never push.
11
+
12
+ ## What you check, in order
13
+
14
+ 0. **Staleness.** Run the `stale-check` skill. A diff built on a stale base, or one that overlaps an open PR, is a finding in its own right — report it as **must-fix** regardless of how good the code is.
15
+ 1. **Conformance.** Does every requirement in the brief actually appear in the diff? Name any that do not. A requirement that was restated in a comment but not implemented is not implemented.
16
+ 2. **Verification, reproduced.** Run the brief's check yourself — plus `python -m pytest -q` and `black --check src tests`, and `npm run format:check`, `npm run test:scripts` / `npm run test:hooks` when the slice touches `scripts/` or `.claude/hooks/`. **Do not trust reported output; produce your own.** If your result disagrees with what was reported, that is the most important finding in your review.
17
+ 3. **Test quality.** Would each new test fail without the change? Check by reading, and where it is cheap, by running it against the base version of the code. Assertions that hold trivially, tests that assert a mock was called rather than that behaviour is right, and expected values copied from the code's own output all count as missing tests. Changes to the statistics need an integration test on a real git repository.
18
+ 4. **Scope.** Did anything outside the slice change? An unrelated refactor, a dependency bump, a reformat of an untouched file, a stray `print` or `console.log`.
19
+ 5. **SPEC traceability.** Are the cited requirement ids real and correct? If the slice adds or completes requirements, is `docs/SPEC.md` updated — `#### <FAMILY>-<N> <Title>` under a `### <N>. <Title>` section, with a correct `**Status:**` line — and does `npm run board:derive` leave `scripts/board/manifest.yaml` matching the committed version?
20
+ 6. **The expensive mistakes**, weighted heavily when the slice touches them:
21
+ - **Statistical accuracy** — every commit credited to exactly one author; no double counting (e.g. merges); exact name matching; date boundaries inclusive; results independent of commit-message content.
22
+ - **Output contracts** — CSV columns and order, JSON validity across several repositories, nothing but results on standard output.
23
+ - **Command-line compatibility** — existing flags and defaults keep working; a changed default is called out in the README.
24
+ - **Secrets** — `.pypirc` and `.env*` untouched; no tokens in code, tests or logs.
25
+
26
+ ## How to report
27
+
28
+ Rank findings by severity and label each one:
29
+
30
+ - **must-fix** — wrong statistics or output; data loss; secret exposure; a stated brief requirement unimplemented; a test that passes without the change.
31
+ - **cheap-fix** — duplication or naming _within files the slice already touched_; formatting that breaks Black or Prettier.
32
+ - **note** — anything else.
33
+
34
+ For each finding give the file and line, one sentence on the defect, and a concrete failure scenario: inputs or state, then the wrong result. A finding you cannot write a failure scenario for is a **note**, not a must-fix.
35
+
36
+ **Do not pad the review.** You were asked to find gaps, which means you will be tempted to report some even when the work is sound; that tendency produces defensive scaffolding and tests for impossible cases. If the slice is correct and complete, say so and report nothing. "No must-fix findings" is a valid and useful review.
37
+
38
+ Style preferences, architectural opinions about code the slice did not touch, and speculative hardening are **notes** at most, and usually should not be reported at all.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # Formats a file right after it is edited, so formatting never shows up as
3
+ # review noise in the diff: Black for Python, Prettier for JavaScript and the
4
+ # other web-tooling formats.
5
+ #
6
+ # Deliberately best-effort: this runs after every edit, and a formatter that is
7
+ # missing or unhappy must never block the work. Always exits 0.
8
+
9
+ set -uo pipefail
10
+
11
+ payload=$(cat)
12
+
13
+ file_path=$(printf '%s' "$payload" | python3 -c "
14
+ import json, sys
15
+ try:
16
+ data = json.load(sys.stdin)
17
+ except Exception:
18
+ sys.exit(0)
19
+ value = (data.get('tool_input') or {}).get('file_path')
20
+ if value:
21
+ print(value)
22
+ " 2>/dev/null || true)
23
+
24
+ [ -n "$file_path" ] || exit 0
25
+ [ -f "$file_path" ] || exit 0
26
+
27
+ cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
28
+
29
+ case "$file_path" in
30
+ *.py)
31
+ # Black is the Python formatter (PEP 8 compliant); use it if installed.
32
+ if command -v black >/dev/null 2>&1; then
33
+ black --quiet "$file_path" >/dev/null 2>&1 || true
34
+ fi
35
+ ;;
36
+ *.js | *.mjs | *.cjs | *.json | *.yml | *.yaml)
37
+ # The generated board manifest is written by derive.mjs, not formatted.
38
+ case "$file_path" in */scripts/board/manifest.yaml | scripts/board/manifest.yaml) exit 0 ;; esac
39
+ [ -d node_modules ] || exit 0
40
+ npx --no-install prettier --write "$file_path" >/dev/null 2>&1 || true
41
+ ;;
42
+ esac
43
+
44
+ exit 0
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ # Blocks writes to files that must never be modified by an agent:
3
+ #
4
+ # .pypirc the PyPI API token used to publish the package
5
+ # .env, .env.* any local credentials (e.g. a GitHub token)
6
+ #
7
+ # `.env.example` files are the tracked templates and carry no secrets, so they
8
+ # are explicitly allowed — the rule above would otherwise block editing them.
9
+ #
10
+ # CLAUDE.md rules are advisory; this is not. Exit code 2 blocks the tool call and
11
+ # shows stderr to the model, so a blocked write is a signal to stop, not an
12
+ # obstacle to route around.
13
+ #
14
+ # Reads the PreToolUse hook payload as JSON on stdin.
15
+
16
+ set -euo pipefail
17
+
18
+ payload=$(cat)
19
+
20
+ # Pull the fields we need without assuming jq is present.
21
+ extract() {
22
+ printf '%s' "$payload" | python3 -c "
23
+ import json, sys
24
+ try:
25
+ data = json.load(sys.stdin)
26
+ except Exception:
27
+ sys.exit(0)
28
+ value = data
29
+ for key in sys.argv[1].split('.'):
30
+ if not isinstance(value, dict):
31
+ sys.exit(0)
32
+ value = value.get(key)
33
+ if value is None:
34
+ sys.exit(0)
35
+ print(value)
36
+ " "$1" 2>/dev/null || true
37
+ }
38
+
39
+ tool=$(extract tool_name)
40
+ file_path=$(extract tool_input.file_path)
41
+ command=$(extract tool_input.command)
42
+
43
+ # Paths are matched on the tail of the path, so an absolute path, a relative one
44
+ # and a path reached via a symlink all trip the same rule.
45
+ is_protected() {
46
+ case "$1" in
47
+ # The tracked templates come first: they are the one .env.* that is public.
48
+ */.env.example | .env.example) return 1 ;;
49
+ */.env | .env | */.env.* | .env.*) return 0 ;;
50
+ */.pypirc | .pypirc) return 0 ;;
51
+ *) return 1 ;;
52
+ esac
53
+ }
54
+
55
+ deny() {
56
+ echo "BLOCKED: $1" >&2
57
+ echo "This path is protected because it holds live credentials." >&2
58
+ echo "Do not work around this. Stop and report the block." >&2
59
+ exit 2
60
+ }
61
+
62
+ case "$tool" in
63
+ Write | Edit | MultiEdit | NotebookEdit)
64
+ if [ -n "$file_path" ] && is_protected "$file_path"; then
65
+ deny "write to $file_path"
66
+ fi
67
+ ;;
68
+ Bash)
69
+ # Only guard commands that plainly mutate a protected path. A read (cat,
70
+ # grep, a `grep -E '^KEY=' .env` to list key names) is allowed: the risk
71
+ # here is loss and leakage through modification, not inspection.
72
+ #
73
+ # Matching is restricted to the FIRST LINE of the command. Everything after
74
+ # a newline is a heredoc body, a commit message, or a multi-line script
75
+ # argument — text that merely mentions `.env` rather than acting on it.
76
+ # Without this, writing a commit message about the guard trips the guard,
77
+ # which is how an over-broad rule teaches people to disable it.
78
+ if [ -n "$command" ]; then
79
+ first_line=${command%%$'\n'*}
80
+
81
+ # Force-add: the one operation that puts a gitignored secret into history.
82
+ case "$first_line" in
83
+ *"git add "*" -f"* | *"git add "*" --force"* | *"git add -f"* | *"git add --force"*)
84
+ deny "git add --force can commit an ignored secret: $first_line"
85
+ ;;
86
+ esac
87
+
88
+ # Destructive operations naming a protected path.
89
+ case "$first_line" in
90
+ rm\ * | *\;\ rm\ * | *"&& rm "* | *"| rm "* | *truncate\ * | *shred\ *)
91
+ case "$first_line" in
92
+ *.env.example*) ;;
93
+ *.env | *.env\ * | *.env.* | *.pypirc*)
94
+ deny "command would delete a protected path: $first_line"
95
+ ;;
96
+ esac
97
+ ;;
98
+ esac
99
+
100
+ # Redirection into a protected path (`> .env`, `>> server/.env.production`).
101
+ case "$first_line" in
102
+ *">"*.env.example*) ;;
103
+ *">"*/.env* | *">"\ .env* | *">"*.pypirc*)
104
+ deny "command would overwrite a protected path: $first_line"
105
+ ;;
106
+ esac
107
+ fi
108
+ ;;
109
+ esac
110
+
111
+ exit 0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Tests for the PreToolUse path guard.
3
+ *
4
+ * This hook is the only mechanism standing between an agent and the files
5
+ * holding this project's live credentials — `.pypirc` (the PyPI publishing
6
+ * token) and any `.env` files. A regression here is silent — the hook simply
7
+ * stops blocking — so it is tested like production code.
8
+ */
9
+ import { test } from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+ import { execFileSync } from 'node:child_process'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { dirname, join } from 'node:path'
14
+
15
+ const HOOK = join(dirname(fileURLToPath(import.meta.url)), 'guard-paths.sh')
16
+
17
+ /** Runs the hook with a payload; returns its exit code. */
18
+ const runHook = payload => {
19
+ try {
20
+ execFileSync(HOOK, {
21
+ input: JSON.stringify(payload),
22
+ stdio: ['pipe', 'pipe', 'pipe'],
23
+ })
24
+ return 0
25
+ } catch (error) {
26
+ return error.status
27
+ }
28
+ }
29
+
30
+ const BLOCKED = 2
31
+ const ALLOWED = 0
32
+
33
+ const write = file_path => ({ tool_name: 'Write', tool_input: { file_path } })
34
+ const bash = command => ({ tool_name: 'Bash', tool_input: { command } })
35
+
36
+ test('blocks writes to files holding live credentials', () => {
37
+ for (const path of [
38
+ '.pypirc',
39
+ '/abs/path/gitlogstats/.pypirc',
40
+ '.env',
41
+ '.env.local',
42
+ 'scripts/.env',
43
+ '/abs/path/gitlogstats/.env.production',
44
+ ])
45
+ assert.equal(runHook(write(path)), BLOCKED, path)
46
+ })
47
+
48
+ test('blocks a force-add, which is how a gitignored secret reaches the repo', () => {
49
+ assert.equal(runHook(bash('git add -f .pypirc')), BLOCKED)
50
+ assert.equal(runHook(bash('git add --force .env')), BLOCKED)
51
+ })
52
+
53
+ test('blocks commands that delete or overwrite a protected path', () => {
54
+ assert.equal(runHook(bash('rm -f .pypirc')), BLOCKED)
55
+ assert.equal(runHook(bash('rm .env')), BLOCKED)
56
+ assert.equal(runHook(bash('echo token > .pypirc')), BLOCKED)
57
+ assert.equal(runHook(bash('echo X=1 > scripts/.env')), BLOCKED)
58
+ })
59
+
60
+ test('allows the tracked env templates', () => {
61
+ // `.env.example` is committed and carries no secrets; the `.env.*` rule
62
+ // would block it without an explicit exception.
63
+ for (const path of ['.env.example', 'scripts/.env.example'])
64
+ assert.equal(runHook(write(path)), ALLOWED, path)
65
+ assert.equal(runHook(bash('rm -f .env.example')), ALLOWED)
66
+ })
67
+
68
+ test('allows ordinary source files', () => {
69
+ assert.equal(runHook(write('src/gitlogstats/git_logs_parser.py')), ALLOWED)
70
+ assert.equal(runHook(write('scripts/board/sync.mjs')), ALLOWED)
71
+ assert.equal(runHook(write('docs/SPEC.md')), ALLOWED)
72
+ assert.equal(runHook(bash('python -m pytest -q')), ALLOWED)
73
+ })
74
+
75
+ test('allows reads of protected paths — the risk is modification, not inspection', () => {
76
+ assert.equal(runHook(bash('grep -c password .pypirc')), ALLOWED)
77
+ })
78
+
79
+ test('only the first line is inspected, so prose about a protected path is not a command', () => {
80
+ // Regression: the guard originally matched the whole command string, so a
81
+ // commit message describing the guard tripped the guard. An over-broad rule
82
+ // that blocks honest work is how people learn to disable the rule.
83
+ const message = [
84
+ "git commit -F - <<'EOF'",
85
+ 'Add guardrails',
86
+ '',
87
+ 'Blocks writes to .pypirc and .env files, and refuses git add -f.',
88
+ 'EOF',
89
+ ].join('\n')
90
+ assert.equal(runHook(bash(message)), ALLOWED)
91
+ assert.equal(runHook(bash('git add -A')), ALLOWED)
92
+ assert.equal(runHook(bash('grep -r "pypirc" .gitignore')), ALLOWED)
93
+ assert.equal(runHook(bash('echo "never commit .pypirc"')), ALLOWED)
94
+ })
95
+
96
+ test('a malformed payload does not block work', () => {
97
+ // A hook that fails open on garbage is right: it runs before every tool call,
98
+ // and an unparseable payload is a bug in the harness, not an attack.
99
+ assert.equal(runHook({}), ALLOWED)
100
+ })
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: the turn does not end until the project's checks pass.
3
+ #
4
+ # Runs the gate CLAUDE.md defines for a PR:
5
+ # Python black --check, pytest (unit + integration)
6
+ # JavaScript prettier --check, the board-script tests and the hook tests
7
+ #
8
+ # Only runs checks whose tools are installed: a missing tool must read as
9
+ # "nothing to check", never as a failure, or the hook blocks every turn for the
10
+ # wrong reason.
11
+ #
12
+ # Exit 2 blocks the turn and shows stderr to the model.
13
+
14
+ set -uo pipefail
15
+
16
+ cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
17
+
18
+ failures=""
19
+
20
+ run_check() {
21
+ local name="$1"
22
+ shift
23
+ local output
24
+ if ! output=$("$@" 2>&1); then
25
+ failures="${failures}
26
+ --- ${name} ---
27
+ ${output}
28
+ "
29
+ fi
30
+ }
31
+
32
+ # Python: the tool itself.
33
+ if command -v black >/dev/null 2>&1; then
34
+ run_check "black --check" black --check --quiet src tests
35
+ fi
36
+ if python -c "import pytest" >/dev/null 2>&1; then
37
+ run_check "pytest" python -m pytest -q
38
+ fi
39
+
40
+ # JavaScript: project tooling (scripts/, .claude/hooks/).
41
+ if [ -f package.json ] && [ -d node_modules ]; then
42
+ run_check "prettier --check" npx --no-install prettier --check "scripts/**/*.mjs" ".claude/**/*.mjs"
43
+ run_check "npm run test:scripts" npm run --silent test:scripts
44
+ run_check "npm run test:hooks" npm run --silent test:hooks
45
+ fi
46
+
47
+ if [ -n "$failures" ]; then
48
+ {
49
+ echo "Checks failed. Fix these before ending the turn:"
50
+ echo "$failures"
51
+ } >&2
52
+ exit 2
53
+ fi
54
+
55
+ exit 0
@@ -0,0 +1,36 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-paths.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Write|Edit|MultiEdit",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-edited.sh"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify.sh"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
36
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: phase-handoff
3
+ description: The brief template and definition of done for handing a slice to developer-agent. Use when scoping work on gitlogstats.
4
+ ---
5
+
6
+ # Handing off a slice
7
+
8
+ A slice is work small enough that its **diff can be reviewed in one pass**. If you cannot describe the change in a few sentences, split it.
9
+
10
+ ## The brief template
11
+
12
+ A brief without a runnable check is not ready to hand over. That check is the whole point: it is what lets the loop close without a human watching it.
13
+
14
+ ````markdown
15
+ ## Slice: <short name>
16
+
17
+ **SPEC ids:** <FAMILY-N, …> (must already exist in docs/SPEC.md, or be added by this slice)
18
+ **Branch:** feat/<REQ-ID>-<slug>, cut from `master` (the default branch)
19
+
20
+ ### Goal
21
+
22
+ One paragraph. What exists after this slice that did not before.
23
+
24
+ ### Files and interfaces
25
+
26
+ - `src/gitlogstats/<file>.py` — what it must contain
27
+ - Reuse `existingHelper()` from an existing module rather than writing a new one
28
+
29
+ ### Out of scope
30
+
31
+ Explicitly. Name the adjacent things you do NOT want touched — this is the line that keeps the diff reviewable, and the reviewer checks it.
32
+
33
+ ### Verification
34
+
35
+ ```bash
36
+ <the exact command, and what its output must show>
37
+ ```
38
+
39
+ ### Notes
40
+
41
+ Prior art elsewhere in this repo, and any decision already taken that constrains the implementation.
42
+ ````
43
+
44
+ ## Definition of done
45
+
46
+ - `python -m pytest -q` and `black --check src tests` pass — plus `npm run format:check`, and `npm run test:scripts` if the slice touched `scripts/` or `npm run test:hooks` if it touched `.claude/hooks/`
47
+ - Both reviewers have run and every **must-fix** finding is resolved
48
+ - New behaviour has a test that **fails without the change**; statistics changes have an integration test on a real git repository
49
+ - `docs/SPEC.md` describes the new behaviour, with the requirement's `**Status:**` updated
50
+ - `npm run board:derive` has been run and `scripts/board/manifest.yaml` is committed with the change
51
+ - Requirement ids cited in code comments where implemented
52
+ - `README.md` updated if a command-line option, default or output changed
53
+ - Judgment calls the brief did not settle are recorded in the PR description
54
+ - The PR body carries `Closes #N`
55
+
56
+ ## The loop
57
+
58
+ 1. Run `stale-check`. An open PR touching these files means coordinate, not proceed.
59
+ 2. Spawn `developer-agent` with the brief.
60
+ 3. Run `/code-review` and `spec-reviewer` in parallel on the diff. Effort scales with blast radius — high for anything that changes how statistics are computed or what is printed to standard output, since users compare and grade on those numbers.
61
+ 4. Triage. **must-fix** → back to the developer. **cheap-fix** → only within files already touched. **note** → the PR description or a Future Work bullet in the SPEC, and ship.
62
+ 5. Commit, push, open the PR with `Closes #N` (verify the number — issues are not numbered in family order).
63
+ 6. After merge, run `npm run board:sync` if the SPEC changed, so the board reflects it.
64
+
65
+ **Two rounds of rework, maximum.** A third means the slice was mis-scoped rather than merely wrong — re-split it and write a sharper brief.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: stale-check
3
+ description: Check for stale branches, open PRs, and drift from the base branch before starting or reviewing a slice. Run this before any new task.
4
+ ---
5
+
6
+ # Stale branch & PR check
7
+
8
+ Run this **before starting a slice** and **before reviewing one**. The most
9
+ expensive failure is two slices editing the same files from different bases, and that is invisible unless
10
+ somebody looks.
11
+
12
+ ```bash
13
+ # 1. What is already in flight? An open PR touching your files means coordinate, not proceed.
14
+ gh pr list --state open --json number,title,headRefName,updatedAt,files \
15
+ --jq '.[] | "#\(.number) \(.headRefName) — \(.title) (updated \(.updatedAt))"'
16
+
17
+ # 2. Is this branch behind its base? A slice built on a stale base merges dirty.
18
+ # The default branch here is `master`.
19
+ git fetch origin --quiet
20
+ git rev-list --count HEAD..origin/master # commits on the base we do not have
21
+
22
+ # 3. Which remote branches are merged and can be deleted?
23
+ git branch -r --merged origin/master \
24
+ | grep -v 'origin/master\|origin/HEAD' || echo "none"
25
+
26
+ # 4. Which remote branches have gone quiet? Anything older than ~2 weeks with no PR is a candidate.
27
+ git for-each-ref --sort=-committerdate refs/remotes/origin \
28
+ --format='%(refname:short) %(committerdate:relative)'
29
+ ```
30
+
31
+ ## How to act on it
32
+
33
+ | finding | action |
34
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
35
+ | An open PR touches files in your slice | **Stop and report it.** Do not edit those files in parallel; the supervisor decides whether to rebase, wait, or re-scope. |
36
+ | Your branch is behind its base | Rebase onto the base before starting. Starting from a stale base is how a clean review turns into a merge conflict nobody reads. |
37
+ | A merged branch still exists on the remote | Report it for deletion. Do not delete branches yourself — the supervisor owns the git history. |
38
+ | A branch is quiet, unmerged, and has no open PR | Report it. It is either abandoned work to drop or unfinished work to finish, and both need a human-level decision. |
@@ -155,3 +155,6 @@ dmypy.json
155
155
 
156
156
  # Cython debug symbols
157
157
  cython_debug/
158
+
159
+ # node tooling
160
+ node_modules/
@@ -0,0 +1,5 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "arrowParens": "avoid"
5
+ }