Skip to content

Commit 8a00192

Browse files
OriNachumclaude
andauthored
Scaffold lepenseur as a full CLI/PyPI AgentCulture sibling (#1) (#3)
* docs: add scaffold design spec for issue #1 Design for scaffolding lepenseur as a full CLI/PyPI AgentCulture sibling: copy the lecodeur twin as base, add the agent-first quartet (whoami/learn/explain/overview/doctor), wire the afi doctor --strict rubric gate, and rewrite identity content for the thinker role. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: use bare lepenseur package name (not lepenseur-cli) Align packaging with the lecodeur twin: dist == package == script == lepenseur. Note the deviation from CLAUDE.md/issue #1 (which say lepenseur-cli) and that CLAUDE.md must be updated as part of the scaffold work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: refine spec (afi cli doctor gate, cli noun, doctor stub) + add implementation plan Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: scaffold lepenseur package from lecodeur twin (renamed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: rewrite identity for the thinker (catalog, learn, runtime files) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: correct CLI description to thinking agent The _build_parser description mistakenly called lepenseur the "local coding agent" — it is the "local thinking agent". Aligns CLI help text with CLAUDE.md, AGENTS.md, and the project's stated identity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add read-only overview verb Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add cli noun with cli overview (rubric cli_noun check) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add doctor stub verb (rubric bundle 7; semantics deferred) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: address code-quality review (format, public report(), cli --json) - Run black + isort to resolve CI lint blockers (overview.py, doctor.py, test file) - Rename _report() -> report() in whoami.py; update overview.py import and call site - Add --json argument to bare `cli` parser so `lepenseur cli --json` no longer errors - Add 'cli overview' self-description to cli_sections() Verbs list - Extend test_doctor_json_shape to assert 'remediation' key (full documented contract) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: add afi cli doctor --strict rubric gate to lint job Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: reframe vendored skills for lepenseur (provenance: steward) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add CHANGELOG and align CLAUDE.md to bare lepenseur package Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: fix stale lecodeur comment in markdownlint config Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: commit uv.lock (tracked, per sibling convention) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add sonar config, structured cli errors, mirror culture.yaml to AGENTS.md - Add sonar-project.properties (agentculture_lepenseur) — the missing file caused the tests.yml SonarCloud step to fail (no projectKey/organization). - cli noun: propagate parser_class to nested subparsers so 'cli overview' parse errors route through the structured error contract (Qodo bug). - culture.yaml system_prompt now mirrors AGENTS.md verbatim (Qodo rule). - Add regression test for structured cli overview parse errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style: merge implicitly concatenated string in doctor stub (SonarCloud) Single literal instead of adjacent-string concatenation; tightened wording ('not yet defined' -> 'undefined') so it fits the 100-char line limit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a09621f commit 8a00192

52 files changed

Lines changed: 5901 additions & 5 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills.local.yaml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Per-machine paths and settings for lepenseur's skills.
2+
#
3+
# Copy this file to `.claude/skills.local.yaml` (git-ignored) and fill in the
4+
# values for your machine. Skills read the local file and fall back to this
5+
# example when the local copy hasn't been created yet.
6+
7+
# Where sibling Culture projects are checked out on this machine. Used by the
8+
# `communicate` and `cicd` skills when they reference siblings. Defaults assume
9+
# the shared-workspace layout (siblings alongside this repo).
10+
workspace_root: ".."
11+
12+
# The Culture nick this repo's agent posts as. Resolved from culture.yaml's
13+
# `suffix:` when unset; the repo basename is the final fallback.
14+
nick: "lepenseur"

.claude/skills/cicd/SKILL.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
name: cicd
3+
description: >
4+
Steward's CI/CD lane, layered on `agex pr`. Delegates lint / open /
5+
read / reply / delta to agex; adds two steward extensions — `status`
6+
(SonarCloud quality gate + hotspots + unresolved-thread tally) and
7+
`await` (read --wait + status with non-zero exit on Sonar ERROR or
8+
unresolved threads). Use when: creating PRs in steward, handling
9+
review feedback, polling CI status, or the user says "create PR",
10+
"review comments", "address feedback", "resolve threads". Renamed
11+
from `pr-review` in steward 0.7.0; rebased on agex in 0.12.0.
12+
---
13+
14+
# CI/CD — Steward edition
15+
16+
> Vendored from steward (canonical upstream: `docs/skill-sources.md`); this copy is owned by lepenseur and may diverge.
17+
18+
`agex pr` (in `agentculture/agex-cli`) is the upstream for the
19+
five core PR-lifecycle verbs — `lint`, `open`, `read`, `reply`,
20+
`delta`. Steward used to vendor parallel scripts for each; in 0.12.0
21+
those vendored copies were dropped in favor of delegating to `agex`.
22+
What's left in this skill is **the steward-specific gating layer**:
23+
24+
- `status` — SonarCloud quality gate, OPEN issues, hotspots, deploy
25+
preview URL, unresolved-inline-thread tally.
26+
- `await` — composes `agex pr read --wait` with `status` and gates on
27+
Sonar `ERROR` / unresolved threads. The single command to run after
28+
pushing a fix when you want "wake me when this PR is triage-able."
29+
30+
Those two are the steward unique surface today. They're filed as a
31+
feature ask upstream
32+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41));
33+
once they land they migrate out of this skill.
34+
35+
The workflow is encapsulated in `scripts/workflow.sh` — follow that
36+
(or call `agex pr` directly).
37+
38+
## Prerequisites
39+
40+
Hard requirements: `agex` (>=0.1), `gh` (GitHub CLI), `jq`, `bash`,
41+
`python3` (stdlib only), `curl` (used by `pr-status.sh`).
42+
43+
Install agex once:
44+
45+
```bash
46+
uv tool install agex-cli # or: pip install --user agex-cli
47+
```
48+
49+
Soft requirement: `PyYAML` is needed **only for suffix mode** of the
50+
sibling `agent-config` skill, where it parses Culture's server
51+
manifest. Every `cicd` script works without it; suffix mode prints a
52+
clear install hint when invoked without it.
53+
54+
Per-machine paths (sibling-project layout) live in
55+
`.claude/skills.local.yaml`; see the committed `.example` for the
56+
schema. `agex pr delta` reads the same file.
57+
58+
## How to run
59+
60+
`scripts/workflow.sh` is the entry point. Subcommands:
61+
62+
| Command | What it does |
63+
|---------|--------------|
64+
| `workflow.sh lint` | `agex pr lint --exit-on-violation` — portability + alignment-trigger check. |
65+
| `workflow.sh open [gh-flags]` | `agex pr open --delayed-read`. Creates the PR, then polls 180s for an initial briefing. `--title TITLE` required; body via `--body-file PATH` or stdin. |
66+
| `workflow.sh read [PR] [--wait N]` | `agex pr read`. One-shot briefing (CI checks, SonarCloud gate + new issues, all comments, next-step footer). Pass `--wait N` to poll up to N seconds for required reviewers. |
67+
| `workflow.sh reply <PR>` | `agex pr reply <PR>` — batch JSONL replies (stdin) + thread resolve. agex auto-signs from `culture.yaml`. |
68+
| `workflow.sh delta` | `agex pr delta` — sibling alignment dump. |
69+
| `workflow.sh status <PR>` | **Steward extension.** `pr-status.sh` — Sonar gate, OPEN issues, hotspots, unresolved-thread breakdown, deploy preview URL. Authoritative gate for `await`. |
70+
| `workflow.sh await <PR>` | **Steward extension.** `agex pr read --wait` then `status`. Exits non-zero on Sonar ERROR or unresolved threads. Tunables: `STEWARD_PR_AWAIT_WAIT` (default 1800s passed to `--wait`), `STEWARD_PR_AWAIT_SECONDS` (legacy fixed pre-sleep, deprecated). |
71+
| `workflow.sh help` | Print the list. |
72+
73+
You can also call `agex pr <verb>` directly — `workflow.sh` is a
74+
typing-saver around the same verbs. The steward `status` and `await`
75+
extensions only have shell entry points.
76+
77+
The vendored single-comment helper `pr-reply.sh` (plus its
78+
`_resolve-nick.sh` dependency) is still shipped — pinned by
79+
`tests/test_pr_reply_signature.py` and `tests/test_resolve_nick.py`,
80+
and useful when a one-off reply doesn't merit batch JSONL. It is not
81+
called by `workflow.sh` anymore. The vendored `portability-lint.sh`
82+
is also still shipped — `steward doctor`'s portability check runs it
83+
directly against target repos. Both are scheduled for follow-up
84+
migration to agex.
85+
86+
## Long waits (background polling)
87+
88+
`agex pr read --wait N` polls in-session for up to N seconds. The
89+
Anthropic prompt cache has a 5-minute TTL; sleeping past it burns
90+
context every cache miss. Two ways to drive the wait:
91+
92+
- **Synchronous**`workflow.sh await <PR>` after `gh pr create` /
93+
`workflow.sh open`. Fine when readiness is expected within ~5
94+
minutes.
95+
- **Asynchronous** — for longer waits, run `agex pr read --wait NNN`
96+
inside a background subagent (Agent tool, `run_in_background: true`)
97+
so the main session only pays the cache cost when readiness fires.
98+
The subagent's only job is to invoke `agex pr read --wait` and echo
99+
its headline back. The parent triages with `workflow.sh await`
100+
when the notification arrives. The user can interrupt with
101+
TaskStop.
102+
103+
This pattern was originally borrowed from sibling repo
104+
[`agentculture/cfafi`](https://github.com/agentculture/cfafi)'s `poll`
105+
skill. The async guidance is also filed upstream
106+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
107+
108+
## Conventions
109+
110+
`agex pr` emits a **"Next step:"** footer at the end of every command
111+
that names the right next verb (the same chain `agex learn cicd`
112+
documents) — follow that rather than memorizing an order. `workflow.sh
113+
help` mirrors the verb table when you need the steward-flavored
114+
extensions (`status`, `await`) on top.
115+
116+
Branch naming: `fix/<desc>`, `feat/<desc>`, `docs/<desc>`,
117+
`skill/<name>`. PR / comment signature: `- <nick> (Claude)`, where
118+
`<nick>` is resolved by `agex` from the agent's own `culture.yaml`
119+
(first agent's `suffix`), falling back to the git-repo basename. agex
120+
auto-appends the signature on `pr open` and `pr reply` only when the
121+
body isn't already signed.
122+
123+
## Triage rules
124+
125+
For every comment, decide **FIX** or **PUSHBACK** with reasoning.
126+
127+
Default to **FIX** for: portability complaints (always valid for
128+
Steward — recurring bug class), test or doc requests, style nits
129+
aligned with workspace conventions.
130+
131+
Default to **PUSHBACK** for: architecture opinions that conflict with
132+
workspace `CLAUDE.md` or the all-backends rule; greenfield
133+
false-positives (e.g. "add tests" before there's any source — defer
134+
to a later PR, don't refuse).
135+
136+
### Alignment-delta rule
137+
138+
If the PR touches `CLAUDE.md`, `culture.yaml`, or anything under
139+
`.claude/skills/`, run `workflow.sh delta` **before** declaring FIX or
140+
PUSHBACK on each comment. Note any sibling that needs a follow-up PR
141+
and mention it in your reply.
142+
143+
## Greenfield-aware steps
144+
145+
The lint and the workflow script are always-on. Stack-specific steps
146+
are conditional and currently no-op (greenfield repo):
147+
148+
```bash
149+
[ -d tests ] && [ -f pyproject.toml ] && uv run pytest tests/ -x -q
150+
[ -f pyproject.toml ] && bump_version_per_project_convention # see project README
151+
[ -f .markdownlint-cli2.yaml ] && markdownlint-cli2 "$(git diff --name-only --cached '*.md')"
152+
```
153+
154+
Revisit each line as the corresponding stack element actually lands.
155+
A `pr lint --extra=tests,version,markdown` ask is filed upstream
156+
([agex-cli#41](https://github.com/agentculture/agex-cli/issues/41)).
157+
158+
## Reply etiquette
159+
160+
Every comment must get a reply — no silent fixes. `agex pr reply`
161+
includes thread-resolve by default. Reference the review-comment IDs
162+
in the fix-up commit message.
163+
164+
The `status` extension queries SonarCloud directly (it predates the
165+
upstream Sonar integration in `agex pr read`). Both surfaces are
166+
trustworthy — `agex pr read` for display in the briefing, `status` for
167+
the gate. Steward isn't yet a registered mesh agent, so the
168+
post-merge IRC ping that Culture's `pr-review` includes is still
169+
skipped — that returns when Steward joins the mesh.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Resolve the agent's nick for GitHub message signing.
5+
# Order: first agent's `suffix` in <repo-root>/culture.yaml,
6+
# then basename of the git repo root.
7+
# Prints the nick to stdout. Always exits 0 — pr-reply.sh needs *some*
8+
# nick to sign with — but if a culture.yaml exists and we couldn't
9+
# extract a suffix from it, emits a stderr warning so a misconfigured
10+
# manifest doesn't silently mask itself behind the basename fallback.
11+
12+
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
13+
if [[ -z "$repo_root" ]]; then
14+
repo_root="$PWD"
15+
fi
16+
17+
manifest="$repo_root/culture.yaml"
18+
19+
if [[ -f "$manifest" ]]; then
20+
if ! command -v python3 >/dev/null 2>&1; then
21+
echo "_resolve-nick: python3 not found; cannot parse $manifest, falling back to repo basename" >&2
22+
else
23+
nick="$(python3 - "$manifest" <<'PY' 2>/dev/null || true
24+
import re, sys
25+
path = sys.argv[1]
26+
with open(path, encoding="utf-8") as f:
27+
for raw in f:
28+
line = raw.rstrip("\n")
29+
m = re.match(r"^[\s-]*\s*suffix:\s*(\S+)", line)
30+
if m:
31+
print(m.group(1).strip("'\""))
32+
break
33+
PY
34+
)"
35+
if [[ -n "$nick" ]]; then
36+
printf '%s\n' "$nick"
37+
exit 0
38+
fi
39+
echo "_resolve-nick: $manifest exists but no suffix could be parsed; falling back to repo basename" >&2
40+
fi
41+
fi
42+
43+
basename "$repo_root"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
# Portability lint: catch path leaks and per-user config dependencies in
3+
# committed docs/configs before they ship in a PR. Steward's recurring bug
4+
# class.
5+
#
6+
# Usage: portability-lint.sh [--all]
7+
# default: lint files modified vs HEAD (staged + unstaged)
8+
# --all: lint all tracked files
9+
#
10+
# Exits 0 if clean, 1 if any leak is found.
11+
12+
set -euo pipefail
13+
14+
mode="${1:-diff}"
15+
case "$mode" in
16+
--all) files=$(git ls-files -- ':(exclude)*.lock') ;;
17+
diff|--diff) files=$(git diff --diff-filter=AMR --name-only HEAD -- ':(exclude)*.lock') ;;
18+
*) echo "Usage: $(basename "$0") [--all]" >&2; exit 2 ;;
19+
esac
20+
21+
[ -z "$files" ] && { echo "(no files to check)"; exit 0; }
22+
23+
# ----- Check 1: hard-coded /home/<user>/... paths -----
24+
hits1=$(echo "$files" | xargs -r grep -nE '/home/[a-z][a-z0-9_-]+/' 2>/dev/null || true)
25+
26+
# ----- Check 2: per-user dotfile *config* refs in committed docs/configs -----
27+
# Carve-outs (allowed, NOT flagged):
28+
# - ~/.claude/skills/<x>/scripts/ vendored tool calls
29+
# - ~/.culture/ Culture mesh data this skill is supposed to read
30+
md_yaml=$(echo "$files" | grep -E '\.(md|ya?ml|toml|json|jsonc)$' || true)
31+
if [ -n "$md_yaml" ]; then
32+
hits2=$(echo "$md_yaml" | xargs -r grep -nE '~/\.[A-Za-z]' 2>/dev/null \
33+
| grep -vE '~/\.claude/skills/[^[:space:]"]+/scripts/' \
34+
| grep -vE '~/\.culture/' \
35+
|| true)
36+
else
37+
hits2=""
38+
fi
39+
40+
fail=0
41+
if [ -n "$hits1" ]; then
42+
echo "❌ Hard-coded /home/<user>/ paths:"
43+
echo "$hits1" | sed 's/^/ /'
44+
echo " Fix: use ../sibling, repo URL, or \$WORKSPACE/sibling instead."
45+
fail=1
46+
fi
47+
if [ -n "$hits2" ]; then
48+
[ "$fail" -eq 1 ] && echo
49+
echo "❌ Per-user ~/.<dotfile> config refs in committed doc/config:"
50+
echo "$hits2" | sed 's/^/ /'
51+
echo " Allowed carve-outs: ~/.claude/skills/.../scripts/ (tool calls), ~/.culture/ (mesh data)."
52+
echo " Otherwise: commit a repo-local config or document a portable lookup."
53+
fail=1
54+
fi
55+
56+
[ "$fail" -eq 0 ] && echo "✓ portability lint clean ($(echo "$files" | wc -l | tr -d ' ') files checked)"
57+
exit $fail
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Reply to a PR review comment, optionally resolve its thread.
5+
# Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] PR_NUMBER COMMENT_ID "body"
6+
7+
REPO=""
8+
RESOLVE=false
9+
PRINT_BODY=false
10+
11+
while [[ $# -gt 0 ]]; do
12+
case "$1" in
13+
--repo) REPO="$2"; shift 2 ;;
14+
--resolve) RESOLVE=true; shift ;;
15+
--print-body) PRINT_BODY=true; shift ;;
16+
*) break ;;
17+
esac
18+
done
19+
20+
PR_NUMBER="${1:?Usage: pr-reply.sh [--repo OWNER/REPO] [--resolve] [--print-body] PR_NUMBER COMMENT_ID \"body\"}"
21+
COMMENT_ID="${2:?Missing COMMENT_ID}"
22+
BODY="${3:?Missing reply body}"
23+
24+
if [[ "$PRINT_BODY" != true && -z "$REPO" ]]; then
25+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
26+
fi
27+
28+
# Sign with the agent's nick. Resolved per invocation so siblings that
29+
# vendor this skill pick up their own culture.yaml suffix automatically.
30+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31+
NICK="$("$SCRIPT_DIR/_resolve-nick.sh")"
32+
SIG="- ${NICK} (Claude)"
33+
if ! printf '%s' "$BODY" | grep -qFx -- "$SIG"; then
34+
BODY="${BODY}
35+
36+
${SIG}"
37+
fi
38+
39+
if [[ "$PRINT_BODY" == true ]]; then
40+
printf '%s\n' "$BODY"
41+
exit 0
42+
fi
43+
44+
# Post reply
45+
REPLY_URL=$(gh api "repos/$REPO/pulls/$PR_NUMBER/comments/$COMMENT_ID/replies" \
46+
-f body="$BODY" \
47+
--jq '.html_url')
48+
echo "Replied: $REPLY_URL"
49+
50+
# Resolve thread if requested
51+
if [[ "$RESOLVE" == true ]]; then
52+
# Find the thread ID for this comment
53+
THREAD_ID=$(gh api graphql -f query="
54+
{
55+
repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
56+
pullRequest(number: $PR_NUMBER) {
57+
reviewThreads(first: 100) {
58+
nodes {
59+
id
60+
comments(first: 100) {
61+
nodes { databaseId }
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}" --jq ".data.repository.pullRequest.reviewThreads.nodes[] | select(any(.comments.nodes[]; .databaseId == $COMMENT_ID)) | .id")
68+
69+
if [[ -n "$THREAD_ID" ]]; then
70+
RESOLVED=$(gh api graphql -f query="
71+
mutation { resolveReviewThread(input: {threadId: \"$THREAD_ID\"}) { thread { isResolved } } }
72+
" --jq '.data.resolveReviewThread.thread.isResolved')
73+
echo "Resolved: $RESOLVED (thread $THREAD_ID)"
74+
else
75+
echo "Warning: could not find thread for comment $COMMENT_ID"
76+
fi
77+
fi

0 commit comments

Comments
 (0)