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.
Files changed (137) hide show
  1. claudeloop-0.2.0/.claude/settings.json +37 -0
  2. claudeloop-0.2.0/.claude/skills/claudeloop-agent-sdk/SKILL.md +109 -0
  3. claudeloop-0.2.0/.claude/skills/claudeloop-architecture/SKILL.md +112 -0
  4. claudeloop-0.2.0/.claude/skills/claudeloop-docs/SKILL.md +71 -0
  5. claudeloop-0.2.0/.claude/skills/claudeloop-domain-model/SKILL.md +131 -0
  6. claudeloop-0.2.0/.claude/skills/claudeloop-quality-gates/SKILL.md +62 -0
  7. claudeloop-0.2.0/.claude/skills/claudeloop-releasing/SKILL.md +81 -0
  8. claudeloop-0.2.0/.claude/skills/claudeloop-rest-surface/SKILL.md +68 -0
  9. claudeloop-0.2.0/.claude/skills/claudeloop-testing/SKILL.md +95 -0
  10. claudeloop-0.2.0/.editorconfig +22 -0
  11. claudeloop-0.2.0/.gitattributes +13 -0
  12. claudeloop-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +76 -0
  13. claudeloop-0.2.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
  14. claudeloop-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +29 -0
  15. claudeloop-0.2.0/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  16. claudeloop-0.2.0/.github/dependabot.yml +31 -0
  17. claudeloop-0.2.0/.github/workflows/ci.yml +122 -0
  18. claudeloop-0.2.0/.github/workflows/docs.yml +46 -0
  19. claudeloop-0.2.0/.github/workflows/publish-to-pypi.yml +118 -0
  20. claudeloop-0.2.0/.github/workflows/release-please.yml +27 -0
  21. claudeloop-0.2.0/.gitignore +45 -0
  22. claudeloop-0.2.0/.pre-commit-config.yaml +72 -0
  23. claudeloop-0.2.0/.release-please-manifest.json +3 -0
  24. claudeloop-0.2.0/CHANGELOG.md +38 -0
  25. claudeloop-0.2.0/CLAUDE.md +56 -0
  26. claudeloop-0.2.0/CODE_OF_CONDUCT.md +68 -0
  27. claudeloop-0.2.0/CONTRIBUTING.md +212 -0
  28. claudeloop-0.2.0/LICENSE +21 -0
  29. claudeloop-0.2.0/PKG-INFO +165 -0
  30. claudeloop-0.2.0/README.md +110 -0
  31. claudeloop-0.2.0/SECURITY.md +78 -0
  32. claudeloop-0.2.0/docs/architecture/decisions/0001-onion-architecture-with-import-linter.md +41 -0
  33. claudeloop-0.2.0/docs/architecture/decisions/0002-agent-sdk-over-subprocess.md +51 -0
  34. claudeloop-0.2.0/docs/architecture/decisions/0003-credits-exhausted-distinct-from-window-exhausted.md +53 -0
  35. claudeloop-0.2.0/docs/architecture/decisions/0004-adaptive-waiting-with-probes-not-sleep.md +62 -0
  36. claudeloop-0.2.0/docs/architecture/decisions/0005-retry-watchdog-off-by-default.md +49 -0
  37. claudeloop-0.2.0/docs/architecture/decisions/0006-generated-rest-surface-not-hand-written.md +58 -0
  38. claudeloop-0.2.0/docs/architecture/decisions/0007-ask-user-question-denied-with-guidance.md +48 -0
  39. claudeloop-0.2.0/docs/architecture/domain-model.md +232 -0
  40. claudeloop-0.2.0/docs/architecture/overview.md +95 -0
  41. claudeloop-0.2.0/docs/architecture/ports-and-adapters.md +89 -0
  42. claudeloop-0.2.0/docs/architecture/run-loop-state-machine.md +114 -0
  43. claudeloop-0.2.0/docs/contributing/development.md +106 -0
  44. claudeloop-0.2.0/docs/contributing/documentation.md +69 -0
  45. claudeloop-0.2.0/docs/contributing/release-process.md +87 -0
  46. claudeloop-0.2.0/docs/contributing/testing.md +136 -0
  47. claudeloop-0.2.0/docs/getting-started/configuration.md +49 -0
  48. claudeloop-0.2.0/docs/getting-started/installation.md +59 -0
  49. claudeloop-0.2.0/docs/getting-started/quickstart.md +62 -0
  50. claudeloop-0.2.0/docs/guides/autonomous-runs.md +67 -0
  51. claudeloop-0.2.0/docs/guides/completion-detection.md +59 -0
  52. claudeloop-0.2.0/docs/guides/live-testing.md +70 -0
  53. claudeloop-0.2.0/docs/guides/never-blocking.md +40 -0
  54. claudeloop-0.2.0/docs/guides/rate-limits-and-credits.md +62 -0
  55. claudeloop-0.2.0/docs/guides/rest-api-surface.md +67 -0
  56. claudeloop-0.2.0/docs/index.md +42 -0
  57. claudeloop-0.2.0/docs/plans/architecture-and-roadmap.md +174 -0
  58. claudeloop-0.2.0/docs/plans/foss-and-documentation-plan.md +196 -0
  59. claudeloop-0.2.0/docs/reference/api.md +19 -0
  60. claudeloop-0.2.0/docs/reference/cli.md +37 -0
  61. claudeloop-0.2.0/legacy/claude_autoresume.py +674 -0
  62. claudeloop-0.2.0/mkdocs.yml +91 -0
  63. claudeloop-0.2.0/pyproject.toml +154 -0
  64. claudeloop-0.2.0/release-please-config.json +13 -0
  65. claudeloop-0.2.0/scripts/check_skill_frontmatter.py +98 -0
  66. claudeloop-0.2.0/src/claudeloop/__init__.py +4 -0
  67. claudeloop-0.2.0/src/claudeloop/application/__init__.py +0 -0
  68. claudeloop-0.2.0/src/claudeloop/application/dto.py +40 -0
  69. claudeloop-0.2.0/src/claudeloop/application/ports.py +69 -0
  70. claudeloop-0.2.0/src/claudeloop/application/runner.py +140 -0
  71. claudeloop-0.2.0/src/claudeloop/application/usecases/__init__.py +0 -0
  72. claudeloop-0.2.0/src/claudeloop/application/usecases/doctor.py +98 -0
  73. claudeloop-0.2.0/src/claudeloop/application/usecases/list_sessions.py +11 -0
  74. claudeloop-0.2.0/src/claudeloop/application/usecases/resume_session.py +34 -0
  75. claudeloop-0.2.0/src/claudeloop/application/usecases/run_plan.py +41 -0
  76. claudeloop-0.2.0/src/claudeloop/bootstrap.py +93 -0
  77. claudeloop-0.2.0/src/claudeloop/cli/__init__.py +0 -0
  78. claudeloop-0.2.0/src/claudeloop/cli/app.py +60 -0
  79. claudeloop-0.2.0/src/claudeloop/cli/asyncio.py +50 -0
  80. claudeloop-0.2.0/src/claudeloop/cli/commands/__init__.py +0 -0
  81. claudeloop-0.2.0/src/claudeloop/cli/commands/doctor.py +25 -0
  82. claudeloop-0.2.0/src/claudeloop/cli/commands/resume.py +86 -0
  83. claudeloop-0.2.0/src/claudeloop/cli/commands/run.py +76 -0
  84. claudeloop-0.2.0/src/claudeloop/cli/commands/sessions.py +25 -0
  85. claudeloop-0.2.0/src/claudeloop/cli/render.py +47 -0
  86. claudeloop-0.2.0/src/claudeloop/domain/__init__.py +0 -0
  87. claudeloop-0.2.0/src/claudeloop/domain/budget.py +60 -0
  88. claudeloop-0.2.0/src/claudeloop/domain/capacity.py +53 -0
  89. claudeloop-0.2.0/src/claudeloop/domain/classify.py +73 -0
  90. claudeloop-0.2.0/src/claudeloop/domain/completion.py +65 -0
  91. claudeloop-0.2.0/src/claudeloop/domain/errors.py +26 -0
  92. claudeloop-0.2.0/src/claudeloop/domain/loop.py +196 -0
  93. claudeloop-0.2.0/src/claudeloop/domain/plan.py +73 -0
  94. claudeloop-0.2.0/src/claudeloop/domain/session.py +61 -0
  95. claudeloop-0.2.0/src/claudeloop/domain/waiting.py +86 -0
  96. claudeloop-0.2.0/src/claudeloop/infrastructure/__init__.py +0 -0
  97. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/__init__.py +0 -0
  98. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/autonomy.py +83 -0
  99. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/catalog.py +53 -0
  100. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/gateway.py +102 -0
  101. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/options.py +69 -0
  102. claudeloop-0.2.0/src/claudeloop/infrastructure/agent/translate.py +141 -0
  103. claudeloop-0.2.0/src/claudeloop/infrastructure/api/__init__.py +0 -0
  104. claudeloop-0.2.0/src/claudeloop/infrastructure/audit.py +26 -0
  105. claudeloop-0.2.0/src/claudeloop/infrastructure/clock.py +27 -0
  106. claudeloop-0.2.0/src/claudeloop/infrastructure/config.py +93 -0
  107. claudeloop-0.2.0/src/claudeloop/infrastructure/doctor_env.py +86 -0
  108. claudeloop-0.2.0/src/claudeloop/infrastructure/lock.py +41 -0
  109. claudeloop-0.2.0/src/claudeloop/infrastructure/logging.py +79 -0
  110. claudeloop-0.2.0/src/claudeloop/infrastructure/notify.py +16 -0
  111. claudeloop-0.2.0/src/claudeloop/infrastructure/progress.py +18 -0
  112. claudeloop-0.2.0/src/claudeloop/infrastructure/state.py +27 -0
  113. claudeloop-0.2.0/src/claudeloop/py.typed +0 -0
  114. claudeloop-0.2.0/tests/__init__.py +0 -0
  115. claudeloop-0.2.0/tests/application/__init__.py +0 -0
  116. claudeloop-0.2.0/tests/application/fakes.py +127 -0
  117. claudeloop-0.2.0/tests/application/test_runner.py +176 -0
  118. claudeloop-0.2.0/tests/application/test_usecases.py +178 -0
  119. claudeloop-0.2.0/tests/cli/__init__.py +0 -0
  120. claudeloop-0.2.0/tests/cli/test_app.py +76 -0
  121. claudeloop-0.2.0/tests/domain/__init__.py +0 -0
  122. claudeloop-0.2.0/tests/domain/test_budget.py +78 -0
  123. claudeloop-0.2.0/tests/domain/test_capacity.py +23 -0
  124. claudeloop-0.2.0/tests/domain/test_classify.py +96 -0
  125. claudeloop-0.2.0/tests/domain/test_completion.py +37 -0
  126. claudeloop-0.2.0/tests/domain/test_loop.py +183 -0
  127. claudeloop-0.2.0/tests/domain/test_plan.py +66 -0
  128. claudeloop-0.2.0/tests/domain/test_session.py +48 -0
  129. claudeloop-0.2.0/tests/domain/test_waiting.py +193 -0
  130. claudeloop-0.2.0/tests/infrastructure/__init__.py +0 -0
  131. claudeloop-0.2.0/tests/infrastructure/test_catalog.py +77 -0
  132. claudeloop-0.2.0/tests/infrastructure/test_config.py +75 -0
  133. claudeloop-0.2.0/tests/infrastructure/test_translate.py +39 -0
  134. claudeloop-0.2.0/tests/live/__init__.py +0 -0
  135. claudeloop-0.2.0/tests/live/conftest.py +61 -0
  136. claudeloop-0.2.0/tests/live/test_free_tier.py +110 -0
  137. 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`.