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.
Files changed (78) hide show
  1. vycari_pepper-0.1.0/.claude/guidelines/coding.md +16 -0
  2. vycari_pepper-0.1.0/.claude/guidelines/invariants.md +13 -0
  3. vycari_pepper-0.1.0/.claude/guidelines/testing.md +11 -0
  4. vycari_pepper-0.1.0/.claude/maintainerd/coverage-adapt.sh +169 -0
  5. vycari_pepper-0.1.0/.claude/maintainerd/coverage-check.sh +119 -0
  6. vycari_pepper-0.1.0/.claude/maintainerd.json +86 -0
  7. vycari_pepper-0.1.0/.claude/settings.json +22 -0
  8. vycari_pepper-0.1.0/.github/CODEOWNERS +4 -0
  9. vycari_pepper-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +60 -0
  10. vycari_pepper-0.1.0/.github/dependabot.yml +35 -0
  11. vycari_pepper-0.1.0/.github/workflows/ci.yml +99 -0
  12. vycari_pepper-0.1.0/.github/workflows/contract-sync.yml +166 -0
  13. vycari_pepper-0.1.0/.github/workflows/pip-audit.yml +70 -0
  14. vycari_pepper-0.1.0/.github/workflows/release.yml +82 -0
  15. vycari_pepper-0.1.0/.github/workflows/zizmor.yml +61 -0
  16. vycari_pepper-0.1.0/.gitignore +10 -0
  17. vycari_pepper-0.1.0/.greptile/rules.md +10 -0
  18. vycari_pepper-0.1.0/.markdownlint-cli2.jsonc +32 -0
  19. vycari_pepper-0.1.0/CLAUDE.md +44 -0
  20. vycari_pepper-0.1.0/LICENSE +202 -0
  21. vycari_pepper-0.1.0/NOTICE +2 -0
  22. vycari_pepper-0.1.0/PKG-INFO +22 -0
  23. vycari_pepper-0.1.0/README.md +413 -0
  24. vycari_pepper-0.1.0/contracts/README.md +31 -0
  25. vycari_pepper-0.1.0/contracts/api-v1.openapi.json +19733 -0
  26. vycari_pepper-0.1.0/pyproject.toml +170 -0
  27. vycari_pepper-0.1.0/src/pepper_cli/__init__.py +11 -0
  28. vycari_pepper-0.1.0/src/pepper_cli/__main__.py +12 -0
  29. vycari_pepper-0.1.0/src/pepper_cli/_version.py +33 -0
  30. vycari_pepper-0.1.0/src/pepper_cli/agent_skill.py +48 -0
  31. vycari_pepper-0.1.0/src/pepper_cli/api_client.py +3002 -0
  32. vycari_pepper-0.1.0/src/pepper_cli/cli.py +5474 -0
  33. vycari_pepper-0.1.0/src/pepper_cli/credentials.py +268 -0
  34. vycari_pepper-0.1.0/src/pepper_cli/data/agent-skill/SKILL.md +338 -0
  35. vycari_pepper-0.1.0/src/pepper_cli/next_view.py +251 -0
  36. vycari_pepper-0.1.0/src/pepper_cli/proctitle.py +27 -0
  37. vycari_pepper-0.1.0/src/pepper_cli/provider/__init__.py +29 -0
  38. vycari_pepper-0.1.0/src/pepper_cli/provider/fs.py +510 -0
  39. vycari_pepper-0.1.0/src/pepper_cli/render.py +1120 -0
  40. vycari_pepper-0.1.0/src/pepper_cli/secret.py +71 -0
  41. vycari_pepper-0.1.0/src/pepper_cli/skill_doc.py +99 -0
  42. vycari_pepper-0.1.0/src/pepper_cli/terminal_text.py +99 -0
  43. vycari_pepper-0.1.0/src/pepper_cli/window_title.py +66 -0
  44. vycari_pepper-0.1.0/tests/__init__.py +0 -0
  45. vycari_pepper-0.1.0/tests/conftest.py +16 -0
  46. vycari_pepper-0.1.0/tests/test_agent_skill.py +581 -0
  47. vycari_pepper-0.1.0/tests/test_api_client.py +748 -0
  48. vycari_pepper-0.1.0/tests/test_api_client_groups.py +799 -0
  49. vycari_pepper-0.1.0/tests/test_api_client_notes.py +451 -0
  50. vycari_pepper-0.1.0/tests/test_api_client_room_skills.py +151 -0
  51. vycari_pepper-0.1.0/tests/test_api_client_scheduled_tasks.py +400 -0
  52. vycari_pepper-0.1.0/tests/test_api_client_stream.py +351 -0
  53. vycari_pepper-0.1.0/tests/test_api_client_takeout.py +246 -0
  54. vycari_pepper-0.1.0/tests/test_api_client_todos.py +174 -0
  55. vycari_pepper-0.1.0/tests/test_api_contract.py +440 -0
  56. vycari_pepper-0.1.0/tests/test_cli.py +1209 -0
  57. vycari_pepper-0.1.0/tests/test_cli_calendar.py +261 -0
  58. vycari_pepper-0.1.0/tests/test_cli_groups.py +1322 -0
  59. vycari_pepper-0.1.0/tests/test_cli_groups_watch.py +730 -0
  60. vycari_pepper-0.1.0/tests/test_cli_groups_watch_scoped.py +926 -0
  61. vycari_pepper-0.1.0/tests/test_cli_next.py +231 -0
  62. vycari_pepper-0.1.0/tests/test_cli_notes.py +1300 -0
  63. vycari_pepper-0.1.0/tests/test_cli_room_commands.py +214 -0
  64. vycari_pepper-0.1.0/tests/test_cli_room_skills.py +386 -0
  65. vycari_pepper-0.1.0/tests/test_cli_scheduled_tasks.py +906 -0
  66. vycari_pepper-0.1.0/tests/test_cli_skills.py +240 -0
  67. vycari_pepper-0.1.0/tests/test_cli_takeout.py +108 -0
  68. vycari_pepper-0.1.0/tests/test_cli_todos.py +358 -0
  69. vycari_pepper-0.1.0/tests/test_client_tools_roundtrip.py +308 -0
  70. vycari_pepper-0.1.0/tests/test_credentials.py +195 -0
  71. vycari_pepper-0.1.0/tests/test_proctitle.py +78 -0
  72. vycari_pepper-0.1.0/tests/test_provider_fs.py +334 -0
  73. vycari_pepper-0.1.0/tests/test_repl.py +762 -0
  74. vycari_pepper-0.1.0/tests/test_secret.py +64 -0
  75. vycari_pepper-0.1.0/tests/test_terminal_text.py +635 -0
  76. vycari_pepper-0.1.0/tests/test_user_agent_and_upgrade.py +290 -0
  77. vycari_pepper-0.1.0/tests/test_window_title.py +105 -0
  78. 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,4 @@
1
+ # Every path in this repo is owned by the maintainer. The repo profile
2
+ # (Vycari/vycari, plugins/vycari-ops/references/repo-profile.json) owns this
3
+ # line's contents — it is the one file whose text the standard pins.
4
+ * @allenhutchison
@@ -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