vycari-pepper 0.1.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.
- vycari_pepper-0.1.0/.claude/guidelines/coding.md +16 -0
- vycari_pepper-0.1.0/.claude/guidelines/invariants.md +13 -0
- vycari_pepper-0.1.0/.claude/guidelines/testing.md +11 -0
- vycari_pepper-0.1.0/.claude/maintainerd/coverage-adapt.sh +169 -0
- vycari_pepper-0.1.0/.claude/maintainerd/coverage-check.sh +119 -0
- vycari_pepper-0.1.0/.claude/maintainerd.json +86 -0
- vycari_pepper-0.1.0/.claude/settings.json +22 -0
- vycari_pepper-0.1.0/.github/CODEOWNERS +4 -0
- vycari_pepper-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +60 -0
- vycari_pepper-0.1.0/.github/dependabot.yml +35 -0
- vycari_pepper-0.1.0/.github/workflows/ci.yml +99 -0
- vycari_pepper-0.1.0/.github/workflows/contract-sync.yml +166 -0
- vycari_pepper-0.1.0/.github/workflows/pip-audit.yml +70 -0
- vycari_pepper-0.1.0/.github/workflows/release.yml +82 -0
- vycari_pepper-0.1.0/.github/workflows/zizmor.yml +61 -0
- vycari_pepper-0.1.0/.gitignore +10 -0
- vycari_pepper-0.1.0/.greptile/rules.md +10 -0
- vycari_pepper-0.1.0/.markdownlint-cli2.jsonc +32 -0
- vycari_pepper-0.1.0/CLAUDE.md +44 -0
- vycari_pepper-0.1.0/LICENSE +202 -0
- vycari_pepper-0.1.0/NOTICE +2 -0
- vycari_pepper-0.1.0/PKG-INFO +22 -0
- vycari_pepper-0.1.0/README.md +413 -0
- vycari_pepper-0.1.0/contracts/README.md +31 -0
- vycari_pepper-0.1.0/contracts/api-v1.openapi.json +19733 -0
- vycari_pepper-0.1.0/pyproject.toml +170 -0
- vycari_pepper-0.1.0/src/pepper_cli/__init__.py +11 -0
- vycari_pepper-0.1.0/src/pepper_cli/__main__.py +12 -0
- vycari_pepper-0.1.0/src/pepper_cli/_version.py +33 -0
- vycari_pepper-0.1.0/src/pepper_cli/agent_skill.py +48 -0
- vycari_pepper-0.1.0/src/pepper_cli/api_client.py +3002 -0
- vycari_pepper-0.1.0/src/pepper_cli/cli.py +5474 -0
- vycari_pepper-0.1.0/src/pepper_cli/credentials.py +268 -0
- vycari_pepper-0.1.0/src/pepper_cli/data/agent-skill/SKILL.md +338 -0
- vycari_pepper-0.1.0/src/pepper_cli/next_view.py +251 -0
- vycari_pepper-0.1.0/src/pepper_cli/proctitle.py +27 -0
- vycari_pepper-0.1.0/src/pepper_cli/provider/__init__.py +29 -0
- vycari_pepper-0.1.0/src/pepper_cli/provider/fs.py +510 -0
- vycari_pepper-0.1.0/src/pepper_cli/render.py +1120 -0
- vycari_pepper-0.1.0/src/pepper_cli/secret.py +71 -0
- vycari_pepper-0.1.0/src/pepper_cli/skill_doc.py +99 -0
- vycari_pepper-0.1.0/src/pepper_cli/terminal_text.py +99 -0
- vycari_pepper-0.1.0/src/pepper_cli/window_title.py +66 -0
- vycari_pepper-0.1.0/tests/__init__.py +0 -0
- vycari_pepper-0.1.0/tests/conftest.py +16 -0
- vycari_pepper-0.1.0/tests/test_agent_skill.py +581 -0
- vycari_pepper-0.1.0/tests/test_api_client.py +748 -0
- vycari_pepper-0.1.0/tests/test_api_client_groups.py +799 -0
- vycari_pepper-0.1.0/tests/test_api_client_notes.py +451 -0
- vycari_pepper-0.1.0/tests/test_api_client_room_skills.py +151 -0
- vycari_pepper-0.1.0/tests/test_api_client_scheduled_tasks.py +400 -0
- vycari_pepper-0.1.0/tests/test_api_client_stream.py +351 -0
- vycari_pepper-0.1.0/tests/test_api_client_takeout.py +246 -0
- vycari_pepper-0.1.0/tests/test_api_client_todos.py +174 -0
- vycari_pepper-0.1.0/tests/test_api_contract.py +440 -0
- vycari_pepper-0.1.0/tests/test_cli.py +1209 -0
- vycari_pepper-0.1.0/tests/test_cli_calendar.py +261 -0
- vycari_pepper-0.1.0/tests/test_cli_groups.py +1322 -0
- vycari_pepper-0.1.0/tests/test_cli_groups_watch.py +730 -0
- vycari_pepper-0.1.0/tests/test_cli_groups_watch_scoped.py +926 -0
- vycari_pepper-0.1.0/tests/test_cli_next.py +231 -0
- vycari_pepper-0.1.0/tests/test_cli_notes.py +1300 -0
- vycari_pepper-0.1.0/tests/test_cli_room_commands.py +214 -0
- vycari_pepper-0.1.0/tests/test_cli_room_skills.py +386 -0
- vycari_pepper-0.1.0/tests/test_cli_scheduled_tasks.py +906 -0
- vycari_pepper-0.1.0/tests/test_cli_skills.py +240 -0
- vycari_pepper-0.1.0/tests/test_cli_takeout.py +108 -0
- vycari_pepper-0.1.0/tests/test_cli_todos.py +358 -0
- vycari_pepper-0.1.0/tests/test_client_tools_roundtrip.py +308 -0
- vycari_pepper-0.1.0/tests/test_credentials.py +195 -0
- vycari_pepper-0.1.0/tests/test_proctitle.py +78 -0
- vycari_pepper-0.1.0/tests/test_provider_fs.py +334 -0
- vycari_pepper-0.1.0/tests/test_repl.py +762 -0
- vycari_pepper-0.1.0/tests/test_secret.py +64 -0
- vycari_pepper-0.1.0/tests/test_terminal_text.py +635 -0
- vycari_pepper-0.1.0/tests/test_user_agent_and_upgrade.py +290 -0
- vycari_pepper-0.1.0/tests/test_window_title.py +105 -0
- vycari_pepper-0.1.0/uv.lock +529 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Coding guidelines
|
|
2
|
+
|
|
3
|
+
- Python 3.13+. Format with `ruff format`, lint with `ruff check` (the rule set is `ALL` minus the
|
|
4
|
+
documented ignores in `pyproject.toml`), type-check with `ty check src/pepper_cli/`.
|
|
5
|
+
- Do not add new lint ignores to make a change pass. Fix the code, or justify a narrowly scoped
|
|
6
|
+
ignore in the PR description.
|
|
7
|
+
- Bearer tokens and PATs are wrapped in `SecretStr` (`secret.py`) at the boundary and are never
|
|
8
|
+
logged, printed, or put in an exception message.
|
|
9
|
+
- Wire models mirror the server's `/api/v1` response shapes. Each mirrored model carries a
|
|
10
|
+
`# MIRROR:` comment naming the server model it follows. Keep models tolerant: ignore unknown
|
|
11
|
+
fields and unknown stream frame types rather than failing.
|
|
12
|
+
- Formatted output goes through `render.py` and its rich consoles. Bare `print` is allowed in
|
|
13
|
+
`cli.py` only, for plain lines meant for pipes (a created id, `whoami` fields) and argv errors
|
|
14
|
+
on stderr; the `T201` ignore in `pyproject.toml` is scoped to that one file, so a `print`
|
|
15
|
+
anywhere else fails lint.
|
|
16
|
+
- Keep the CLI dependency-light. A new runtime dependency needs a reason in the PR.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Invariants
|
|
2
|
+
|
|
3
|
+
1. **Tokens stay secret.** A PAT or session token is never logged, echoed, or included in an error
|
|
4
|
+
message or in rendered output. The credentials file is written with owner-only permissions.
|
|
5
|
+
2. **The CLI follows the server contract, not the reverse.** The server's `/api/v1` is additive-only
|
|
6
|
+
for supported clients. The CLI must keep working against a server that adds fields, endpoints, or
|
|
7
|
+
stream frame types it does not know about.
|
|
8
|
+
3. **`# MIRROR:` comments stay accurate.** When a mirrored model changes, update the comment and the
|
|
9
|
+
model together.
|
|
10
|
+
4. **Exit codes and stdout are an interface.** Scripts and agents parse the output; changing the
|
|
11
|
+
shape of `--json` output or an exit code is a breaking change and needs a deliberate PR.
|
|
12
|
+
5. **The bundled agent skill matches the CLI.** `src/pepper_cli/data/agent-skill/SKILL.md`
|
|
13
|
+
documents the commands; change both together.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Testing guidelines
|
|
2
|
+
|
|
3
|
+
- Run `uv run pytest`. The suite must pass with no network: HTTP is mocked with `pytest-httpx`.
|
|
4
|
+
- Coverage is a ratchet. `coverage.floor` in `.claude/maintainerd.json` may be raised, never
|
|
5
|
+
lowered to make a change pass. CI fails when measured line coverage is below it.
|
|
6
|
+
- A behaviour change comes with a test that fails without it. Prefer asserting on rendered output
|
|
7
|
+
and request shape over asserting on internals.
|
|
8
|
+
- Tests use obviously fake credentials and identities (for example `pat_supersecret`,
|
|
9
|
+
`alice@example.com`). Never put a real token, address, or hostname in a fixture.
|
|
10
|
+
- Tests never read or write the real credentials file; use the temporary-directory fixtures in
|
|
11
|
+
`tests/conftest.py`.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# coverage-adapt.sh — normalize a coverage tool's native JSON into the maintainerd
|
|
4
|
+
# coverage summary contract:
|
|
5
|
+
#
|
|
6
|
+
# {"metric": "lines", "percent": <float>}
|
|
7
|
+
#
|
|
8
|
+
# Canonical source: maintainerd, plugins/core/scripts/coverage-adapt.sh
|
|
9
|
+
# A consuming repo carries a copy at .claude/maintainerd/coverage-adapt.sh so CI can run
|
|
10
|
+
# it without the plugin installed. Re-copy it when maintainerd-core updates.
|
|
11
|
+
#
|
|
12
|
+
# coverage-adapt.sh # auto-detect the one input and its format
|
|
13
|
+
# coverage-adapt.sh --tool pytest-cov # force the pytest-cov (coverage.py) shape
|
|
14
|
+
# coverage-adapt.sh --tool istanbul --input coverage/coverage-summary.json
|
|
15
|
+
# coverage-adapt.sh --output build/coverage-summary.json
|
|
16
|
+
#
|
|
17
|
+
# Line coverage only. Branch coverage is deliberately not gated: it is noisier, moves for
|
|
18
|
+
# reasons unrelated to test quality, and a ratchet on it ratchets on noise.
|
|
19
|
+
#
|
|
20
|
+
# Requires: bash 3.2+, jq.
|
|
21
|
+
#
|
|
22
|
+
# Exits non-zero — and writes nothing — on anything it cannot read with certainty. A
|
|
23
|
+
# coverage gate that guesses a number is worse than one that stops.
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
# Float parsing and printing must not follow the runner's locale: a comma decimal
|
|
27
|
+
# separator turns 87.4 into 87 in awk, and the gate silently moves.
|
|
28
|
+
export LC_ALL=C
|
|
29
|
+
|
|
30
|
+
usage() {
|
|
31
|
+
cat >&2 <<'USAGE'
|
|
32
|
+
usage: coverage-adapt.sh [--tool auto|pytest-cov|istanbul] [--input FILE] [--output FILE]
|
|
33
|
+
|
|
34
|
+
--tool input format. Default "auto": detect from the JSON's shape.
|
|
35
|
+
pytest-cov coverage.py / pytest-cov `--cov-report=json` (.totals.percent_covered)
|
|
36
|
+
istanbul vitest / jest / nyc `json-summary` (.total.lines.pct)
|
|
37
|
+
--input the tool's native JSON. Default: whichever ONE of coverage-summary.json,
|
|
38
|
+
coverage.json, coverage/coverage-summary.json holds a measurement. Two or more
|
|
39
|
+
is an error, not a ranking: one of them is a previous run's number. This
|
|
40
|
+
script's own normalized output does not count as a rival measurement.
|
|
41
|
+
--output where to write the normalized summary. Default: coverage-summary.json
|
|
42
|
+
USAGE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tool=auto
|
|
46
|
+
input=""
|
|
47
|
+
output="coverage-summary.json"
|
|
48
|
+
|
|
49
|
+
while [ $# -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--tool) [ $# -ge 2 ] || { echo "coverage-adapt: --tool needs a value" >&2; exit 2; }; tool="$2"; shift 2 ;;
|
|
52
|
+
--input) [ $# -ge 2 ] || { echo "coverage-adapt: --input needs a value" >&2; exit 2; }; input="$2"; shift 2 ;;
|
|
53
|
+
--output) [ $# -ge 2 ] || { echo "coverage-adapt: --output needs a value" >&2; exit 2; }; output="$2"; shift 2 ;;
|
|
54
|
+
-h|--help) usage; exit 0 ;;
|
|
55
|
+
*) echo "coverage-adapt: unknown argument: $1" >&2; usage; exit 2 ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
case "$tool" in
|
|
60
|
+
auto|pytest-cov|istanbul) ;;
|
|
61
|
+
*) echo "coverage-adapt: unknown --tool '$tool' (auto|pytest-cov|istanbul)" >&2; exit 2 ;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
command -v jq >/dev/null 2>&1 || {
|
|
65
|
+
echo "coverage-adapt: jq is not installed — the summary cannot be read" >&2; exit 2; }
|
|
66
|
+
|
|
67
|
+
# Discovery refuses to choose between two *measurements* rather than ranking them: a repo
|
|
68
|
+
# whose tool writes coverage.json while an unrelated coverage-summary.json is still lying
|
|
69
|
+
# around has two plausible inputs, and either ranking silently gates the current commit on
|
|
70
|
+
# a previous run's number — the one failure this whole gate exists to prevent.
|
|
71
|
+
#
|
|
72
|
+
# The adapter's own earlier output is the exception, and not a special case so much as the
|
|
73
|
+
# absence of one: an already-normalized file sitting at the output path is this script's
|
|
74
|
+
# work, not a rival measurement, so a native input beside it simply wins. That keeps the
|
|
75
|
+
# step re-runnable, which matters because CI steps get retried.
|
|
76
|
+
if [ -z "$input" ]; then
|
|
77
|
+
found=""
|
|
78
|
+
own_output=""
|
|
79
|
+
count=0
|
|
80
|
+
for candidate in coverage-summary.json coverage.json coverage/coverage-summary.json; do
|
|
81
|
+
[ -f "$candidate" ] || continue
|
|
82
|
+
if [ "$candidate" = "$output" ] && jq -e \
|
|
83
|
+
'type == "object" and .metric == "lines" and (.percent | type == "number")' \
|
|
84
|
+
"$candidate" >/dev/null 2>&1; then
|
|
85
|
+
own_output="$candidate"
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
found="${found:+$found, }$candidate"
|
|
89
|
+
input="$candidate"
|
|
90
|
+
count=$((count + 1))
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
if [ "$count" -gt 1 ]; then
|
|
94
|
+
echo "coverage-adapt: more than one coverage JSON is present ($found)." >&2
|
|
95
|
+
echo " One of them is probably left over from an earlier run, and gating on the wrong" >&2
|
|
96
|
+
echo " one would check this commit against a previous commit's number. Pass --input to" >&2
|
|
97
|
+
echo " name the file this run wrote, or delete the stale one before the coverage step." >&2
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
# Nothing but our own output: the re-run no-op.
|
|
101
|
+
if [ "$count" -eq 0 ] && [ -n "$own_output" ]; then input="$own_output"; fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ -z "$input" ] || [ ! -f "$input" ]; then
|
|
105
|
+
echo "coverage-adapt: no coverage JSON found${input:+ at $input}." >&2
|
|
106
|
+
echo " Run the repo's commands.coverage first, or pass --input." >&2
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# `type == "object"` rather than a bare parse check: every shape below indexes the top
|
|
111
|
+
# level, and indexing an array or a string is a jq *error*, which would surface as a raw
|
|
112
|
+
# jq message rather than as this one.
|
|
113
|
+
jq -e 'type == "object"' "$input" >/dev/null 2>&1 || {
|
|
114
|
+
echo "coverage-adapt: $input is not a valid JSON object — refusing to guess a percentage." >&2
|
|
115
|
+
exit 1; }
|
|
116
|
+
|
|
117
|
+
# Each supported shape, as the jq path to its line-coverage percentage. The normalized
|
|
118
|
+
# shape is included so re-running the adapter on its own output is a no-op rather than an
|
|
119
|
+
# error: CI steps get re-run, and a step that only works once is a step that breaks.
|
|
120
|
+
percent=""
|
|
121
|
+
detected=""
|
|
122
|
+
try_shape() {
|
|
123
|
+
local name="$1" filter="$2" value
|
|
124
|
+
value="$(jq -r "$filter // empty" "$input" 2>/dev/null || true)"
|
|
125
|
+
if [ -n "$value" ]; then detected="$name"; percent="$value"; return 0; fi
|
|
126
|
+
return 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "$tool" in
|
|
130
|
+
pytest-cov) try_shape pytest-cov 'select(.totals.percent_covered | type == "number") | .totals.percent_covered' || true ;;
|
|
131
|
+
istanbul) try_shape istanbul 'select(.total.lines.pct | type == "number") | .total.lines.pct' || true ;;
|
|
132
|
+
auto)
|
|
133
|
+
try_shape normalized 'select(.metric == "lines" and (.percent | type == "number")) | .percent' \
|
|
134
|
+
|| try_shape pytest-cov 'select(.totals.percent_covered | type == "number") | .totals.percent_covered' \
|
|
135
|
+
|| try_shape istanbul 'select(.total.lines.pct | type == "number") | .total.lines.pct' \
|
|
136
|
+
|| true
|
|
137
|
+
;;
|
|
138
|
+
esac
|
|
139
|
+
|
|
140
|
+
if [ -z "$percent" ]; then
|
|
141
|
+
echo "coverage-adapt: $input has no line-coverage percentage in the ${tool} shape." >&2
|
|
142
|
+
echo " Expected .totals.percent_covered (pytest-cov) or .total.lines.pct (istanbul)," >&2
|
|
143
|
+
echo " as a number. istanbul writes \"Unknown\" when it measured no lines at all." >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# 0-100 is the only range a percentage can be in. Anything else means the shape matched
|
|
148
|
+
# something that is not a percentage (a count, a ratio) and the gate would be nonsense.
|
|
149
|
+
awk -v p="$percent" 'BEGIN { exit !(p + 0 >= 0 && p + 0 <= 100) }' || {
|
|
150
|
+
echo "coverage-adapt: $input yielded percent=$percent, which is not in 0-100." >&2; exit 1; }
|
|
151
|
+
|
|
152
|
+
outdir="$(dirname "$output")"
|
|
153
|
+
[ -d "$outdir" ] || mkdir -p "$outdir"
|
|
154
|
+
|
|
155
|
+
# The temp file is created beside the output, not in TMPDIR: mv is only atomic within one
|
|
156
|
+
# filesystem, and on CI runners TMPDIR is regularly on another. A reader — the gate, the
|
|
157
|
+
# artifact upload — must never see a half-written summary.
|
|
158
|
+
tmp="$(mktemp "$outdir/.coverage-summary.XXXXXX")"
|
|
159
|
+
trap 'rm -f "$tmp"' EXIT
|
|
160
|
+
# Written through jq so the output is valid JSON with a real number, whatever the input's
|
|
161
|
+
# formatting. `-n` plus --argjson keeps the value numeric rather than a quoted string.
|
|
162
|
+
jq -n --argjson percent "$percent" '{metric: "lines", percent: $percent}' > "$tmp"
|
|
163
|
+
# mktemp creates the file 0600; the summary is uploaded as an artifact and read by the
|
|
164
|
+
# gate, so give it the ordinary readable mode a generated file would have.
|
|
165
|
+
chmod 644 "$tmp"
|
|
166
|
+
mv "$tmp" "$output"
|
|
167
|
+
trap - EXIT
|
|
168
|
+
|
|
169
|
+
echo "coverage-adapt: $input ($detected) -> $output lines ${percent}%"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# coverage-check.sh — the coverage ratchet gate.
|
|
4
|
+
#
|
|
5
|
+
# Reads the normalized summary (`{"metric": "lines", "percent": <float>}`, produced by
|
|
6
|
+
# coverage-adapt.sh) and the repo's `coverage.floor` from .claude/maintainerd.json, and
|
|
7
|
+
# fails when the measured percentage is below the floor. Equal passes: the floor is the
|
|
8
|
+
# lowest acceptable value, not a value to beat.
|
|
9
|
+
#
|
|
10
|
+
# Canonical source: maintainerd, plugins/core/scripts/coverage-check.sh
|
|
11
|
+
# A consuming repo carries a copy at .claude/maintainerd/coverage-check.sh so CI can run
|
|
12
|
+
# it without the plugin installed. Re-copy it when maintainerd-core updates.
|
|
13
|
+
#
|
|
14
|
+
# coverage-check.sh
|
|
15
|
+
# coverage-check.sh --summary build/coverage-summary.json
|
|
16
|
+
# coverage-check.sh --floor 80 # override the config, for a local what-if
|
|
17
|
+
#
|
|
18
|
+
# There are no tool-specific threshold flags anywhere in the pipeline: the gate is this
|
|
19
|
+
# one shell step in every repo, and the number it reads lives in one tracked file, so
|
|
20
|
+
# lowering it is a reviewable diff rather than an invisible edit to a workflow.
|
|
21
|
+
#
|
|
22
|
+
# The workflow uploads the summary as an artifact named `coverage`; `doctor` reads that
|
|
23
|
+
# artifact from the latest default-branch run. See the workflow snippet in the plugin's
|
|
24
|
+
# references/config-schema.md.
|
|
25
|
+
#
|
|
26
|
+
# Requires: bash 3.2+, jq.
|
|
27
|
+
#
|
|
28
|
+
# Fails closed. A missing summary, malformed JSON, or an absent floor is an error, never
|
|
29
|
+
# a pass — every one of those states is indistinguishable from "the tests did not run",
|
|
30
|
+
# and a gate that waves those through is a gate that has never once fired.
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
export LC_ALL=C
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
cat >&2 <<'USAGE'
|
|
37
|
+
usage: coverage-check.sh [--summary FILE] [--config FILE] [--floor N]
|
|
38
|
+
|
|
39
|
+
--summary normalized coverage summary. Default: coverage-summary.json
|
|
40
|
+
--config repo config holding coverage.floor. Default: .claude/maintainerd.json
|
|
41
|
+
--floor use this floor instead of the one in the config (an integer, 0-100)
|
|
42
|
+
USAGE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
summary="coverage-summary.json"
|
|
46
|
+
config=".claude/maintainerd.json"
|
|
47
|
+
floor=""
|
|
48
|
+
|
|
49
|
+
while [ $# -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--summary) [ $# -ge 2 ] || { echo "coverage-check: --summary needs a value" >&2; exit 2; }; summary="$2"; shift 2 ;;
|
|
52
|
+
--config) [ $# -ge 2 ] || { echo "coverage-check: --config needs a value" >&2; exit 2; }; config="$2"; shift 2 ;;
|
|
53
|
+
--floor) [ $# -ge 2 ] || { echo "coverage-check: --floor needs a value" >&2; exit 2; }; floor="$2"; shift 2 ;;
|
|
54
|
+
-h|--help) usage; exit 0 ;;
|
|
55
|
+
*) echo "coverage-check: unknown argument: $1" >&2; usage; exit 2 ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
command -v jq >/dev/null 2>&1 || {
|
|
60
|
+
echo "coverage-check: jq is not installed — the summary cannot be read" >&2; exit 2; }
|
|
61
|
+
|
|
62
|
+
# GitHub Actions renders `::error::` as an annotation on the job. Elsewhere it is just a
|
|
63
|
+
# line of text, so this costs nothing off CI.
|
|
64
|
+
fail() {
|
|
65
|
+
if [ -n "${GITHUB_ACTIONS:-}" ]; then printf '::error::%s\n' "$1" >&2; else printf '%s\n' "$1" >&2; fi
|
|
66
|
+
shift
|
|
67
|
+
for line in "$@"; do printf ' %s\n' "$line" >&2; done
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ── The floor ────────────────────────────────────────────────────────────────
|
|
72
|
+
if [ -z "$floor" ]; then
|
|
73
|
+
[ -f "$config" ] || fail "coverage-check: no $config — the floor lives there." \
|
|
74
|
+
"Run /bootstrap in this repo, then /bootstrap --adopt to measure and record the floor."
|
|
75
|
+
jq -e 'type == "object"' "$config" >/dev/null 2>&1 \
|
|
76
|
+
|| fail "coverage-check: $config is not a valid JSON object."
|
|
77
|
+
floor="$(jq -r 'if (.coverage | type) == "object" then (.coverage.floor // empty) else empty end' "$config")"
|
|
78
|
+
[ -n "$floor" ] || fail "coverage-check: $config has no coverage.floor — this repo has not adopted the ratchet." \
|
|
79
|
+
"Run /bootstrap --adopt to measure the default branch and record coverage.floor," \
|
|
80
|
+
"or drop this step from the workflow if the repo is exempt from coverage."
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
case "$floor" in
|
|
84
|
+
''|*[!0-9]*) fail "coverage-check: coverage.floor is '$floor'; it must be a whole number of percent." ;;
|
|
85
|
+
esac
|
|
86
|
+
[ "$floor" -le 100 ] || fail "coverage-check: coverage.floor is $floor, which is above 100%."
|
|
87
|
+
|
|
88
|
+
# ── The measurement ──────────────────────────────────────────────────────────
|
|
89
|
+
[ -f "$summary" ] || fail "coverage-check: $summary does not exist." \
|
|
90
|
+
"The repo's commands.coverage must leave a summary there, normalized by coverage-adapt.sh." \
|
|
91
|
+
"Failing closed: a missing summary is indistinguishable from a suite that never ran."
|
|
92
|
+
|
|
93
|
+
# `type == "object"`, not a bare parse check: indexing an array or a bare string with
|
|
94
|
+
# .metric is a jq error, and it would surface as a raw jq message instead of a finding.
|
|
95
|
+
jq -e 'type == "object"' "$summary" >/dev/null 2>&1 \
|
|
96
|
+
|| fail "coverage-check: $summary is not a JSON object — it may be truncated or the wrong file." \
|
|
97
|
+
"Failing closed rather than reading a percentage out of it."
|
|
98
|
+
|
|
99
|
+
metric="$(jq -r '.metric // empty' "$summary")"
|
|
100
|
+
[ "$metric" = "lines" ] || fail "coverage-check: $summary reports metric '${metric:-<absent>}', expected 'lines'." \
|
|
101
|
+
"Run coverage-adapt.sh over the tool's native output first; the gate reads only the normalized shape."
|
|
102
|
+
|
|
103
|
+
percent="$(jq -r 'select(.percent | type == "number") | .percent // empty' "$summary")"
|
|
104
|
+
[ -n "$percent" ] || fail "coverage-check: $summary has no numeric .percent." \
|
|
105
|
+
"Run coverage-adapt.sh over the tool's native output first."
|
|
106
|
+
|
|
107
|
+
awk -v p="$percent" 'BEGIN { exit !(p + 0 >= 0 && p + 0 <= 100) }' \
|
|
108
|
+
|| fail "coverage-check: $summary reports percent=$percent, which is not in 0-100."
|
|
109
|
+
|
|
110
|
+
# ── The gate ─────────────────────────────────────────────────────────────────
|
|
111
|
+
# awk, not bash arithmetic: the percentage is a float and bash 3.2 has integers only.
|
|
112
|
+
# Truncating it here would let a real regression from 85.0 to 84.9 read as no change.
|
|
113
|
+
if awk -v p="$percent" -v f="$floor" 'BEGIN { exit !(p + 0 < f + 0) }'; then
|
|
114
|
+
fail "coverage-check: line coverage ${percent}% is below the floor of ${floor}%." \
|
|
115
|
+
"The floor is a ratchet: it may rise, never fall. Add tests for what this change" \
|
|
116
|
+
"left uncovered rather than lowering coverage.floor in $config."
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
printf 'coverage-check: line coverage %s%% (floor %s%%) — OK\n' "$percent" "$floor"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "Vycari/pepper-cli",
|
|
3
|
+
"defaultBranch": "main",
|
|
4
|
+
"language": "python",
|
|
5
|
+
"commands": {
|
|
6
|
+
"format": "uv run ruff format --check",
|
|
7
|
+
"lint": "uv run ruff check",
|
|
8
|
+
"build": null,
|
|
9
|
+
"test": "uv run pytest",
|
|
10
|
+
"coverage": "uv run pytest --cov --cov-report=json:coverage-summary.json",
|
|
11
|
+
"typecheck": "uv run ty check src/pepper_cli/"
|
|
12
|
+
},
|
|
13
|
+
"coverage": {
|
|
14
|
+
"floor": 88
|
|
15
|
+
},
|
|
16
|
+
"paths": {
|
|
17
|
+
"source": "src/pepper_cli/",
|
|
18
|
+
"tests": "tests/",
|
|
19
|
+
"designDocs": [],
|
|
20
|
+
"productDocs": [
|
|
21
|
+
"README.md",
|
|
22
|
+
"CLAUDE.md"
|
|
23
|
+
],
|
|
24
|
+
"changelogDir": null,
|
|
25
|
+
"researchRadarDir": null,
|
|
26
|
+
"prTemplate": ".github/PULL_REQUEST_TEMPLATE.md",
|
|
27
|
+
"skillsDir": ".claude/skills/"
|
|
28
|
+
},
|
|
29
|
+
"guidelines": {
|
|
30
|
+
"coding": ".claude/guidelines/coding.md",
|
|
31
|
+
"testing": ".claude/guidelines/testing.md",
|
|
32
|
+
"invariants": ".claude/guidelines/invariants.md"
|
|
33
|
+
},
|
|
34
|
+
"labels": {
|
|
35
|
+
"architecture": "architecture",
|
|
36
|
+
"testQuality": "test-quality",
|
|
37
|
+
"security": "security",
|
|
38
|
+
"dependencies": "dependencies",
|
|
39
|
+
"automated": "automated"
|
|
40
|
+
},
|
|
41
|
+
"dailyUpdate": {
|
|
42
|
+
"subSkills": [
|
|
43
|
+
"audit-product-docs"
|
|
44
|
+
],
|
|
45
|
+
"branchPrefix": "daily-update-",
|
|
46
|
+
"commitSubject": "chore(daily): {date} daily update"
|
|
47
|
+
},
|
|
48
|
+
"autoDev": {
|
|
49
|
+
"enabled": false,
|
|
50
|
+
"branchPrefix": "auto/issue-",
|
|
51
|
+
"marker": "<!-- auto-dev -->",
|
|
52
|
+
"stateLabels": {
|
|
53
|
+
"needsInfo": "auto:needs-info",
|
|
54
|
+
"planned": "auto:planned",
|
|
55
|
+
"ready": "auto:ready",
|
|
56
|
+
"inProgress": "auto:in-progress",
|
|
57
|
+
"parked": "auto:parked",
|
|
58
|
+
"skip": "auto:skip"
|
|
59
|
+
},
|
|
60
|
+
"excludedLabels": [
|
|
61
|
+
"epic",
|
|
62
|
+
"question",
|
|
63
|
+
"wontfix",
|
|
64
|
+
"duplicate",
|
|
65
|
+
"invalid"
|
|
66
|
+
],
|
|
67
|
+
"openPrsAsDraft": false,
|
|
68
|
+
"prLabel": "auto:pr",
|
|
69
|
+
"fallbackReviewMinutes": 60,
|
|
70
|
+
"maxPrsInFlight": 1,
|
|
71
|
+
"orphanReclaimMinutes": 90
|
|
72
|
+
},
|
|
73
|
+
"createPr": {
|
|
74
|
+
"requireIssueForDeferredWork": true
|
|
75
|
+
},
|
|
76
|
+
"review": {
|
|
77
|
+
"bots": [
|
|
78
|
+
"greptile-apps[bot]"
|
|
79
|
+
],
|
|
80
|
+
"approvalThreshold": "5/5",
|
|
81
|
+
"responderTier": "capable",
|
|
82
|
+
"impasseRounds": 2,
|
|
83
|
+
"sameFileRoundCap": 3
|
|
84
|
+
},
|
|
85
|
+
"release": null
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extraKnownMarketplaces": {
|
|
3
|
+
"maintainerd": {
|
|
4
|
+
"source": {
|
|
5
|
+
"source": "github",
|
|
6
|
+
"repo": "Vycari/maintainerd"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"vycari": {
|
|
10
|
+
"source": {
|
|
11
|
+
"source": "github",
|
|
12
|
+
"repo": "Vycari/vycari"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"enabledPlugins": {
|
|
17
|
+
"maintainerd-core@maintainerd": true,
|
|
18
|
+
"repo-ops@maintainerd": true,
|
|
19
|
+
"audits@maintainerd": true,
|
|
20
|
+
"vycari-ops@vycari": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Two audiences, one template, and this file is the canonical copy of both: a repo's
|
|
3
|
+
`.github/PULL_REQUEST_TEMPLATE.md` is this file with its checklist block filled in. Change the
|
|
4
|
+
shape here, in `plugins/vycari-ops/references/pr-template.md`, not in a repo.
|
|
5
|
+
|
|
6
|
+
Everything above `## AI reviewer` is for a human who is busy and reading across many workstreams.
|
|
7
|
+
Everything below it is for the Greptile reviewer and for future Claude sessions reading this PR as
|
|
8
|
+
history. Write each for its own reader: don't pad the human half with detail, and don't thin the AI
|
|
9
|
+
half because a human might skim it. State gaps and trade-offs plainly — a description that admits
|
|
10
|
+
its own limitations is the one a reviewer trusts. No marketing language, no emoji.
|
|
11
|
+
|
|
12
|
+
Closing keywords are a live footgun. GitHub matches `close`/`fix`/`resolve` next to an issue number
|
|
13
|
+
ANYWHERE in the body and ignores negation — the sentence "does not close #123" closes #123 on merge
|
|
14
|
+
(that has wrongly closed issues before). For a partial fix write `Refs #123`, and keep the
|
|
15
|
+
keyword away from the number: "this does not finish #123".
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
## Human overview
|
|
19
|
+
|
|
20
|
+
<!-- 2–4 sentences, glanceable: what changed and why, for a reviewer working across many streams.
|
|
21
|
+
Link the issue (`Fixes #N` / `Refs #N`). No implementation detail — that is the AI section's job. -->
|
|
22
|
+
|
|
23
|
+
### Human required (optional)
|
|
24
|
+
|
|
25
|
+
<!-- Only when a person must do or decide something: a merge-order constraint, a settings / secrets
|
|
26
|
+
/ prod / branch-protection step, a product decision. One line each, imperative. Drop the whole
|
|
27
|
+
section when it is empty. -->
|
|
28
|
+
|
|
29
|
+
- [ ]
|
|
30
|
+
|
|
31
|
+
## AI reviewer
|
|
32
|
+
|
|
33
|
+
<!-- For Greptile and for future Claude sessions; assume no human reads this. As dense as is useful.
|
|
34
|
+
Cover what applies and drop what doesn't:
|
|
35
|
+
|
|
36
|
+
- what the diff does, by subsystem
|
|
37
|
+
- invariants touched, and why they still hold
|
|
38
|
+
- alternatives rejected, and why
|
|
39
|
+
- failure posture: what breaks if this is wrong, and what it takes with it
|
|
40
|
+
- migration notes: ordering, backfill, rollback
|
|
41
|
+
- where to look hardest — the subtle bug this nearly shipped, the "simplification" a later PR would
|
|
42
|
+
apply without understanding
|
|
43
|
+
- verification: exact commands and their actual results, not what should pass; say what was skipped
|
|
44
|
+
and why
|
|
45
|
+
- deferred work, each with an issue link (a follow-up that lives only in a PR dies with the PR)
|
|
46
|
+
-->
|
|
47
|
+
|
|
48
|
+
### Checklist (repo-specific)
|
|
49
|
+
|
|
50
|
+
<!-- REPO-SPECIFIC SLOT — the only part of this template a repo owns. Fill it with that repo's own
|
|
51
|
+
gates (its format / lint / typecheck / test commands, migration and coverage rules, doc surfaces
|
|
52
|
+
that must move with the code). Tick only what actually ran; mark an inapplicable box `[ ]` with a
|
|
53
|
+
one-line reason. An N/A with a reason is fine, a silently ticked box is not. -->
|
|
54
|
+
|
|
55
|
+
- [ ] `uv run ruff format --check` and `uv run ruff check` clean (no new lint ignores)
|
|
56
|
+
- [ ] `uv run ty check src/pepper_cli/` clean
|
|
57
|
+
- [ ] `uv run pytest` passes; coverage floor in `.claude/maintainerd.json` not lowered
|
|
58
|
+
- [ ] Server-contract changes: `/api/v1` usage stays additive-only, `# MIRROR:` comments updated, unknown fields and frames still tolerated
|
|
59
|
+
- [ ] No token or credential is logged, printed, or put in a fixture
|
|
60
|
+
- [ ] README and the bundled `SKILL.md` updated if commands or output changed
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Ecosystems come from the repo profile (Vycari/vycari,
|
|
4
|
+
# plugins/vycari-ops/references/repo-profile.json -> languages.python-service.dependabot),
|
|
5
|
+
# minus `docker`: this repo has no Dockerfile.
|
|
6
|
+
#
|
|
7
|
+
# `uv`, not `pip`: this repo's dependency truth is uv.lock, and the pip ecosystem does not
|
|
8
|
+
# regenerate it.
|
|
9
|
+
updates:
|
|
10
|
+
# Python dependencies (pyproject.toml + uv.lock).
|
|
11
|
+
#
|
|
12
|
+
# Patch and minor are batched into one PR each; majors are deliberately ungrouped so each
|
|
13
|
+
# arrives as its own revertable PR. Batching matters because every bump regenerates uv.lock,
|
|
14
|
+
# and separate lock regenerations conflict with each other in the merge queue.
|
|
15
|
+
- package-ecosystem: "uv"
|
|
16
|
+
directory: "/"
|
|
17
|
+
schedule:
|
|
18
|
+
interval: "weekly"
|
|
19
|
+
open-pull-requests-limit: 5
|
|
20
|
+
groups:
|
|
21
|
+
python-patch:
|
|
22
|
+
update-types: ["patch"]
|
|
23
|
+
python-minor:
|
|
24
|
+
update-types: ["minor"]
|
|
25
|
+
|
|
26
|
+
# GitHub Actions used in .github/workflows. Grouped across update types: an Actions bump that
|
|
27
|
+
# breaks fails loudly in CI rather than subtly at runtime, so the batch is cheap to bisect.
|
|
28
|
+
- package-ecosystem: "github-actions"
|
|
29
|
+
directory: "/"
|
|
30
|
+
schedule:
|
|
31
|
+
interval: "weekly"
|
|
32
|
+
open-pull-requests-limit: 5
|
|
33
|
+
groups:
|
|
34
|
+
github-actions:
|
|
35
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Format, lint, type-check and test the CLI; lint the markdown. Everything here needs no
|
|
4
|
+
# network access to a Pepper server: the tests mock the HTTP layer.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
# Required by the merge queue, and load-bearing: the queue holds a PR until its required
|
|
11
|
+
# checks report against the *speculative merge* it builds, so a required workflow that never
|
|
12
|
+
# runs on `merge_group` leaves every entry pending forever. There is deliberately no `paths:`
|
|
13
|
+
# filter anywhere in this file — `merge_group` does not support one, and a required workflow
|
|
14
|
+
# that path-filters itself out of a queue run has the same effect.
|
|
15
|
+
merge_group:
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
# `github.ref` is the queue's own ref for a merge_group run
|
|
19
|
+
# (`refs/heads/gh-readonly-queue/main/pr-N-<sha>`), unique per entry, so cancel-in-progress
|
|
20
|
+
# cannot cancel a sibling entry's checks out from under it — a cancelled check is an eviction
|
|
21
|
+
# from the queue.
|
|
22
|
+
group: ci-${{ github.ref }}
|
|
23
|
+
cancel-in-progress: true
|
|
24
|
+
|
|
25
|
+
# Least-privilege default: the jobs here only read the checkout.
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
# The repo profile's `python-service` required checks are `ci` and `docs`. Required checks are
|
|
31
|
+
# named per JOB, not per workflow, so these job keys are exactly the check-run names branch
|
|
32
|
+
# protection must list.
|
|
33
|
+
ci:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
timeout-minutes: 15
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
38
|
+
with:
|
|
39
|
+
persist-credentials: false
|
|
40
|
+
|
|
41
|
+
- name: Install uv
|
|
42
|
+
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
43
|
+
with:
|
|
44
|
+
enable-cache: true
|
|
45
|
+
python-version: "3.13"
|
|
46
|
+
|
|
47
|
+
# --frozen: fail rather than silently resolving a different dependency set than uv.lock
|
|
48
|
+
# records. --extra dev pulls pytest, pytest-httpx, pytest-cov, ruff and ty.
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: uv sync --frozen --extra dev
|
|
51
|
+
|
|
52
|
+
- name: Format check (ruff format)
|
|
53
|
+
run: uv run --no-sync ruff format --check
|
|
54
|
+
|
|
55
|
+
- name: Lint (ruff check)
|
|
56
|
+
run: uv run --no-sync ruff check
|
|
57
|
+
|
|
58
|
+
- name: Type check (ty)
|
|
59
|
+
run: uv run --no-sync ty check src/pepper_cli/
|
|
60
|
+
|
|
61
|
+
# `commands.coverage` from .claude/maintainerd.json. Running the suite under coverage is
|
|
62
|
+
# also how the suite runs at all — there is no separate test step, so a test failure fails
|
|
63
|
+
# here rather than being masked by the gate below.
|
|
64
|
+
- name: Test and measure coverage
|
|
65
|
+
run: uv run --no-sync pytest --cov --cov-report=json:coverage-summary.json
|
|
66
|
+
|
|
67
|
+
# coverage.py's JSON is not the summary contract; the adapter rewrites it to
|
|
68
|
+
# {"metric": "lines", "percent": N}. Vendored under .claude/maintainerd/ so CI runs it
|
|
69
|
+
# without the plugin installed.
|
|
70
|
+
- name: Normalize the coverage summary
|
|
71
|
+
run: .claude/maintainerd/coverage-adapt.sh --tool pytest-cov
|
|
72
|
+
|
|
73
|
+
# Before the gate, so a regression still publishes the number that proves it. The artifact
|
|
74
|
+
# name is load-bearing: the maintainerd doctor looks for exactly this.
|
|
75
|
+
- name: Upload the coverage summary
|
|
76
|
+
if: always()
|
|
77
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
78
|
+
with:
|
|
79
|
+
name: coverage
|
|
80
|
+
path: coverage-summary.json
|
|
81
|
+
if-no-files-found: error
|
|
82
|
+
retention-days: 30
|
|
83
|
+
|
|
84
|
+
# The ratchet. Reads coverage.floor from .claude/maintainerd.json — one number, in a
|
|
85
|
+
# tracked file, so lowering it is a reviewable diff.
|
|
86
|
+
- name: Coverage ratchet
|
|
87
|
+
run: .claude/maintainerd/coverage-check.sh
|
|
88
|
+
|
|
89
|
+
# Markdown lint over the whole tree. Config, and the reason each rule is off, are in
|
|
90
|
+
# .markdownlint-cli2.jsonc.
|
|
91
|
+
docs:
|
|
92
|
+
runs-on: ubuntu-latest
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
95
|
+
with:
|
|
96
|
+
persist-credentials: false
|
|
97
|
+
|
|
98
|
+
- name: Lint markdown
|
|
99
|
+
run: npx --yes markdownlint-cli2@0.18.1
|