claudeloop 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claudeloop-0.2.0/.claude/settings.json +37 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-agent-sdk/SKILL.md +109 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-architecture/SKILL.md +112 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-docs/SKILL.md +71 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-domain-model/SKILL.md +131 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-quality-gates/SKILL.md +62 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-releasing/SKILL.md +81 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-rest-surface/SKILL.md +68 -0
- claudeloop-0.2.0/.claude/skills/claudeloop-testing/SKILL.md +95 -0
- claudeloop-0.2.0/.editorconfig +22 -0
- claudeloop-0.2.0/.gitattributes +13 -0
- claudeloop-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +76 -0
- claudeloop-0.2.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
- claudeloop-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +29 -0
- claudeloop-0.2.0/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- claudeloop-0.2.0/.github/dependabot.yml +31 -0
- claudeloop-0.2.0/.github/workflows/ci.yml +122 -0
- claudeloop-0.2.0/.github/workflows/docs.yml +46 -0
- claudeloop-0.2.0/.github/workflows/publish-to-pypi.yml +118 -0
- claudeloop-0.2.0/.github/workflows/release-please.yml +27 -0
- claudeloop-0.2.0/.gitignore +45 -0
- claudeloop-0.2.0/.pre-commit-config.yaml +72 -0
- claudeloop-0.2.0/.release-please-manifest.json +3 -0
- claudeloop-0.2.0/CHANGELOG.md +38 -0
- claudeloop-0.2.0/CLAUDE.md +56 -0
- claudeloop-0.2.0/CODE_OF_CONDUCT.md +68 -0
- claudeloop-0.2.0/CONTRIBUTING.md +212 -0
- claudeloop-0.2.0/LICENSE +21 -0
- claudeloop-0.2.0/PKG-INFO +165 -0
- claudeloop-0.2.0/README.md +110 -0
- claudeloop-0.2.0/SECURITY.md +78 -0
- claudeloop-0.2.0/docs/architecture/decisions/0001-onion-architecture-with-import-linter.md +41 -0
- claudeloop-0.2.0/docs/architecture/decisions/0002-agent-sdk-over-subprocess.md +51 -0
- claudeloop-0.2.0/docs/architecture/decisions/0003-credits-exhausted-distinct-from-window-exhausted.md +53 -0
- claudeloop-0.2.0/docs/architecture/decisions/0004-adaptive-waiting-with-probes-not-sleep.md +62 -0
- claudeloop-0.2.0/docs/architecture/decisions/0005-retry-watchdog-off-by-default.md +49 -0
- claudeloop-0.2.0/docs/architecture/decisions/0006-generated-rest-surface-not-hand-written.md +58 -0
- claudeloop-0.2.0/docs/architecture/decisions/0007-ask-user-question-denied-with-guidance.md +48 -0
- claudeloop-0.2.0/docs/architecture/domain-model.md +232 -0
- claudeloop-0.2.0/docs/architecture/overview.md +95 -0
- claudeloop-0.2.0/docs/architecture/ports-and-adapters.md +89 -0
- claudeloop-0.2.0/docs/architecture/run-loop-state-machine.md +114 -0
- claudeloop-0.2.0/docs/contributing/development.md +106 -0
- claudeloop-0.2.0/docs/contributing/documentation.md +69 -0
- claudeloop-0.2.0/docs/contributing/release-process.md +87 -0
- claudeloop-0.2.0/docs/contributing/testing.md +136 -0
- claudeloop-0.2.0/docs/getting-started/configuration.md +49 -0
- claudeloop-0.2.0/docs/getting-started/installation.md +59 -0
- claudeloop-0.2.0/docs/getting-started/quickstart.md +62 -0
- claudeloop-0.2.0/docs/guides/autonomous-runs.md +67 -0
- claudeloop-0.2.0/docs/guides/completion-detection.md +59 -0
- claudeloop-0.2.0/docs/guides/live-testing.md +70 -0
- claudeloop-0.2.0/docs/guides/never-blocking.md +40 -0
- claudeloop-0.2.0/docs/guides/rate-limits-and-credits.md +62 -0
- claudeloop-0.2.0/docs/guides/rest-api-surface.md +67 -0
- claudeloop-0.2.0/docs/index.md +42 -0
- claudeloop-0.2.0/docs/plans/architecture-and-roadmap.md +174 -0
- claudeloop-0.2.0/docs/plans/foss-and-documentation-plan.md +196 -0
- claudeloop-0.2.0/docs/reference/api.md +19 -0
- claudeloop-0.2.0/docs/reference/cli.md +37 -0
- claudeloop-0.2.0/legacy/claude_autoresume.py +674 -0
- claudeloop-0.2.0/mkdocs.yml +91 -0
- claudeloop-0.2.0/pyproject.toml +154 -0
- claudeloop-0.2.0/release-please-config.json +13 -0
- claudeloop-0.2.0/scripts/check_skill_frontmatter.py +98 -0
- claudeloop-0.2.0/src/claudeloop/__init__.py +4 -0
- claudeloop-0.2.0/src/claudeloop/application/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/application/dto.py +40 -0
- claudeloop-0.2.0/src/claudeloop/application/ports.py +69 -0
- claudeloop-0.2.0/src/claudeloop/application/runner.py +140 -0
- claudeloop-0.2.0/src/claudeloop/application/usecases/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/application/usecases/doctor.py +98 -0
- claudeloop-0.2.0/src/claudeloop/application/usecases/list_sessions.py +11 -0
- claudeloop-0.2.0/src/claudeloop/application/usecases/resume_session.py +34 -0
- claudeloop-0.2.0/src/claudeloop/application/usecases/run_plan.py +41 -0
- claudeloop-0.2.0/src/claudeloop/bootstrap.py +93 -0
- claudeloop-0.2.0/src/claudeloop/cli/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/cli/app.py +60 -0
- claudeloop-0.2.0/src/claudeloop/cli/asyncio.py +50 -0
- claudeloop-0.2.0/src/claudeloop/cli/commands/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/cli/commands/doctor.py +25 -0
- claudeloop-0.2.0/src/claudeloop/cli/commands/resume.py +86 -0
- claudeloop-0.2.0/src/claudeloop/cli/commands/run.py +76 -0
- claudeloop-0.2.0/src/claudeloop/cli/commands/sessions.py +25 -0
- claudeloop-0.2.0/src/claudeloop/cli/render.py +47 -0
- claudeloop-0.2.0/src/claudeloop/domain/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/domain/budget.py +60 -0
- claudeloop-0.2.0/src/claudeloop/domain/capacity.py +53 -0
- claudeloop-0.2.0/src/claudeloop/domain/classify.py +73 -0
- claudeloop-0.2.0/src/claudeloop/domain/completion.py +65 -0
- claudeloop-0.2.0/src/claudeloop/domain/errors.py +26 -0
- claudeloop-0.2.0/src/claudeloop/domain/loop.py +196 -0
- claudeloop-0.2.0/src/claudeloop/domain/plan.py +73 -0
- claudeloop-0.2.0/src/claudeloop/domain/session.py +61 -0
- claudeloop-0.2.0/src/claudeloop/domain/waiting.py +86 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/autonomy.py +83 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/catalog.py +53 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/gateway.py +102 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/options.py +69 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/agent/translate.py +141 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/api/__init__.py +0 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/audit.py +26 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/clock.py +27 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/config.py +93 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/doctor_env.py +86 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/lock.py +41 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/logging.py +79 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/notify.py +16 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/progress.py +18 -0
- claudeloop-0.2.0/src/claudeloop/infrastructure/state.py +27 -0
- claudeloop-0.2.0/src/claudeloop/py.typed +0 -0
- claudeloop-0.2.0/tests/__init__.py +0 -0
- claudeloop-0.2.0/tests/application/__init__.py +0 -0
- claudeloop-0.2.0/tests/application/fakes.py +127 -0
- claudeloop-0.2.0/tests/application/test_runner.py +176 -0
- claudeloop-0.2.0/tests/application/test_usecases.py +178 -0
- claudeloop-0.2.0/tests/cli/__init__.py +0 -0
- claudeloop-0.2.0/tests/cli/test_app.py +76 -0
- claudeloop-0.2.0/tests/domain/__init__.py +0 -0
- claudeloop-0.2.0/tests/domain/test_budget.py +78 -0
- claudeloop-0.2.0/tests/domain/test_capacity.py +23 -0
- claudeloop-0.2.0/tests/domain/test_classify.py +96 -0
- claudeloop-0.2.0/tests/domain/test_completion.py +37 -0
- claudeloop-0.2.0/tests/domain/test_loop.py +183 -0
- claudeloop-0.2.0/tests/domain/test_plan.py +66 -0
- claudeloop-0.2.0/tests/domain/test_session.py +48 -0
- claudeloop-0.2.0/tests/domain/test_waiting.py +193 -0
- claudeloop-0.2.0/tests/infrastructure/__init__.py +0 -0
- claudeloop-0.2.0/tests/infrastructure/test_catalog.py +77 -0
- claudeloop-0.2.0/tests/infrastructure/test_config.py +75 -0
- claudeloop-0.2.0/tests/infrastructure/test_translate.py +39 -0
- claudeloop-0.2.0/tests/live/__init__.py +0 -0
- claudeloop-0.2.0/tests/live/conftest.py +61 -0
- claudeloop-0.2.0/tests/live/test_free_tier.py +110 -0
- claudeloop-0.2.0/tests/live/test_paid_tier.py +133 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Agent(*)",
|
|
6
|
+
"Artifact(*)",
|
|
7
|
+
"Bash(*)",
|
|
8
|
+
"Cd(*)",
|
|
9
|
+
"Edit(*)",
|
|
10
|
+
"EnterWorktree(*)",
|
|
11
|
+
"ExitPlanMode(*)",
|
|
12
|
+
"Glob(*)",
|
|
13
|
+
"Grep(*)",
|
|
14
|
+
"KillShell(*)",
|
|
15
|
+
"LSP(*)",
|
|
16
|
+
"Monitor(*)",
|
|
17
|
+
"MultiEdit(*)",
|
|
18
|
+
"NotebookEdit(*)",
|
|
19
|
+
"PowerShell(*)",
|
|
20
|
+
"Read(*)",
|
|
21
|
+
"ShareOnboardingGuide(*)",
|
|
22
|
+
"Skill(*)",
|
|
23
|
+
"TaskCreate(*)",
|
|
24
|
+
"TaskGet(*)",
|
|
25
|
+
"TaskList(*)",
|
|
26
|
+
"TaskOutput(*)",
|
|
27
|
+
"TaskStop(*)",
|
|
28
|
+
"TaskUpdate(*)",
|
|
29
|
+
"TodoWrite(*)",
|
|
30
|
+
"ToolSearch(*)",
|
|
31
|
+
"WebFetch(*)",
|
|
32
|
+
"WebSearch(*)",
|
|
33
|
+
"Workflow(*)",
|
|
34
|
+
"Write(*)"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-agent-sdk
|
|
3
|
+
description: Explains how claudeloop integrates with claude-agent-sdk (ClaudeAgentOptions, ClaudeSDKClient vs query(), RateLimitEvent, ResultMessage, AssistantMessage, session discovery via list_sessions()) and the never-block-on-a-human design (permission_mode, can_use_tool, AskUserQuestion handling, hooks). Use this whenever building or modifying src/claudeloop/infrastructure/agent/ (the M2+ agent gateway, options builder, message translation, session catalog, capacity probe), whenever the user asks about the Claude Agent SDK, ClaudeAgentOptions fields, session resumption, rate-limit events, or why a run might block on a human. Make sure to consult this before writing any code that calls claude_agent_sdk directly — infrastructure/agent/ is the ONLY place that import is allowed, per the onion architecture (see the claudeloop-architecture skill), and getting the never-block guarantees wrong here defeats the entire purpose of this project.
|
|
4
|
+
allowed-tools: Read Grep Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop + claude-agent-sdk integration
|
|
8
|
+
|
|
9
|
+
`infrastructure/agent/` is the only place `claude_agent_sdk`
|
|
10
|
+
may be imported — see the `claudeloop-architecture` skill for the enforced
|
|
11
|
+
onion rule. This skill covers the SDK integration specifics; consult
|
|
12
|
+
`claudeloop-domain-model` for how the resulting signals get classified.
|
|
13
|
+
|
|
14
|
+
## `ClaudeSDKClient` over `query()` — this is not optional
|
|
15
|
+
|
|
16
|
+
`query()` (single-shot) raises a **plain `Exception`** after yielding an
|
|
17
|
+
error `ResultMessage`, and the underlying process exits non-zero. A
|
|
18
|
+
streaming-input `ClaudeSDKClient` **stays alive** across error results — you
|
|
19
|
+
can keep sending messages on one live process with no resume/respawn. The
|
|
20
|
+
`AgentGateway` adapter (infrastructure/agent/gateway.py::ClaudeAgentGateway) MUST use `ClaudeSDKClient`, not `query()`, or
|
|
21
|
+
the whole point of collapsing the legacy respawn loop is lost. See ADR 0002
|
|
22
|
+
for the full reasoning.
|
|
23
|
+
|
|
24
|
+
## Key `ClaudeAgentOptions` fields for this project
|
|
25
|
+
|
|
26
|
+
- **`permission_mode="bypassPermissions"`** — required for autonomy. Note:
|
|
27
|
+
the Python SDK has **no `dangerously_skip_permissions` field**; this is
|
|
28
|
+
its equivalent. Do not go looking for a field that doesn't exist.
|
|
29
|
+
- **`can_use_tool`** — a defensive callback that must NEVER await input.
|
|
30
|
+
Intercepts `AskUserQuestion` specifically and **denies with guidance**
|
|
31
|
+
(not auto-answers) — see the "Never blocking" section below.
|
|
32
|
+
- **`output_format`** — a JSON schema
|
|
33
|
+
(`{complete: bool, remaining_work: [str], blocked_on: str|null, summary:
|
|
34
|
+
str}`) so each turn returns a `ResultMessage.structured_output` the
|
|
35
|
+
domain layer's `completion.py` can evaluate directly, rather than
|
|
36
|
+
substring-matching output text.
|
|
37
|
+
- **`resume` / `continue_conversation` / `fork_session` / `session_id`** —
|
|
38
|
+
`session_id` cannot combine with `resume`/`continue_conversation` unless
|
|
39
|
+
`fork_session=True` is also set. Get this wrong and session resumption
|
|
40
|
+
breaks in a way that's easy to miss in a quick test.
|
|
41
|
+
- **`setting_sources=None`** for the throwaway capacity probe specifically
|
|
42
|
+
— no `CLAUDE.md` should load for a one-token "are we still limited"
|
|
43
|
+
check.
|
|
44
|
+
- **`hooks`** — `PermissionRequest` auto-allows; `Notification` logs only,
|
|
45
|
+
never blocks.
|
|
46
|
+
|
|
47
|
+
## Never blocking on a human — every mitigation, concretely
|
|
48
|
+
|
|
49
|
+
| Stall path | Mitigation |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Permission prompts | `permission_mode="bypassPermissions"` |
|
|
52
|
+
| Unexpected permission path | Defensive `can_use_tool` returning `PermissionResultAllow` without ever awaiting input |
|
|
53
|
+
| `AskUserQuestion` | Intercepted, **denied with guidance** — e.g. "running autonomously, no user available — choose the option you would recommend, note the assumption, and proceed." NEVER fabricate an answer; that silently invents a decision nobody made. |
|
|
54
|
+
| `ExitPlanMode` | Auto-approved |
|
|
55
|
+
| `Notification` hooks | Logged, never awaited |
|
|
56
|
+
| Model asks "Shall I proceed?" in plain text | No tool call to intercept — mitigated via an appended system-prompt fragment establishing autonomous operation |
|
|
57
|
+
| TTY-dependent stdin | Never inherit a TTY — must be safe under `nohup`/systemd |
|
|
58
|
+
| MCP OAuth | Genuinely cannot complete unattended — the `doctor` command checks configured MCP servers up front and fails fast, naming them, rather than discovering the problem mid-run |
|
|
59
|
+
|
|
60
|
+
If you're adding a new tool-interception path, apply the same test every
|
|
61
|
+
time: **can this ever wait on stdin or a human response?** If yes, it needs
|
|
62
|
+
an explicit mitigation in this table before it ships.
|
|
63
|
+
|
|
64
|
+
## Rate-limit signals — read three, trust none alone
|
|
65
|
+
|
|
66
|
+
`RateLimitEvent.rate_limit_info`: `status` (`allowed`/`allowed_warning`/
|
|
67
|
+
`rejected`), `resets_at`, `rate_limit_type`
|
|
68
|
+
(`five_hour`/`seven_day`/`seven_day_opus`/`seven_day_sonnet`/`overage`),
|
|
69
|
+
`utilization`, `overage_status`, `overage_resets_at`,
|
|
70
|
+
`overage_disabled_reason`. The Claude Code binary contains the string
|
|
71
|
+
`[sdkMessageAdapter] Ignoring rate_limit_event message` — treat this event
|
|
72
|
+
as possibly absent and always corroborate with `ResultMessage
|
|
73
|
+
.api_error_status` and `AssistantMessage.error` (see the
|
|
74
|
+
`claudeloop-domain-model` skill's `TurnSignals` section for exactly how
|
|
75
|
+
these three combine).
|
|
76
|
+
|
|
77
|
+
## Session discovery — use the supported API
|
|
78
|
+
|
|
79
|
+
`list_sessions()` / `get_session_info()` return `SDKSessionInfo` (session_id,
|
|
80
|
+
summary, last_modified, custom_title, first_prompt, git_branch, cwd, tag,
|
|
81
|
+
created_at) via cheap stat + head/tail reads. **Never** hand-parse
|
|
82
|
+
`~/.claude/projects/<encoded-cwd>/*.jsonl` directly — the docs explicitly
|
|
83
|
+
warn the transcript format changes between Claude Code releases, and this
|
|
84
|
+
API exists specifically to replace that fragile approach from the legacy
|
|
85
|
+
script.
|
|
86
|
+
|
|
87
|
+
## The capacity probe — deliberately minimal
|
|
88
|
+
|
|
89
|
+
The throwaway turn used while `WAITING` (see `claudeloop-domain-model`):
|
|
90
|
+
one-token prompt, `max_turns=1`, no tools, `setting_sources=None`, and
|
|
91
|
+
`extra_args={"no-session-persistence": None}` so it leaves no transcript
|
|
92
|
+
and doesn't pollute the working session's history with "OK" turns. A
|
|
93
|
+
rejected probe isn't billed by the API.
|
|
94
|
+
|
|
95
|
+
## `CLAUDE_CODE_RETRY_WATCHDOG` — deliberately off by default
|
|
96
|
+
|
|
97
|
+
Do not set this env var in the default agent gateway configuration. It
|
|
98
|
+
retries 429/529 in-process indefinitely with no progress reporting, no
|
|
99
|
+
credits-vs-window discrimination, and no `--max-wait`. It's exposed as an
|
|
100
|
+
explicit opt-in flag (`--retry-watchdog`), never the default. See ADR 0005
|
|
101
|
+
for the full reasoning if you're tempted to "simplify" by using it instead
|
|
102
|
+
of the probe-based waiting policy.
|
|
103
|
+
|
|
104
|
+
## Full reference
|
|
105
|
+
|
|
106
|
+
`docs/architecture/decisions/0002-agent-sdk-over-subprocess.md`,
|
|
107
|
+
`docs/architecture/decisions/0005-retry-watchdog-off-by-default.md`,
|
|
108
|
+
`docs/architecture/decisions/0007-ask-user-question-denied-with-guidance.md`,
|
|
109
|
+
`docs/guides/never-blocking.md`, `docs/guides/rate-limits-and-credits.md`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-architecture
|
|
3
|
+
description: Explains claudeloop's onion architecture — the four layers (domain, application, infrastructure, cli), the import-linter contract enforcing them, and exactly where new code belongs. Use this whenever adding a new file, class, or function to src/claudeloop/, whenever unsure which layer something belongs in, whenever import-linter or lint-imports fails, or when the user mentions "onion architecture", "layers", "ports and adapters", "bootstrap.py", "composition root", or asks "where should this go". Make sure to consult this before writing any new module under src/claudeloop/ — placing code in the wrong layer is the most common mistake in this codebase and import-linter will reject it in CI regardless.
|
|
4
|
+
allowed-tools: Read Grep Glob Bash(lint-imports)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop architecture
|
|
8
|
+
|
|
9
|
+
`claudeloop` is a strict onion / ports-and-adapters design. Dependencies
|
|
10
|
+
point inward only. This is enforced in CI and pre-commit by `import-linter`,
|
|
11
|
+
not by convention — a violation fails with a named contract error, not a
|
|
12
|
+
review comment.
|
|
13
|
+
|
|
14
|
+
## The four layers, innermost first
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/claudeloop/
|
|
18
|
+
├── domain/ # PURE. stdlib only. No I/O, no async, no third-party imports.
|
|
19
|
+
├── application/ # Protocol-based ports + use cases. Imports domain + stdlib only.
|
|
20
|
+
├── infrastructure/ # Adapters. The ONLY layer allowed to import anthropic /
|
|
21
|
+
│ # claude_agent_sdk / structlog / httpx / any third-party SDK.
|
|
22
|
+
├── cli/ # Typer commands. Calls application use cases via bootstrap.
|
|
23
|
+
└── bootstrap.py # Composition root — the ONE module allowed to see every layer.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The decision test — where does new code go?
|
|
27
|
+
|
|
28
|
+
Ask in this order:
|
|
29
|
+
|
|
30
|
+
1. **Does it touch the filesystem, network, clock, or an SDK?**
|
|
31
|
+
→ `infrastructure/`, behind a `Protocol` port defined in
|
|
32
|
+
`application/ports.py`. Never call `anthropic.*` or `claude_agent_sdk.*`
|
|
33
|
+
from anywhere else.
|
|
34
|
+
|
|
35
|
+
2. **Is it a decision — a branch determining what happens next — with zero
|
|
36
|
+
I/O of its own?**
|
|
37
|
+
→ `domain/`. Test: can you write its test as
|
|
38
|
+
`assert some_function(SomeDataclass(...)) == ExpectedResult(...)` with no
|
|
39
|
+
mocking, no fixtures beyond dataclass literals? If yes, it's domain.
|
|
40
|
+
Existing examples: `domain/classify.py` (turn signals → capacity state),
|
|
41
|
+
`domain/waiting.py` (capacity state → next probe instant),
|
|
42
|
+
`domain/completion.py` (structured output → completion verdict),
|
|
43
|
+
`domain/loop.py` (the run-loop state machine itself).
|
|
44
|
+
|
|
45
|
+
3. **Is it orchestration — call a port, feed the result to a domain
|
|
46
|
+
function, call another port?**
|
|
47
|
+
→ `application/`, as a use case in `application/usecases/` or inside
|
|
48
|
+
`application/runner.py`.
|
|
49
|
+
|
|
50
|
+
4. **Is it argument parsing or terminal output formatting for a human?**
|
|
51
|
+
→ `cli/`.
|
|
52
|
+
|
|
53
|
+
**When in doubt, push logic inward.** A `cli/` command containing an
|
|
54
|
+
`if/elif` deciding what a rate-limit response *means* is a bug: that
|
|
55
|
+
decision belongs in `domain/classify.py`, testable with zero CLI process
|
|
56
|
+
spun up.
|
|
57
|
+
|
|
58
|
+
## The enforced import rules
|
|
59
|
+
|
|
60
|
+
From `pyproject.toml`:
|
|
61
|
+
|
|
62
|
+
```toml
|
|
63
|
+
[[tool.importlinter.contracts]]
|
|
64
|
+
name = "Onion layering"
|
|
65
|
+
type = "layers"
|
|
66
|
+
layers = ["claudeloop.cli", "claudeloop.bootstrap", "claudeloop.application", "claudeloop.domain"]
|
|
67
|
+
|
|
68
|
+
[[tool.importlinter.contracts]]
|
|
69
|
+
name = "Infrastructure only reachable from bootstrap"
|
|
70
|
+
type = "forbidden"
|
|
71
|
+
source_modules = ["claudeloop.domain", "claudeloop.application"]
|
|
72
|
+
forbidden_modules = ["claudeloop.infrastructure"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Concretely:
|
|
76
|
+
|
|
77
|
+
- `domain/*.py` may `import` from stdlib and other `domain/*` modules ONLY.
|
|
78
|
+
No `typing.Protocol`-based ports either — those live one layer out.
|
|
79
|
+
- `application/*.py` may import `domain` and stdlib. Ports are
|
|
80
|
+
`typing.Protocol`, never an ABC a concrete adapter must inherit from —
|
|
81
|
+
this is what keeps `application/` from ever needing to import
|
|
82
|
+
`infrastructure/` just to name a type.
|
|
83
|
+
- `infrastructure/*.py` may import `domain`, `application`, and any
|
|
84
|
+
third-party package. This is the ONLY place `anthropic` or
|
|
85
|
+
`claude_agent_sdk` appears in an `import` statement anywhere in
|
|
86
|
+
`src/claudeloop/`.
|
|
87
|
+
- `cli/*.py` calls into `application/` use cases obtained from
|
|
88
|
+
`bootstrap.build_runner(...)` (or equivalent) — never constructs an
|
|
89
|
+
`infrastructure/` adapter directly.
|
|
90
|
+
- `bootstrap.py` is the single seam permitted to import from every layer —
|
|
91
|
+
it's where a concrete adapter (e.g.
|
|
92
|
+
`infrastructure.agent.gateway.ClaudeAgentGateway`) gets wired into the
|
|
93
|
+
`Protocol` port `application/ports.py` declares (e.g. `AgentGateway`).
|
|
94
|
+
|
|
95
|
+
## Verifying your change respects the contract
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
lint-imports
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Run this before committing any new module. It's also a `pre-commit` hook and
|
|
102
|
+
runs in CI (`ci.yml`). A violation names the exact contract broken —
|
|
103
|
+
"Onion layering" or "Infrastructure only reachable from bootstrap" — and the
|
|
104
|
+
offending import chain.
|
|
105
|
+
|
|
106
|
+
## Full reference
|
|
107
|
+
|
|
108
|
+
See `docs/architecture/overview.md` for the complete layer table and
|
|
109
|
+
`docs/architecture/decisions/0001-onion-architecture-with-import-linter.md`
|
|
110
|
+
for why this was chosen over convention-only layering. See
|
|
111
|
+
`docs/architecture/ports-and-adapters.md` for the planned port list
|
|
112
|
+
(M2+) and why ports are `Protocol` rather than ABC.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-docs
|
|
3
|
+
description: Explains where different kinds of content belong in this repo (CLAUDE.md vs .claude/skills/ vs docs/), how to build and strict-check the MkDocs Material site, and the writing conventions used throughout (Roadmap admonitions, relative links, ADR format). Use this whenever writing or editing any file under docs/, whenever deciding whether new content belongs in CLAUDE.md, a skill, or docs/, whenever the user asks about documentation structure, or before adding a page to mkdocs.yml's nav. Make sure to consult this before adding procedural, multi-step content to CLAUDE.md directly — CLAUDE.md is deliberately kept short and holds only facts; procedures belong in a skill instead, and getting this wrong bloats the context every single session pays for.
|
|
4
|
+
allowed-tools: Read Grep Glob Bash(mkdocs *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop documentation
|
|
8
|
+
|
|
9
|
+
## Where content belongs — the decision table
|
|
10
|
+
|
|
11
|
+
| Content | Goes in |
|
|
12
|
+
|---|---|
|
|
13
|
+
| An always-true fact, cheap to state, every session needs | `CLAUDE.md` — kept deliberately SHORT, a router not a manual |
|
|
14
|
+
| A procedure for working on a specific part of this codebase | `.claude/skills/<name>/SKILL.md` |
|
|
15
|
+
| User-facing "how do I..." | `docs/getting-started/` or `docs/guides/` |
|
|
16
|
+
| System design — what exists, how pieces fit | `docs/architecture/` |
|
|
17
|
+
| The reasoning behind ONE specific hard decision | `docs/architecture/decisions/` — an ADR |
|
|
18
|
+
| Generated API docs | `docs/reference/api.md` via `mkdocstrings` — never hand-write signatures that already exist as docstrings |
|
|
19
|
+
| Contributor process | `docs/contributing/` |
|
|
20
|
+
| Historical plans, preserved verbatim once superseded | `docs/plans/` |
|
|
21
|
+
|
|
22
|
+
**The CLAUDE.md/skill dividing line, precisely: CLAUDE.md holds facts,
|
|
23
|
+
skills hold procedures.** If you're about to add a multi-step "when doing
|
|
24
|
+
X, do Y then Z" instruction to `CLAUDE.md`, it belongs in a skill instead —
|
|
25
|
+
skills load into context only when relevant, so long procedural content
|
|
26
|
+
there is free until needed, while everything in `CLAUDE.md` costs context
|
|
27
|
+
on every single session regardless of relevance.
|
|
28
|
+
|
|
29
|
+
## Building and checking the site
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e ".[docs]"
|
|
33
|
+
mkdocs serve # local preview at http://127.0.0.1:8000, live-reloads
|
|
34
|
+
mkdocs build --strict # what CI runs — fails on ANY warning, most commonly a broken link
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run `mkdocs build --strict` before opening a PR that touches `docs/` — it's
|
|
38
|
+
exactly what `docs.yml` runs in CI.
|
|
39
|
+
|
|
40
|
+
## Writing conventions
|
|
41
|
+
|
|
42
|
+
- **Plain markdown only** — the extensions enabled in `mkdocs.yml`
|
|
43
|
+
(admonitions, code fences, tables) and nothing docs-only-syntax beyond
|
|
44
|
+
that, because every page must read correctly as plain text on GitHub
|
|
45
|
+
(`CLAUDE.md` and the skills link straight to file paths, not built-site
|
|
46
|
+
URLs).
|
|
47
|
+
- **Relative links, not absolute site URLs** — so links work both on GitHub
|
|
48
|
+
and in the built MkDocs site.
|
|
49
|
+
- **Explain "why," not just "what."** A page restating a function signature
|
|
50
|
+
the code already shows isn't earning its place — put reasoning a stranger
|
|
51
|
+
can't get from the code alone into the page. This is exactly the ADRs'
|
|
52
|
+
purpose.
|
|
53
|
+
- **Mark roadmap content with `!!! note "Roadmap"`** at the top of any page
|
|
54
|
+
describing a not-yet-built milestone, so a reader never mistakes a design
|
|
55
|
+
intention for current behavior. `grep -rl '!!! note "Roadmap"' docs/`
|
|
56
|
+
finds every page that needs revisiting as a milestone lands.
|
|
57
|
+
- **New page → add it to `mkdocs.yml`'s `nav:`** or `mkdocs build --strict`
|
|
58
|
+
will not error (an orphaned page is not itself a strict-mode failure) but
|
|
59
|
+
the page becomes unreachable from the site nav — always add it.
|
|
60
|
+
|
|
61
|
+
## ADR format
|
|
62
|
+
|
|
63
|
+
Follow the existing files in `docs/architecture/decisions/` exactly:
|
|
64
|
+
`NNNN-kebab-case-title.md`, sections `## Status` / `## Context` / `##
|
|
65
|
+
Decision` / `## Consequences`, four-digit zero-padded sequential numbering.
|
|
66
|
+
Add a new ADR when a PR makes a hard, non-obvious design call worth
|
|
67
|
+
preserving the reasoning for — not for routine changes.
|
|
68
|
+
|
|
69
|
+
## Full reference
|
|
70
|
+
|
|
71
|
+
`docs/contributing/documentation.md`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-domain-model
|
|
3
|
+
description: Explains every value object and ADT in src/claudeloop/domain/ — CapacityState (Available/WindowExhausted/CreditsExhausted/AuthenticationFailed), TurnSignals classification, CompletionVerdict (Done/Continue/Blocked), the wait-probe policy in waiting.py, Budget/BudgetLedger, and the run-loop state machine in loop.py. Use this whenever reading, modifying, or extending anything in src/claudeloop/domain/ or tests/domain/, whenever the user asks about rate-limit classification, credits vs. rate limits, capacity states, completion detection, the waiting/backoff policy, or the run-loop state machine, and whenever adding a new domain type or branch. Make sure to consult this before touching domain/classify.py, domain/waiting.py, domain/completion.py, or domain/loop.py — the ordering of branches in each is deliberate and tested, and an out-of-order edit silently reintroduces bugs this project was specifically built to fix.
|
|
4
|
+
allowed-tools: Read Grep Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop domain model
|
|
8
|
+
|
|
9
|
+
Everything in `src/claudeloop/domain/` is a frozen dataclass or a closed
|
|
10
|
+
union of them, has zero third-party imports, and requires 100% test
|
|
11
|
+
coverage. This skill is the map; for full prose explanation with rationale
|
|
12
|
+
see `docs/architecture/domain-model.md`.
|
|
13
|
+
|
|
14
|
+
## `capacity.py` — CapacityState
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
CapacityState = Available | WindowExhausted | CreditsExhausted | AuthenticationFailed
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**The single most important fact in this codebase**: `CreditsExhausted` has
|
|
21
|
+
**no `resets_at` field at all** — not `None`, the type literally doesn't
|
|
22
|
+
carry one — because waiting for a clock can never fix an empty credits
|
|
23
|
+
balance. `WindowExhausted` carries `resets_at: datetime | None`.
|
|
24
|
+
`is_waitable(state)` is `False` only for `AuthenticationFailed`.
|
|
25
|
+
|
|
26
|
+
**Never conflate these two states** and never add a `resets_at` field to
|
|
27
|
+
`CreditsExhausted` "for consistency" — that would silently reintroduce the
|
|
28
|
+
exact bug (`claudeloop` replacing sleeping-an-hour-forever with something
|
|
29
|
+
smarter) this project exists to fix.
|
|
30
|
+
|
|
31
|
+
## `classify.py` — TurnSignals → CapacityState
|
|
32
|
+
|
|
33
|
+
`classify(signals: TurnSignals) -> CapacityState`. Reads **three
|
|
34
|
+
independent SDK signals** (a `RateLimitEvent`, `ResultMessage
|
|
35
|
+
.api_error_status`, `AssistantMessage.error`) — never trust
|
|
36
|
+
`RateLimitEvent` alone; it's reportedly dropped on some adapter paths.
|
|
37
|
+
|
|
38
|
+
**Ordering is load-bearing, in this exact sequence:**
|
|
39
|
+
|
|
40
|
+
1. `assistant_error == "authentication_failed"` → `AuthenticationFailed`, checked FIRST, outranks everything.
|
|
41
|
+
2. `rate_limit_status == "allowed_warning"` → `Available` (NOT a rejection — this is the exact false positive that caused multi-day cooldowns in the legacy script).
|
|
42
|
+
3. A rejection signal present → split further: credit signals (`error_code == "credits_required"`, `disabled_reason == "out_of_credits"`, a set `overage_disabled_reason`) win over a stray `resets_at` — check credits BEFORE falling through to `WindowExhausted`.
|
|
43
|
+
4. Anything else rejected → `WindowExhausted`, falling back to `rate_limit_type="unknown", resets_at=None` if thin.
|
|
44
|
+
|
|
45
|
+
If you're editing `classify.py`, preserve this order and re-run
|
|
46
|
+
`tests/domain/test_classify.py` — every branch above has a dedicated test
|
|
47
|
+
asserting it, including the adversarial case where a `resets_at` is present
|
|
48
|
+
alongside a credits signal (credits must still win).
|
|
49
|
+
|
|
50
|
+
## `completion.py` — CompletionVerdict
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
CompletionVerdict = Done | Continue | Blocked
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Primary signal: `StructuredVerdict` from `ClaudeAgentOptions.output_format`
|
|
57
|
+
(`{complete, remaining_work, blocked_on, summary}`). `blocked_on` outranks
|
|
58
|
+
`complete` — never let a turn claim both. Fallback (only when `structured is
|
|
59
|
+
None`): substring-match `CLAUDELOOP_TASK_FULLY_COMPLETE` (or the configured
|
|
60
|
+
marker) in raw output text — this is a fallback, not the primary path, and
|
|
61
|
+
must stay that way; the substring approach has two documented failure modes
|
|
62
|
+
(collision with user prompt text, truncation inside a limit message).
|
|
63
|
+
|
|
64
|
+
## `waiting.py` — WaitPolicyConfig & next_probe_instant()
|
|
65
|
+
|
|
66
|
+
`next_probe_instant(state, *, now, started_waiting_at, probe_count, config)
|
|
67
|
+
-> datetime`. **Never returns a duration to sleep — always the next instant
|
|
68
|
+
to probe.** Behavior differs by `CapacityState` type:
|
|
69
|
+
|
|
70
|
+
- `CreditsExhausted` — exponential backoff, `credits_probe_interval`
|
|
71
|
+
(default 120s) to `credits_probe_ceiling` (default 600s). **Compute the
|
|
72
|
+
backoff in float seconds and clamp to the ceiling BEFORE constructing a
|
|
73
|
+
`timedelta`** — `interval * factor**probe_count` unclamped overflows
|
|
74
|
+
`timedelta`'s magnitude limit at realistic probe counts. A Hypothesis
|
|
75
|
+
property test caught this during development; don't reintroduce it.
|
|
76
|
+
- `WindowExhausted(resets_at=X)` — `min(X + reset_grace, now +
|
|
77
|
+
window_probe_interval)`. Never trust a far-future `resets_at` alone; the
|
|
78
|
+
interval bound is what catches an early overage lift or credit top-up.
|
|
79
|
+
- `WindowExhausted(resets_at=None)` — falls back to `window_probe_interval`.
|
|
80
|
+
|
|
81
|
+
`config.max_wait`, when set, clamps every candidate to
|
|
82
|
+
`started_waiting_at + max_wait`; `wait_exceeded()` is the paired "give up"
|
|
83
|
+
check.
|
|
84
|
+
|
|
85
|
+
**Any new numeric field on `WaitPolicyConfig` needs a `__post_init__`
|
|
86
|
+
validation AND a Hypothesis property test** covering its invariant across
|
|
87
|
+
the full input space — not just hand-picked examples. See
|
|
88
|
+
`tests/domain/test_waiting.py` for the existing property tests as a
|
|
89
|
+
template.
|
|
90
|
+
|
|
91
|
+
## `budget.py` — Budget, BudgetLedger
|
|
92
|
+
|
|
93
|
+
Immutable. `spend_turn()` / `spend_attempt()` return a **new** ledger, never
|
|
94
|
+
mutate. `any_exhausted` ORs `turns_exhausted | dollars_exhausted |
|
|
95
|
+
attempts_exhausted`. An unset cap (`None`) is never exhausted.
|
|
96
|
+
|
|
97
|
+
## `loop.py` — the run-loop state machine
|
|
98
|
+
|
|
99
|
+
`Phase = PREFLIGHT | RUNNING | WAITING | PROBING | COMPLETE | FAILED`.
|
|
100
|
+
Three pure decision functions:
|
|
101
|
+
|
|
102
|
+
- `decide_preflight(state, capacity, *, now)` — before spending the first
|
|
103
|
+
real turn.
|
|
104
|
+
- `decide_after_turn(state, *, capacity, verdict, now)` — **the single most
|
|
105
|
+
important invariant in the whole codebase**: capacity is checked BEFORE
|
|
106
|
+
verdict, always. A `Done` verdict on a turn that also hit a rejection is
|
|
107
|
+
discarded — see `test_after_turn_limit_outranks_completion_claim`. Never
|
|
108
|
+
reorder this check.
|
|
109
|
+
- `decide_after_probe(state, capacity, *, now, config)` — after a throwaway
|
|
110
|
+
probe while waiting.
|
|
111
|
+
|
|
112
|
+
`Decision = SendTurn | RunProbe | ScheduleProbe | Finish` — a closed union
|
|
113
|
+
the `application/runner.py` executor pattern-matches
|
|
114
|
+
exhaustively, never partially.
|
|
115
|
+
|
|
116
|
+
## `plan.py` / `session.py`
|
|
117
|
+
|
|
118
|
+
`WorkPlan.parse(markdown)` extracts checkbox items (`- [ ]` / `- [x]`, both
|
|
119
|
+
bullet styles, either case). `with_items_marked_done(frozenset)` reconciles
|
|
120
|
+
a turn's `remaining_work` back against the plan. `SessionSelector =
|
|
121
|
+
PlanFileSelector | ExplicitSessionSelector | MostRecentSessionSelector` —
|
|
122
|
+
the legacy script's three input modes as a closed union instead of
|
|
123
|
+
if/elif on optional fields.
|
|
124
|
+
|
|
125
|
+
## Full reference
|
|
126
|
+
|
|
127
|
+
`docs/architecture/domain-model.md` (complete prose walkthrough),
|
|
128
|
+
`docs/architecture/run-loop-state-machine.md` (the state machine in detail,
|
|
129
|
+
including the credit-top-up worked example),
|
|
130
|
+
`docs/architecture/decisions/0003-*.md` through `0004-*.md` (why
|
|
131
|
+
`CreditsExhausted` is distinct, why probing beats sleeping).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-quality-gates
|
|
3
|
+
description: Covers how to run and FIX every quality gate in this repo — ruff (lint + format), mypy --strict, pytest with per-layer coverage, import-linter (lint-imports), bandit, and pip-audit. Use this whenever a quality gate fails locally or in CI, whenever the user asks how to lint, format, type-check, or run security scans on this codebase, or before opening a PR to run the full gate set proactively. Make sure to consult this whenever a ruff, mypy, lint-imports, or bandit error appears in tool output — it gives the exact fix command for each gate rather than requiring you to guess at generic remediation.
|
|
4
|
+
allowed-tools: Bash(ruff *) Bash(mypy *) Bash(pytest *) Bash(lint-imports) Bash(bandit *) Bash(pip-audit *) Bash(pre-commit *) Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop quality gates — run and fix
|
|
8
|
+
|
|
9
|
+
## The full set, in the order CI runs them
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
ruff check src tests
|
|
13
|
+
ruff format --check src tests
|
|
14
|
+
mypy src/claudeloop
|
|
15
|
+
pytest
|
|
16
|
+
lint-imports
|
|
17
|
+
bandit -q -r src/claudeloop
|
|
18
|
+
pip-audit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or the pre-commit-wired subset against your working tree:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pre-commit run --all-files
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Fixing each gate
|
|
28
|
+
|
|
29
|
+
| Gate | Symptom | Fix |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `ruff check` | Lint error | `ruff check --fix src tests` for auto-fixable rules; hand-edit the rest. Rule set: `E, F, I, UP, B, SIM, C4` (see `[tool.ruff.lint]` in `pyproject.toml`). |
|
|
32
|
+
| `ruff format --check` | Formatting diff | `ruff format src tests` |
|
|
33
|
+
| `mypy` | Type error | Add/correct annotations. `strict = true` repo-wide — no bare `Any` without a documented reason. `domain/` and `application/` in particular must type-check cleanly with zero suppressions. |
|
|
34
|
+
| `pytest` | Test failure or coverage below the per-layer floor | See the `claudeloop-testing` skill — check whether a branch is genuinely untested (add a test) vs. genuinely unreachable (a justified `# pragma: no cover`) |
|
|
35
|
+
| `lint-imports` | Onion-layering violation | See the `claudeloop-architecture` skill — the fix is almost always moving the offending code to the correct layer, not suppressing the contract |
|
|
36
|
+
| `bandit` | Flagged security pattern | Either fix the underlying issue, or — for a genuine false positive like an exhaustiveness `assert` on a closed union — add `# nosec B1xx` with an inline comment stating *why* it's safe. Two real examples exist in `src/claudeloop/domain/loop.py`; match that level of justification, don't just silence the warning. |
|
|
37
|
+
| `pip-audit` | Known CVE in a dependency | Bump the dependency. If no fix is available yet, this needs to be surfaced explicitly (in the PR description or an issue), not silently ignored — this project handles API credentials and bypasses permission prompts by design, so dependency CVEs are higher-stakes than usual. See `SECURITY.md`. |
|
|
38
|
+
|
|
39
|
+
## Bandit `# nosec` — the bar for using it
|
|
40
|
+
|
|
41
|
+
Only for a verified false positive, never to silence a real finding faster.
|
|
42
|
+
The comment must explain the specific reason the pattern is safe *here*,
|
|
43
|
+
matching this repo's existing style:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Precondition, not a security gate: CompletionVerdict is the closed union
|
|
47
|
+
# {Done, Blocked, Continue} and both other members are handled above, so this
|
|
48
|
+
# is exhaustive by construction — asserted here to fail loudly if a future
|
|
49
|
+
# variant is added to the union without a matching branch here.
|
|
50
|
+
assert isinstance(verdict, Continue) # nosec B101
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Before opening a PR
|
|
54
|
+
|
|
55
|
+
Run the full gate set (not just the pre-commit subset — `pytest` with
|
|
56
|
+
coverage and `pip-audit` aren't pre-commit hooks) once locally. A PR that
|
|
57
|
+
fails CI on a gate that would have caught locally wastes a review round.
|
|
58
|
+
|
|
59
|
+
## Full reference
|
|
60
|
+
|
|
61
|
+
`docs/contributing/development.md#running-the-quality-gates-locally`,
|
|
62
|
+
`CONTRIBUTING.md#quality-gates`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claudeloop-releasing
|
|
3
|
+
description: Explains the gitflow branch model (feature/* -> develop -> main), Conventional Commits requirements and types, how release-please automates version bumps and changelog generation from commit history, and how PyPI Trusted Publishing (OIDC) works via publish-to-pypi.yml. Use this whenever creating a branch, writing a commit message, opening a PR, asking about versioning, releases, or publishing to PyPI, or when a commit-msg hook rejects a commit. Make sure to consult this before writing any commit message in this repo — the commit-msg git hook enforces Conventional Commits and will reject anything that doesn't match, and getting the branch target or merge strategy wrong breaks the automated release pipeline downstream.
|
|
4
|
+
allowed-tools: Bash(git *) Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# claudeloop releasing — gitflow + Conventional Commits + Trusted Publishing
|
|
8
|
+
|
|
9
|
+
## Branch model
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
main ← always releasable; release-please opens release PRs here
|
|
13
|
+
▲ merge commit (preserves individual conventional commits for release-please)
|
|
14
|
+
develop ← integration branch; feature branches target this
|
|
15
|
+
▲ squash-merge (one conventional-commit-titled squash per feature)
|
|
16
|
+
feature/* ← your work — branch from develop, never from main
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Never branch from `main` directly, never target `main` with a feature
|
|
20
|
+
PR.** Feature work is `git checkout -b feature/<short-description> develop`,
|
|
21
|
+
PR into `develop`. `develop` → `main` happens as a merge commit (not
|
|
22
|
+
squashed) specifically so release-please can parse the individual commits
|
|
23
|
+
when it moves `main`.
|
|
24
|
+
|
|
25
|
+
## Conventional Commits — required, enforced by a git hook
|
|
26
|
+
|
|
27
|
+
Every commit message: `<type>[optional scope]: <description>`. The
|
|
28
|
+
`commit-msg` hook (installed by `pre-commit install`) rejects anything else
|
|
29
|
+
in `--strict` mode. Types and what each triggers on release:
|
|
30
|
+
|
|
31
|
+
| Type | Use for | Bump |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `feat` | new feature | minor |
|
|
34
|
+
| `fix` | bug fix | patch |
|
|
35
|
+
| `feat!` / `fix!` / `BREAKING CHANGE:` footer | breaking change | major |
|
|
36
|
+
| `docs` `style` `refactor` `test` `build` `ci` `chore` | no functional/patch/minor change | none |
|
|
37
|
+
| `perf` | performance improvement | patch |
|
|
38
|
+
| `revert` | reverts a prior commit | depends |
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
feat(domain): add CreditsExhausted as a distinct capacity state
|
|
42
|
+
fix(waiting): clamp exponential backoff before constructing timedelta
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Scope in parentheses is optional but strongly preferred — it makes the
|
|
46
|
+
generated changelog dramatically more scannable. If a commit is rejected:
|
|
47
|
+
your editor still has what you typed; fix the first line and commit again.
|
|
48
|
+
|
|
49
|
+
## release-please — fully automated, one human gate
|
|
50
|
+
|
|
51
|
+
release-please watches `main` and maintains a single standing
|
|
52
|
+
`chore(release): x.y.z` PR, its body the generated changelog for everything
|
|
53
|
+
merged since the last release, its diff bumping `pyproject.toml`'s
|
|
54
|
+
`[project].version` and updating `CHANGELOG.md`. **Merging that PR is the
|
|
55
|
+
release** — that merge is the human review gate; nothing else about
|
|
56
|
+
versioning needs to happen by hand. Never hand-edit `CHANGELOG.md` above
|
|
57
|
+
the `<!-- release-please starts and maintains ... -->` marker — it will be
|
|
58
|
+
overwritten on the next run.
|
|
59
|
+
|
|
60
|
+
## Publishing — PyPI Trusted Publishing (OIDC), no stored token
|
|
61
|
+
|
|
62
|
+
On the release-please PR's merge, a GitHub Release is created, which
|
|
63
|
+
triggers `publish-to-pypi.yml` (`on: release: published`). Two jobs: a
|
|
64
|
+
`build` job with no `id-token` permission (builds the sdist/wheel, runs
|
|
65
|
+
`twine check --strict`), then a `publish` job scoped to the GitHub
|
|
66
|
+
environment `pypi` (requires manual approval — a second human gate) with
|
|
67
|
+
`permissions: id-token: write` and nothing else, publishing via
|
|
68
|
+
`pypa/gh-action-pypi-publish`. There is no PyPI API token anywhere in this
|
|
69
|
+
repository's secrets — do not add one; it would disable the OIDC flow for
|
|
70
|
+
no benefit.
|
|
71
|
+
|
|
72
|
+
**The workflow filename `publish-to-pypi.yml` is load-bearing** — PyPI's
|
|
73
|
+
pending-publisher configuration matches on it exactly. Do not rename that
|
|
74
|
+
file without also updating the PyPI project's Trusted Publisher
|
|
75
|
+
configuration to match.
|
|
76
|
+
|
|
77
|
+
## Full reference
|
|
78
|
+
|
|
79
|
+
`docs/contributing/release-process.md` (the complete manual setup steps and
|
|
80
|
+
verification checklist), `docs/contributing/development.md#the-branch-model-gitflow`,
|
|
81
|
+
`CONTRIBUTING.md`.
|