Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e543e8fa3 | |||
| 1031fd0fb1 | |||
|
64b1f4c0b6
|
|||
| e757ca9db0 | |||
| b1f7b51a80 | |||
| b83f575cb5 | |||
|
38bcd41338
|
|||
| c11b05b773 | |||
| 3cfa24854a |
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.3
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ temperature: 0.2
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
color: success
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -91,7 +91,7 @@ Each cycle:
|
||||
|
||||
4. **Monitor workers.** Count active workers, check for stuck sessions.
|
||||
|
||||
5. **Update tracking.** Every 3 cycles, create a status tracking issue via `automation-tracking-manager` with prefix `AUTO-BUG-POOL`.
|
||||
5. **Update tracking (non-blocking).** Every 3 cycles, attempt to create a status tracking issue via `automation-tracking-manager` with prefix `AUTO-BUG-POOL`. This step is **best-effort** — if the call does not complete within a reasonable time or fails, skip it and continue to the next cycle. **Never block the main loop waiting for tracking.** Tracking is informational only; the supervisor's core function (module scanning and worker dispatch) must continue regardless.
|
||||
|
||||
## Finding Validation Gate
|
||||
|
||||
@@ -126,3 +126,4 @@ Supervisor: Bug Hunt Pool | Agent: bug-hunt-pool-supervisor
|
||||
|
||||
7. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
8. **Exhaustive pagination for all list results.** Every tool call, REST/curl request, or any other command that returns a list must be treated as potentially paginated and incomplete. Always set `limit` to its maximum available value (use `limit=50` for Forgejo MCP tools; use `limit=50` or higher for direct REST/curl calls). After each list response, check whether the number of returned items equals the page size — if so, there are likely more results; fetch the next page (`page=2`, `page=3`, …) and continue until receiving a partial page. Never assume the first response is the complete result. This rule applies to every list-returning call without exception. *Examples specific to this agent (not exhaustive):* `forgejo_list_repo_issues` (default 20 — use `limit=50` and paginate to check for all existing bug issues before filing duplicates); `forgejo_list_repo_pull_requests` (same — check all open PRs that may already address a discovered bug).
|
||||
9. **Tracking is non-blocking.** The `automation-tracking-manager` call in step 5 must never block the main loop. If it hangs or fails, skip it and proceed. Core functionality (module mapping, worker dispatch, monitoring) takes priority over status reporting.
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ hidden: true
|
||||
temperature: 0.3
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ temperature: 0.2
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
color: "#059669"
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -61,6 +65,7 @@ You manually fix a specific pull request. The user tells you which PR to fix. Yo
|
||||
4. Fix the code — address both CI failures and review feedback.
|
||||
5. Run quality gates locally (`nox -e lint`, `nox -e typecheck`, `nox -e unit_tests`, `nox -e integration_tests`).
|
||||
6. Commit and push using `git-commit-helper`.
|
||||
7. Clean up the isolated clone using `repo-isolator`.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ When choosing what to groom next, always re-check the current state of issues an
|
||||
|
||||
3. **Issues never groomed** — issues that have never been groomed.
|
||||
|
||||
4. **Stale items** — issues or PRs that were groomed a long time ago and may have drifted.
|
||||
4. **Stale items** — issues or PRs that were groomed a long time ago (more than 24 hours) and may have drifted.
|
||||
|
||||
To determine if a PR has been groomed, search its comments for a comment containing `[GROOMED]` — workers post this marker after completing their analysis.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ permission:
|
||||
"new-issue-creator": allow
|
||||
"forgejo-label-manager": allow
|
||||
"issue-state-updater": allow
|
||||
"forgejo_*": deny
|
||||
"forgejo_*": allow
|
||||
"forgejo_get_issue_by_index": allow
|
||||
"forgejo_list_issue_comments": allow
|
||||
"forgejo_issue_add_comment": allow
|
||||
@@ -77,7 +77,7 @@ Your prompt includes:
|
||||
2. Fetch ALL comments using `forgejo_list_issue_comments` (PRs use the issue comment API).
|
||||
3. Fetch ALL formal reviews using `forgejo_list_pull_reviews`.
|
||||
4. Fetch review comments using `forgejo_list_pull_review_comments`.
|
||||
5. Fetch the PR's labels using `forgejo_get_issue_labels`.
|
||||
5. Fetch the PR's labels using `forgejo-label-manager` subagent.
|
||||
6. Identify the linked issue (from closing keywords like `Closes #N` in the PR body).
|
||||
7. Fetch the linked issue's details and labels.
|
||||
|
||||
@@ -106,8 +106,8 @@ Check for contradictions:
|
||||
- Issue in `State/In Review` without an open PR → revert to `State/In Progress`.
|
||||
- Issue in `State/Paused` without `Blocked` label → add `Blocked` or unpause.
|
||||
|
||||
### 6. Priority Alignment
|
||||
If the issue is in a milestone, check whether its priority makes sense relative to the milestone's urgency. Flag obvious mismatches (e.g., `Priority/Low` in a milestone with an imminent deadline).
|
||||
### 6. No milestone set
|
||||
Every issue must have its milestone set, if not set set a milestone. Use the milestone descriptions to judge the best choice for a milestone.
|
||||
|
||||
### 7. Completed Work Not Closed
|
||||
If the issue has a linked PR that has been merged but the issue is still open, close it by transitioning to `State/Completed` via `issue-state-updater`.
|
||||
@@ -126,10 +126,14 @@ For pull requests ONLY: the PR's labels must be synced to match its linked issue
|
||||
- `MoSCoW/` label (if present)
|
||||
- Milestone assignment
|
||||
|
||||
Also verify the PR has:
|
||||
Also verify the PR has the following, and if it doesnt add it:
|
||||
- A closing keyword (`Closes #N` or `Fixes #N`) in its description
|
||||
- A dependency link (PR blocks the linked issue)
|
||||
|
||||
### 11: PR-Specific: Address any relevant remarks from reviews
|
||||
|
||||
Check formal reviews as well as informal comments left as reviews. Any concerns raised about the PR or its linked ticket, not related to the source code itself (for example labels, the PR description, milestone setting, etc) should be addressed.
|
||||
|
||||
## Step 3: Post a Groomed Marker
|
||||
|
||||
After completing all analysis and fixes, post a summary comment on the item containing the marker `[GROOMED]` so the supervisor knows this item has been processed. The comment should briefly list what was checked and what was fixed:
|
||||
|
||||
@@ -9,7 +9,11 @@ hidden: true
|
||||
temperature: 0.1
|
||||
# No model specified — tier is set by the supervisor via tier selectors
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
color: warning
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ temperature: 0.1
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
color: warning
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -9,7 +9,11 @@ temperature: 0.1
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
color: warning
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -46,7 +50,7 @@ permission:
|
||||
|
||||
# PR CI Test Fixer
|
||||
|
||||
You fix failing CI checks on a PR branch. You work in an isolated clone directory.
|
||||
You fix failing CI checks on a PR branch. You work in an isolated clone directory — `$WORK_DIR` is always a path inside `/tmp/` created by `repo-isolator`. All file edits and every git operation (`add`, `commit`, `push`, branch switching) must be performed inside `$WORK_DIR`, never against `/app`.
|
||||
|
||||
## What You Do
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ temperature: 0.1
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
color: "#059669"
|
||||
permission:
|
||||
edit: deny
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -53,7 +57,7 @@ permission:
|
||||
|
||||
# PR Merge Supervisor
|
||||
|
||||
You are a supervisor that monitors open PRs for merge readiness, verifies all criteria are met, rebases stale PR, resolves conflicts, and merges them. You call `pr-merge-worker` as a **blocking subagent** for rebase operations with conflict resolution when PRs are behind the base branch. Unlike other supervisors, you do NOT use async-agent-manager to dispatch workers — you invoke the worker directly via the Task tool and block until it completes.
|
||||
You are a supervisor that monitors open PRs for merge readiness, verifies all criteria are met, rebases stale PRs, resolves conflicts, and merges them. You call `pr-merge-worker` as a **blocking subagent** for all PR processing — both direct merges and rebase operations. Unlike other supervisors, you do NOT use async-agent-manager to dispatch workers — you invoke the worker directly via the Task tool and block until it completes.
|
||||
|
||||
## What You Receive
|
||||
|
||||
@@ -61,6 +65,17 @@ Your prompt from the product-builder includes:
|
||||
- Repository owner/name, Forgejo PAT, git identity
|
||||
- A customized briefing containing CONTRIBUTING.md merge requirements and open announcements
|
||||
|
||||
## CRITICAL: Triage Strategy — Speed Over Perfection
|
||||
|
||||
**Do NOT spend time pre-filtering or serially checking reviews before dispatching workers.** The correct approach is:
|
||||
|
||||
1. **Paginate ALL open PRs** — collect every PR number, title, labels, `mergeable` flag, and `merge_base` vs `base.sha`.
|
||||
2. **Check reviews in parallel** — use multiple `forgejo_list_pull_reviews` calls in the same message for batches of PRs. Do not check them one at a time sequentially.
|
||||
3. **Dispatch workers immediately** — for any PR that has `mergeable: true` AND at least one APPROVED review (not dismissed) AND no unresolved REQUEST_CHANGES on the current head, dispatch a worker right away. Do not wait to finish checking all other PRs first.
|
||||
4. **Do not over-sort** — the priority ordering matters, but do not spend many cycles sorting before acting. Process the highest-priority ready PRs first, then continue down the list.
|
||||
|
||||
**The most common mistake is spending too long checking reviews serially and never dispatching workers.** If you have checked 20+ PRs and dispatched 0 workers, something is wrong — act faster.
|
||||
|
||||
## Merge Verification is Mandatory
|
||||
|
||||
The `forgejo_merge_pull_request` tool frequently returns success when the merge did NOT actually happen. Forgejo silently rejects merges when a branch is behind. You must verify every merge:
|
||||
@@ -84,52 +99,89 @@ PROCEDURE MERGE_PR(pr_number):
|
||||
update_linked_issues_to_completed(pr_number)
|
||||
RETURN "merged"
|
||||
ELSE:
|
||||
-- Merge silently failed
|
||||
RETURN "failed"
|
||||
-- Merge silently failed — dispatch worker to handle
|
||||
CALL_BLOCKING_WORKER(pr_number)
|
||||
RETURN "worker_handled"
|
||||
```
|
||||
|
||||
## Seven Merge Criteria
|
||||
## Merge Criteria
|
||||
|
||||
Before merging any PR, the following must be true:
|
||||
Before merging any PR, ALL of the following must be true:
|
||||
|
||||
1. **Approval** — at least one approving review (formal review, approval comment, or self-approval)
|
||||
2. **CI passing** — all workflow runs on the latest commit are successful
|
||||
3. **No conflicts** — the PR has no merge conflicts
|
||||
4. **Not stale** — `merge_base` equals `base.sha` (branch is up to date)
|
||||
5. **No `needs feedback` label** — the PR is not waiting for human input
|
||||
6. **Not blocked** — no `Blocked` label
|
||||
1. **Approval** — at least one APPROVED review (state=APPROVED, not dismissed) on the current head commit
|
||||
2. **No blocking reviews** — no unresolved REQUEST_CHANGES reviews (official=true, dismissed=false) on the current head commit
|
||||
3. **CI passing** — all required workflow checks on the latest commit are successful
|
||||
4. **No conflicts** — `mergeable: true` (Forgejo reports no merge conflicts)
|
||||
5. **No `Needs Feedback` label** — the PR is not waiting for human input
|
||||
6. **No `Blocked` label** — the PR is not explicitly blocked
|
||||
|
||||
**Staleness (`merge_base != base.sha`) is NOT a hard blocker for dispatching a worker.** A worker can rebase a stale PR. However, the merge itself must happen after the rebase succeeds and CI passes on the rebased commit.
|
||||
|
||||
**Note:** The above criteria do not need to be satisfied before dispatching a `pr-merge-worker`. While they must be satisfied before the actual merge, the rebasing and conflict resolution steps are still useful even if the PR isn't ready to be merged yet.
|
||||
|
||||
## Workers
|
||||
|
||||
You call `pr-merge-worker` as a **blocking subagent** (via the Task tool) for rebase operations with conflict resolution when PRs are behind the base branch. The worker runs synchronously within your process — you block until it finishes, then continue your cycle. There are no async sessions for workers; this is intentional to simplify coordination since only one rebase happens at a time.
|
||||
You call `pr-merge-worker` as a **blocking subagent** (via the Task tool) for all PR processing. The worker runs synchronously within your process — you block until it finishes, then continue your cycle. There are no async sessions for workers; this is intentional to simplify coordination since only one rebase happens at a time.
|
||||
|
||||
Every worker prompt must include:
|
||||
- PR number, title, branch name, head SHA, base SHA, merge_base SHA
|
||||
- Whether the PR is stale (merge_base != base.sha)
|
||||
- Current review status (any approvals? any unresolved REQUEST_CHANGES?)
|
||||
- Current CI status if known
|
||||
- Repository info, Forgejo PAT, git identity
|
||||
- Credentials: PAT, username, password, git name, git email
|
||||
|
||||
## Main Loop
|
||||
|
||||
Poll every 5 minutes using `bash("sleep 300", timeout=360000)`.
|
||||
|
||||
Each cycle:
|
||||
1. List all open PRs. (the forgejo task paginates so be sure to go through every page)
|
||||
2. for each PR that is marked as mergable attempt to do a fast-forward or rebase merge, as always do the mandatory post-merge verification.
|
||||
2. For all other PR (including those you attempted to merge but were unsuccessful), filter such that only those that have passing CI quality gates/tests remain.
|
||||
4. For each PR that needs rebasing, regardless of if it has conflicts, (prioritized by: milestone order lowest first, then priority label, then MoSCoW label, then issue number), call `pr-merge-worker` as a **blocking subagent** via the Task tool. Pass the PR number, repository info, and credentials in the prompt. The worker will rebase onto the latest master, resolve conflicts, wait for CI, and attempt a fast-forward or rebase merge. You block until the worker finishes before processing the next PR.
|
||||
5. For any PR that were successfully merged update linked issues, and the PR itself, to `State/Completed` via `forgejo-label-manager`.
|
||||
|
||||
### Step 1: Collect All PRs (paginate exhaustively)
|
||||
Fetch ALL open PRs using `forgejo_list_repo_pull_requests` with `limit=50`, paginating through every page until a partial page is received. Record for each PR: number, title, labels, `mergeable`, `merge_base`, `base.sha`, head SHA.
|
||||
|
||||
### Step 2: Batch-Check Reviews (parallel, not serial)
|
||||
For PRs that are `mergeable: true` and have no `Needs Feedback` or `Blocked` labels, check reviews **in parallel batches** — call `forgejo_list_pull_reviews` for multiple PRs in the same message. Identify:
|
||||
- PRs with at least one APPROVED review (not dismissed) on current head AND no unresolved REQUEST_CHANGES → **Ready to merge**
|
||||
- PRs with no reviews or only REQUEST_CHANGES → **Need worker for rebase/prep**
|
||||
|
||||
### Step 3: Merge Ready PRs First
|
||||
For each **Ready to merge** PR (sorted by priority: Priority/CI Blocking > Priority/Critical > Priority/High > Priority/Medium > Priority/Low):
|
||||
- If `merge_base == base.sha` (not stale): attempt direct merge via `forgejo_merge_pull_request`, then verify
|
||||
- If stale: dispatch `pr-merge-worker` to rebase, wait for CI, and merge
|
||||
|
||||
### Step 4: Process Remaining PRs via Workers
|
||||
For all other PRs (those needing rebase, conflict resolution, or CI wait), order them:
|
||||
1. CI passes + has approval (but stale/conflicted)
|
||||
2. CI failing without conflicts + has approval
|
||||
3. CI failing with conflicts + has approval
|
||||
4. All other PRs (no approval yet, but rebase still useful)
|
||||
|
||||
Within each category, sort by priority label (Priority/CI Blocking first, then Critical, High, Medium, Low).
|
||||
|
||||
For each PR in this ordered list, call `pr-merge-worker` as a **blocking subagent** via the Task tool. Block until the worker finishes before processing the next PR.
|
||||
|
||||
### Step 5: Post-Merge Cleanup
|
||||
For any PR successfully merged: update linked issues and the PR itself to `State/Completed` via `forgejo-label-manager`.
|
||||
|
||||
## Tracking
|
||||
|
||||
- Prefix: `AUTO-MERGE`
|
||||
- Cycle interval: ~5 minutes
|
||||
- Create announcements for: merge verification failures, persistent stale PRs
|
||||
- Create announcements for: merge verification failures, persistent stale PRs, PRs stuck with REQUEST_CHANGES for >24h
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always verify merges.** Never trust the merge API response alone.
|
||||
1. **Always verify merges.** Never trust the merge API response alone. Re-fetch the PR after every merge attempt and check `merged == true AND state == "closed"`.
|
||||
2. **Never merge immediately after rebase.** Wait for CI to complete quality gates/tests first.
|
||||
3. **Pass credentials down.** Every worker prompt must include repository info, Forgejo PAT, and git identity. Workers never read environment variables.
|
||||
4. **Bot signature on all Forgejo content:**
|
||||
4. **Batch review checks.** Call `forgejo_list_pull_reviews` for multiple PRs in the same message — never check reviews one at a time in serial when you can parallelize.
|
||||
5. **Act fast on ready PRs.** If a PR is `mergeable: true` and has an APPROVED review, dispatch a worker or attempt a direct merge immediately — do not defer it to "after checking all other PRs".
|
||||
6. **Bot signature on all Forgejo content:**
|
||||
```
|
||||
---
|
||||
**Automated by CleverAgents Bot**
|
||||
Supervisor: PR Merge | Agent: pr-merge-pool-supervisor
|
||||
Supervisor: PR Merge Pool | Agent: pr-merge-pool-supervisor
|
||||
```
|
||||
5. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
6. **Exhaustive pagination for all list results.** Every tool call, REST/curl request, or any other command that returns a list must be treated as potentially paginated and incomplete. Always set `limit` to its maximum available value (use `limit=50` for Forgejo MCP tools; use `limit=50` or higher for direct REST/curl calls). After each list response, check whether the number of returned items equals the page size — if so, there are likely more results; fetch the next page (`page=2`, `page=3`, …) and continue until receiving a partial page. Never assume the first response is the complete result. This rule applies to every list-returning call without exception. *Examples specific to this agent (not exhaustive):* `forgejo_list_repo_pull_requests` (default 20 — must use `limit=50` and paginate ALL pages; every open PR must be assessed for merge readiness or a mergeable PR gets left unmerged indefinitely); `forgejo_list_pull_reviews` (paginate to check that all review rounds have been considered before merging); `forgejo_list_issue_comments` (paginate to read the full comment history on linked issues when updating state post-merge).
|
||||
7. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
8. **Exhaustive pagination for all list results.** Every tool call that returns a list must be treated as potentially paginated. Always set `limit=50` for Forgejo MCP tools. After each list response, check whether the number of returned items equals the page size — if so, fetch the next page. Never assume the first response is the complete result. This applies to: `forgejo_list_repo_pull_requests` (must paginate ALL pages — missing a page means a ready PR never gets merged), `forgejo_list_pull_reviews` (paginate to see all review rounds), `forgejo_list_issue_comments` (paginate when updating linked issues post-merge).
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.1
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
"external_directory":
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -46,7 +50,7 @@ You perform a single rebase operation on a PR branch, resolve any conflicts, and
|
||||
## Procedure
|
||||
|
||||
Your prompt tells you which PR to rebase. You must:
|
||||
1. Create an isolated clone using the `repo-isolator` subagent ensuring you pass it the branch used by the PR.
|
||||
1. Create an isolated clone using the `repo-isolator` subagent ensuring you pass it the branch used by the PR. Make sure all work is done within this clone's directory.
|
||||
2. Rebase it onto the base branch (usually `master`).
|
||||
3. Resolve any conflicts that arise by reviewing the recent git history and using that to fix the conflicts
|
||||
4. Force-push with lease using `git-commit-helper`
|
||||
@@ -59,6 +63,6 @@ Your prompt tells you which PR to rebase. You must:
|
||||
|
||||
1. **One task, then exit.** Do not loop. Do not sleep. Do not look for more work.
|
||||
2. **Force-push with lease only.** Never use `--force` without `--lease`.
|
||||
3. **Clean up your clone.** Delete the temporary directory before exiting.
|
||||
3. **Clean up your clone.** Delete the temporary clone directory before exiting.
|
||||
4. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
5. **Exhaustive pagination for all list results.** Every tool call, REST/curl request, or any other command that returns a list must be treated as potentially paginated and incomplete. Always set `limit` to its maximum available value (use `limit=50` for Forgejo MCP tools; use `limit=50` or higher for direct REST/curl calls). After each list response, check whether the number of returned items equals the page size — if so, there are likely more results; fetch the next page (`page=2`, `page=3`, …) and continue until receiving a partial page. Never assume the first response is the complete result. This rule applies to every list-returning call without exception. *Examples specific to this agent (not exhaustive):* `git log` listing commits during conflict resolution must be fully read; any future REST/curl calls returning JSON arrays must be paginated.
|
||||
|
||||
@@ -9,7 +9,11 @@ temperature: 0.2
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
color: primary
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -29,6 +33,8 @@ permission:
|
||||
task:
|
||||
"*": deny
|
||||
"forgejo-label-manager": allow
|
||||
"repo-isolator": allow
|
||||
"git-commit-helper": allow
|
||||
"forgejo_*": allow
|
||||
# CRITICAL: Never list repo-level labels — use org labels via forgejo-label-manager
|
||||
"forgejo_list_repo_labels": deny
|
||||
@@ -60,5 +66,6 @@ You set up project infrastructure from scratch. Your caller provides the product
|
||||
1. **Detect before creating.** Always check if something exists before creating it.
|
||||
2. **Never overwrite.** If a file or label already exists, skip it.
|
||||
3. **Credentials from prompt.** All Forgejo PAT, git identity, etc. come from the caller's prompt.
|
||||
4. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
5. **Exhaustive pagination for all list results.** Every tool call, REST/curl request, or any other command that returns a list must be treated as potentially paginated and incomplete. Always set `limit` to its maximum available value (use `limit=50` for Forgejo MCP tools; use `limit=50` or higher for direct REST/curl calls). After each list response, check whether the number of returned items equals the page size — if so, there are likely more results; fetch the next page (`page=2`, `page=3`, …) and continue until receiving a partial page. Never assume the first response is the complete result. This rule applies to every list-returning call without exception. *Examples specific to this agent (not exhaustive):* any `forgejo_list_*` calls used when checking for existing labels, milestones, or workflows must be paginated — missing one means re-creating something that already exists.
|
||||
4. **Work in an isolated clone.** Use `repo-isolator` to create an isolated clone of the target repository in `/tmp/`. All file creation and edits must be done in the clone — never directly in `/app`. Use `git-commit-helper` to commit and push changes. Clean up the isolated clone using `repo-isolator` when done.
|
||||
5. **Apply labels via `forgejo-label-manager`.** Never apply labels directly or using the Forgejo MCP/task. All label operations must go through `forgejo-label-manager`.
|
||||
6. **Exhaustive pagination for all list results.** Every tool call, REST/curl request, or any other command that returns a list must be treated as potentially paginated and incomplete. Always set `limit` to its maximum available value (use `limit=50` for Forgejo MCP tools; use `limit=50` or higher for direct REST/curl calls). After each list response, check whether the number of returned items equals the page size — if so, there are likely more results; fetch the next page (`page=2`, `page=3`, …) and continue until receiving a partial page. Never assume the first response is the complete result. This rule applies to every list-returning call without exception. *Examples specific to this agent (not exhaustive):* any `forgejo_list_*` calls used when checking for existing labels, milestones, or workflows must be paginated — missing one means re-creating something that already exists.
|
||||
|
||||
@@ -9,7 +9,9 @@ temperature: 0.1
|
||||
model: openai/gpt-5-codex
|
||||
color: "#6B7280"
|
||||
permission:
|
||||
edit: deny
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -37,6 +39,8 @@ permission:
|
||||
# CRITICAL: DO NOT use forgejo_add_issue_labels directly
|
||||
# Always delegate to forgejo-label-manager for label operations
|
||||
"forgejo_add_issue_labels": deny
|
||||
"external_directory":
|
||||
"/tmp/**": allow
|
||||
---
|
||||
|
||||
# Repository Isolator
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -11,7 +11,11 @@ temperature: 0.1
|
||||
model: openai/gpt-5-codex
|
||||
color: accent
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -9,7 +9,11 @@ temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
color: warning
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ hidden: true
|
||||
temperature: 0.0
|
||||
model: openai/gpt-5-codex
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ hidden: true
|
||||
temperature: 0.0
|
||||
model: anthropic/claude-haiku-4-5
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ hidden: true
|
||||
temperature: 0.0
|
||||
model: anthropic/claude-opus-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -7,7 +7,11 @@ hidden: true
|
||||
temperature: 0.0
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: deny
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.1
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
@@ -53,6 +57,7 @@ Your prompt provides the current milestone status data and the timeline file for
|
||||
3. Add new entries — never overwrite existing ones.
|
||||
4. Commit using `git-commit-helper` with a Conventional Changelog message.
|
||||
5. Push and exit. (No PR needed — timeline updates go directly to master.)
|
||||
6. Clean up the isolated clone using `repo-isolator`.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.1
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
@@ -8,7 +8,11 @@ hidden: true
|
||||
temperature: 0.2
|
||||
# NO MODEL SPECIFIED - inherits from caller (tier selector)
|
||||
permission:
|
||||
edit: allow
|
||||
edit:
|
||||
"*": deny
|
||||
"/tmp/**": allow
|
||||
external_directory:
|
||||
"/tmp/**": allow
|
||||
webfetch: allow
|
||||
bash:
|
||||
"*": deny
|
||||
|
||||
+19
-28
@@ -5,34 +5,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Documentation
|
||||
|
||||
- **CLI Reference: Decisions (v3.2.0)**: Added `docs/api/decisions.md` documenting
|
||||
`agents plan tree` and `agents plan explain` commands, decision recording during
|
||||
the Strategize phase, the full decision data model (id, plan_id, phase,
|
||||
context_snapshot, alternatives), and the `DecisionService` Python API.
|
||||
|
||||
- **CLI Reference: Invariants (v3.2.0)**: Added `docs/api/invariants.md` documenting
|
||||
`agents invariant add`, `agents invariant list`, and `agents invariant remove`
|
||||
commands, the invariant scope hierarchy (global/project/action/plan), merge
|
||||
precedence rules, enforcement records, violation model, and the `InvariantService`
|
||||
Python API.
|
||||
|
||||
- **CLI Reference: Checkpoints (v3.3.0)**: Added `docs/api/checkpoints.md` documenting
|
||||
`agents plan checkpoint list` and `agents plan rollback <checkpoint_id>` commands,
|
||||
automatic checkpoint triggers (`on_tool_write`, `on_tool_write_complete`,
|
||||
`on_subplan_spawn`, `on_error`), the checkpoint data model, retention policy, and
|
||||
the `CheckpointService` Python API.
|
||||
|
||||
- **CLI Reference: Plan Corrections & Subplans (v3.3.0)**: Added
|
||||
`docs/api/plan-corrections.md` documenting `agents plan correct --mode=revert`
|
||||
and `agents plan correct --mode=append` commands, the correction data model,
|
||||
`CorrectionAttemptRecord` lifecycle, and a subplan system overview covering
|
||||
execution modes, merge strategies, and the `SubplanService` Python API.
|
||||
|
||||
- **API Index Update**: Extended `docs/api/index.md` with a new "Plan Intelligence
|
||||
(v3.2.0 / v3.3.0)" section linking to all four new API reference pages.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Automation Profile Silent Fallback** (#8232): `_resolve_profile_for_plan` in
|
||||
@@ -45,6 +17,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
### Added
|
||||
|
||||
- **Developer Setup Guide** (#9123): Added comprehensive developer setup guide at `docs/development/setup.md` covering prerequisites, development workflow, testing, linting, and commit guidelines.
|
||||
- **TDD Issue-Capture Test Activation** (#7025): Replaced 234 bare `@skip` tags
|
||||
across 82 Behave feature files with the correct `@tdd_expected_fail @tdd_issue
|
||||
@tdd_issue_<N>` tag system. Scenarios whose referenced bugs were already fixed
|
||||
@@ -171,6 +144,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Plan Concurrency Race Condition** (#7989): Fixed critical race condition in `execute_plan()` and
|
||||
`apply_plan()` where concurrent CLI/worker sessions could simultaneously modify the same plan,
|
||||
corrupting plan state. `LockService` is now wired into the plan lifecycle with plan-level advisory
|
||||
locking. Each invocation generates a unique caller identity (UUID) to prevent re-entrant lock
|
||||
acquisition by concurrent sessions on the same plan. Concurrent attempts now raise `LockConflictError`
|
||||
instead of silently racing. Lock is acquired before phase transition and released in a `finally`
|
||||
block to ensure cleanup even on error.
|
||||
|
||||
- **Validation Gate Empty-Run Guard** (#7508): Fixed `ApplyValidationSummary.all_required_passed`
|
||||
returning `True` when zero validations were run, silently bypassing the apply gate. The property
|
||||
now returns `False` when the validation result set is empty (`is_empty` is `True`), ensuring
|
||||
@@ -191,6 +172,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
`sandbox_root=.cleveragents/sandbox/`, so LLM file output (`FILE:` blocks)
|
||||
is written to disk during the execute phase. (#4222)
|
||||
|
||||
- **SubplanExecutionService fail_fast cancellation** (#7582): Fixed a race condition where
|
||||
already-running parallel subplans were not cancelled when `fail_fast` fired. Previously,
|
||||
`Future.cancel()` only prevented queued futures from starting but had no effect on
|
||||
in-flight futures that completed after `stop_flag` was set — their `COMPLETE` results
|
||||
were incorrectly included in the merge output. The fix adds a post-completion guard that
|
||||
overrides any non-`ERRORED`/non-`CANCELLED` result to `CANCELLED` when `stop_flag` is
|
||||
active, and clears the associated output to prevent it from entering the merge. Also
|
||||
replaces the O(n) linear `status` lookup in the `as_completed()` loop with an O(1)
|
||||
`status_map` dict pre-computed before the executor block.
|
||||
|
||||
- **Robot Framework TDD Listener Guards** (#5436): Added three guard conditions to the
|
||||
`tdd_expected_fail_listener` `end_test()` function to prevent blindly inverting ALL test
|
||||
failures to passes, which was masking infrastructure errors and causing flaky CI behavior.
|
||||
|
||||
@@ -15,4 +15,5 @@ Below are some of the specific details of various contributions.
|
||||
* Jeffrey Phillips Freeman has acted as Lead Developer, daily contributor, and Project Owner.
|
||||
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
|
||||
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
# Checkpoints API Reference
|
||||
|
||||
Checkpoints allow operators to snapshot sandbox state during plan execution and
|
||||
restore it later. They are used for recovering from mistakes, reverting tool
|
||||
side-effects, and supporting the decision-correction revert flow.
|
||||
|
||||
> **Version:** Introduced in v3.3.0. Automatic checkpoint triggers added in v3.8.0.
|
||||
> **See also:** [`docs/reference/checkpointing.md`](../reference/checkpointing.md)
|
||||
|
||||
---
|
||||
|
||||
## Concept and Purpose
|
||||
|
||||
A **Checkpoint** is an immutable record of sandbox state at a specific point in
|
||||
time. The execution engine creates checkpoints automatically at key moments
|
||||
(before and after write-tool executions, before subplan spawning, and on
|
||||
errors). Operators can also roll back to any checkpoint via the CLI.
|
||||
|
||||
### Automatic Checkpoint Triggers (v3.8.0+)
|
||||
|
||||
| Trigger | When |
|
||||
|--------------------------|---------------------------------------------------|
|
||||
| `on_tool_write` | Before each write-tool execution |
|
||||
| `on_tool_write_complete` | After each write-tool execution |
|
||||
| `on_subplan_spawn` | Before first subplan execution attempt |
|
||||
| `on_error` | When the Execute phase fails |
|
||||
|
||||
All four triggers are enabled by default. See
|
||||
[Configuration](#configuration) to disable specific triggers.
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### `agents plan checkpoint list`
|
||||
|
||||
Lists all checkpoints available for a plan.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan checkpoint list <PLAN_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# List checkpoints for a plan (rich table)
|
||||
agents plan checkpoint list 01HXYZ1234567890ABCDEFGH
|
||||
|
||||
# JSON output for scripting
|
||||
agents plan checkpoint list 01HXYZ1234567890ABCDEFGH --format json
|
||||
```
|
||||
|
||||
#### Output (rich)
|
||||
|
||||
The rich renderer displays a table with columns:
|
||||
|
||||
| Column | Description |
|
||||
|-----------------|-------------------------------------------------------|
|
||||
| ID | Truncated checkpoint ULID |
|
||||
| Type | `pre_write`, `post_step`, or `manual` |
|
||||
| Decision | Aligned decision ULID (if set) |
|
||||
| Size | Checkpoint data size in bytes |
|
||||
| Created | Creation timestamp (UTC) |
|
||||
| Phase | Plan phase when checkpoint was created |
|
||||
|
||||
---
|
||||
|
||||
### `agents plan rollback`
|
||||
|
||||
Rolls back sandbox state to a previously captured checkpoint.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan rollback [--yes|-y] <PLAN_ID> <CHECKPOINT_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|------------------|-------------------------------------------------------|
|
||||
| `PLAN_ID` | ULID of the plan to roll back |
|
||||
| `CHECKPOINT_ID` | ULID of the checkpoint to restore |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `--yes`, `-y` | Skip the interactive confirmation prompt |
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Interactive rollback (prompts for confirmation)
|
||||
agents plan rollback 01HPLAN... 01HCHKPT...
|
||||
|
||||
# Skip confirmation (useful in scripts)
|
||||
agents plan rollback --yes 01HPLAN... 01HCHKPT...
|
||||
|
||||
# JSON output
|
||||
agents plan rollback --yes 01HPLAN... 01HCHKPT... --format json
|
||||
```
|
||||
|
||||
#### JSON Output Envelope
|
||||
|
||||
```json
|
||||
{
|
||||
"rollback_summary": {
|
||||
"plan_id": "01HPLAN...",
|
||||
"from_checkpoint_id": "01HCHKPT...",
|
||||
"restored_files_count": 3
|
||||
},
|
||||
"changes_reverted": [
|
||||
"path/to/file1.py",
|
||||
"path/to/file2.py",
|
||||
"path/to/file3.py"
|
||||
],
|
||||
"impact": {
|
||||
"files_affected": 3
|
||||
},
|
||||
"post_rollback_state": {
|
||||
"active_checkpoint": "01HCHKPT...",
|
||||
"plan_id": "01HPLAN..."
|
||||
},
|
||||
"timing": {
|
||||
"elapsed_seconds": 0.042
|
||||
},
|
||||
"messages": ["Rollback completed successfully."]
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Cases
|
||||
|
||||
| Error | Cause |
|
||||
|----------------------------------------------|----------------------------------------------------|
|
||||
| `Rollback blocked: plan is already applied` | Plan has reached the terminal `applied` state |
|
||||
| `Rollback blocked: sandbox is missing` | Sandbox was cleaned up before rollback |
|
||||
| `Not found: checkpoint` | Checkpoint ID does not exist |
|
||||
| `Validation Error` | Checkpoint does not belong to the specified plan |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Automatic checkpoint triggers are configured via `core.checkpoints.auto_create_on`:
|
||||
|
||||
```toml
|
||||
[core.checkpoints]
|
||||
auto_create_on = ["on_tool_write", "on_tool_write_complete", "on_subplan_spawn", "on_error"]
|
||||
```
|
||||
|
||||
To disable a specific trigger, remove it from the list:
|
||||
|
||||
```toml
|
||||
[core.checkpoints]
|
||||
# Disable post-write checkpoints to reduce storage usage
|
||||
auto_create_on = ["on_tool_write", "on_subplan_spawn", "on_error"]
|
||||
```
|
||||
|
||||
To disable all automatic checkpoints:
|
||||
|
||||
```toml
|
||||
[core.checkpoints]
|
||||
auto_create_on = []
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint Data Model
|
||||
|
||||
**Module:** `cleveragents.domain.models.core.checkpoint`
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------|-----------------|-----------------------------------------------------|
|
||||
| `checkpoint_id` | `str` (ULID) | Unique identifier |
|
||||
| `plan_id` | `str` (ULID) | The plan that owns this checkpoint |
|
||||
| `sandbox_ref` | `str` | Git commit hash or patch reference |
|
||||
| `decision_id` | `str \| None` | Optional decision ULID this checkpoint is aligned to|
|
||||
| `checkpoint_type` | `CheckpointType`| `pre_write`, `post_step`, or `manual` |
|
||||
| `resource_id` | `str \| None` | Optional resource ULID association |
|
||||
| `filesystem_path` | `str` | Relative path within the checkpoint directory |
|
||||
| `size_bytes` | `int` | Size of the checkpoint data in bytes |
|
||||
| `created_at` | `datetime` | UTC creation timestamp |
|
||||
| `metadata` | `CheckpointMetadata` | Audit metadata (reason, source tool, phase) |
|
||||
|
||||
### `CheckpointMetadata`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------------|--------------|-----------------------------------------------------|
|
||||
| `reason` | `str` | Why the checkpoint was created |
|
||||
| `source_tool` | `str \| None`| Which tool triggered the checkpoint |
|
||||
| `phase` | `str \| None`| Which plan phase was active |
|
||||
| `extra` | `dict` | Arbitrary key-value pairs for extension |
|
||||
|
||||
### Retention Policy
|
||||
|
||||
A `CheckpointRetentionPolicy` governs how many checkpoints a plan may keep:
|
||||
|
||||
| Field | Default | Range |
|
||||
|-------------------|---------|---------|
|
||||
| `max_checkpoints` | 50 | 1–100 |
|
||||
| `auto_prune` | `true` | |
|
||||
|
||||
When `auto_prune` is enabled and the checkpoint count exceeds `max_checkpoints`,
|
||||
the oldest **interior** checkpoints are automatically removed. The first
|
||||
(earliest) and most recent checkpoints are always preserved.
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### `CheckpointService`
|
||||
|
||||
**Module:** `cleveragents.application.services.checkpoint_service`
|
||||
|
||||
```python
|
||||
from cleveragents.application.services.checkpoint_service import CheckpointService
|
||||
from cleveragents.domain.models.core.checkpoint import CheckpointType
|
||||
|
||||
svc = CheckpointService(repository=repo, settings=settings)
|
||||
|
||||
# Create a manual checkpoint
|
||||
checkpoint = svc.create_checkpoint(
|
||||
plan_id="01HPLAN...",
|
||||
sandbox_ref="abc123def456",
|
||||
checkpoint_type=CheckpointType.MANUAL,
|
||||
decision_id=None,
|
||||
metadata={"reason": "pre-migration snapshot", "phase": "execute"},
|
||||
)
|
||||
|
||||
# List checkpoints for a plan
|
||||
checkpoints = svc.list_checkpoints(plan_id="01HPLAN...")
|
||||
|
||||
# Get a specific checkpoint
|
||||
cp = svc.get_checkpoint(checkpoint_id="01HCHKPT...")
|
||||
|
||||
# Roll back to a checkpoint
|
||||
result = svc.rollback_to_checkpoint(
|
||||
plan_id="01HPLAN...",
|
||||
checkpoint_id="01HCHKPT...",
|
||||
)
|
||||
# result.restored_files_count, result.changed_paths, result.from_checkpoint_id
|
||||
|
||||
# Delete a checkpoint
|
||||
svc.delete_checkpoint(checkpoint_id="01HCHKPT...")
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
The `CheckpointService` is registered in the DI container and can be obtained
|
||||
via:
|
||||
|
||||
```python
|
||||
from cleveragents.di import get_container
|
||||
|
||||
container = get_container()
|
||||
checkpoint_service = container.checkpoint_service()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Checkpointing and Rollback Reference](../reference/checkpointing.md) — full reference
|
||||
- [Decision Correction Reference](../reference/decision_correction.md) — correction-service integration
|
||||
- [Plan CLI Reference](../reference/plan_cli.md) — plan commands overview
|
||||
- [Subplan Service](../reference/subplan_service.md) — `on_subplan_spawn` trigger
|
||||
@@ -1,259 +0,0 @@
|
||||
# Decisions API Reference
|
||||
|
||||
The decisions subsystem records every choice point in a plan's lifecycle as a
|
||||
**Decision** node in a persistent tree. Decisions are created during the
|
||||
Strategize and Execute phases and form the basis for targeted correction and
|
||||
replay.
|
||||
|
||||
> **Version:** Introduced in v3.2.0.
|
||||
> **See also:** [`docs/reference/decision_model.md`](../reference/decision_model.md),
|
||||
> [`docs/reference/decision_service.md`](../reference/decision_service.md),
|
||||
> [`docs/reference/decision_correction.md`](../reference/decision_correction.md)
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### `agents plan tree`
|
||||
|
||||
Renders the full decision tree for a plan, showing every recorded choice point
|
||||
and how they relate to one another.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan tree <PLAN_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|----------------------|---------------------------------------------------------------|
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich` |
|
||||
| `--show-superseded` | Include superseded (corrected) decisions in the tree |
|
||||
| `--depth` | Maximum tree depth to display (`0` = unlimited, default: `0`) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Default rich tree view
|
||||
agents plan tree 01HXYZ1234567890ABCDEFGH
|
||||
|
||||
# Table format
|
||||
agents plan tree 01HXYZ1234567890ABCDEFGH --format table
|
||||
|
||||
# Include superseded decisions (useful when reviewing corrections)
|
||||
agents plan tree 01HXYZ1234567890ABCDEFGH --show-superseded
|
||||
|
||||
# Limit depth to 2 levels
|
||||
agents plan tree 01HXYZ1234567890ABCDEFGH --depth 2
|
||||
|
||||
# JSON output for scripting
|
||||
agents plan tree 01HXYZ1234567890ABCDEFGH --format json
|
||||
```
|
||||
|
||||
#### Output (rich)
|
||||
|
||||
The rich renderer displays a tree rooted at the `prompt_definition` decision.
|
||||
Each node shows:
|
||||
|
||||
- Decision ID (truncated ULID)
|
||||
- Decision type (e.g., `strategy_choice`, `implementation_choice`)
|
||||
- Chosen option (truncated)
|
||||
- Confidence score (if set)
|
||||
- Superseded indicator (`[superseded]`) when `--show-superseded` is active
|
||||
|
||||
---
|
||||
|
||||
### `agents plan explain`
|
||||
|
||||
Shows full details for a single decision, including the question that was
|
||||
answered, the chosen option, all alternatives considered, and optional context
|
||||
snapshot and actor reasoning.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan explain <DECISION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|----------------------|---------------------------------------------------------------|
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich` |
|
||||
| `--show-context` | Include context snapshot details (hash, resource refs) |
|
||||
| `--show-reasoning` | Include rationale and raw actor reasoning trace |
|
||||
|
||||
Alternatives considered are always included in the output.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Default rich output
|
||||
agents plan explain 01HXYZ1234567890ABCDEFGH
|
||||
|
||||
# JSON with full context snapshot
|
||||
agents plan explain 01HXYZ1234567890ABCDEFGH --format json --show-context
|
||||
|
||||
# Show actor reasoning trace
|
||||
agents plan explain 01HXYZ1234567890ABCDEFGH --show-reasoning
|
||||
|
||||
# YAML output with all details
|
||||
agents plan explain 01HXYZ1234567890ABCDEFGH --format yaml \
|
||||
--show-context --show-reasoning
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decision Recording During Strategize
|
||||
|
||||
During the **Strategize** phase, the strategy actor records decisions for every
|
||||
significant choice it makes. The decision tree is built incrementally:
|
||||
|
||||
1. A `prompt_definition` root decision is created when the plan is initialised.
|
||||
2. An `invariant_enforced` decision is recorded for each invariant applied by
|
||||
the Invariant Reconciliation Actor.
|
||||
3. One or more `strategy_choice` decisions capture the high-level approach.
|
||||
4. `subplan_spawn` or `subplan_parallel_spawn` decisions are recorded when the
|
||||
strategy decomposes work into child plans.
|
||||
|
||||
During the **Execute** phase, `implementation_choice`, `resource_selection`,
|
||||
`tool_invocation`, `error_recovery`, and `validation_response` decisions are
|
||||
added as children of the relevant strategy decisions.
|
||||
|
||||
---
|
||||
|
||||
## Decision Data Model
|
||||
|
||||
**Module:** `cleveragents.domain.models.core.decision`
|
||||
|
||||
### Identity Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------------------|-----------------|-----------------------------------------------------|
|
||||
| `decision_id` | `str` (ULID) | Auto-generated unique identifier |
|
||||
| `plan_id` | `str` (ULID) | Parent plan (required) |
|
||||
| `parent_decision_id` | `str \| None` | Parent decision; `None` for the root |
|
||||
| `sequence_number` | `int` | Monotonic order within the plan (0-indexed) |
|
||||
|
||||
### Classification
|
||||
|
||||
| Field | Type | Description |
|
||||
|-----------------|----------------|------------------------------------------------------|
|
||||
| `decision_type` | `DecisionType` | One of 11 enum values (see table below) |
|
||||
|
||||
#### `DecisionType` Values
|
||||
|
||||
| Value | Phase | Description |
|
||||
|--------------------------|------------|-------------------------------------------|
|
||||
| `prompt_definition` | Strategize | Root decision — the plan prompt |
|
||||
| `invariant_enforced` | Strategize | An invariant constraint was applied |
|
||||
| `strategy_choice` | Strategize | High-level approach chosen |
|
||||
| `implementation_choice` | Execute | How to implement a specific task |
|
||||
| `resource_selection` | Execute | Which resources to read / modify |
|
||||
| `subplan_spawn` | Strategize | Decision to create a child plan |
|
||||
| `subplan_parallel_spawn` | Strategize | Spawn a group of child plans in parallel |
|
||||
| `tool_invocation` | Execute | Which skill / tool to use |
|
||||
| `error_recovery` | Execute | How to handle a failure |
|
||||
| `validation_response` | Execute | Response to a validation failure |
|
||||
| `user_intervention` | Any | User-provided guidance / correction |
|
||||
|
||||
### Content Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------------|-----------------|-----------------------------------------------------|
|
||||
| `question` | `str` | What question was being answered (required) |
|
||||
| `chosen_option` | `str` | The option that was selected (required) |
|
||||
| `alternatives_considered`| `list[str]` | Other options evaluated |
|
||||
| `confidence_score` | `float \| None` | 0.0–1.0 confidence, or `None` |
|
||||
|
||||
### Context Snapshot
|
||||
|
||||
Every decision captures a `ContextSnapshot` for replay:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------------------|--------------|--------------------------------------------------|
|
||||
| `hot_context_hash` | `str` | SHA-256 hash of the context window |
|
||||
| `hot_context_ref` | `str` | Storage pointer to the full serialised context |
|
||||
| `relevant_resources`| `list[ResourceRef]` | Resources referenced at decision time |
|
||||
| `actor_state_ref` | `str \| None`| LangGraph actor checkpoint reference |
|
||||
|
||||
### Rationale Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------------|--------------|-----------------------------------------------------|
|
||||
| `rationale` | `str` | Human-readable explanation |
|
||||
| `actor_reasoning`| `str \| None`| Raw LLM reasoning trace, if available |
|
||||
|
||||
### Correction Metadata
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------------------|-----------------|-------------------------------------------------|
|
||||
| `is_correction` | `bool` | `True` if this decision corrects another |
|
||||
| `corrects_decision_id` | `str \| None` | ULID of the original decision |
|
||||
| `correction_reason` | `str \| None` | Why the correction was made |
|
||||
| `superseded_by` | `str \| None` | ULID of the decision that replaced this one |
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### `DecisionService`
|
||||
|
||||
**Module:** `cleveragents.application.services.decision_service`
|
||||
|
||||
```python
|
||||
from cleveragents.application.services.decision_service import DecisionService
|
||||
from cleveragents.domain.models.core.decision import DecisionType
|
||||
|
||||
svc = DecisionService(settings=settings, unit_of_work=uow)
|
||||
|
||||
# Record a decision during Strategize
|
||||
decision = svc.record_decision(
|
||||
plan_id="01HV...",
|
||||
decision_type=DecisionType.STRATEGY_CHOICE,
|
||||
question="Which approach should we use?",
|
||||
chosen_option="Build a REST API with FastAPI",
|
||||
alternatives_considered=["GraphQL", "gRPC"],
|
||||
confidence_score=0.85,
|
||||
rationale="REST is simpler and better supported by existing tooling.",
|
||||
)
|
||||
|
||||
# Retrieve the full decision tree (BFS, level by level)
|
||||
tree = svc.get_tree(plan_id="01HV...")
|
||||
|
||||
# Get a single decision
|
||||
d = svc.get_decision(decision_id="01HXYZ...")
|
||||
|
||||
# List all decisions for a plan (ordered by sequence number)
|
||||
decisions = svc.list_decisions(plan_id="01HV...")
|
||||
|
||||
# List decisions of a specific type
|
||||
strategy_decisions = svc.list_by_type(
|
||||
plan_id="01HV...",
|
||||
decision_type=DecisionType.STRATEGY_CHOICE,
|
||||
)
|
||||
|
||||
# Walk from a decision up to the root
|
||||
path = svc.get_path_to_root(decision_id="01HXYZ...")
|
||||
```
|
||||
|
||||
### Exceptions
|
||||
|
||||
| Exception | When raised |
|
||||
|-------------------------|--------------------------------------------------|
|
||||
| `DecisionNotFoundError` | Decision ID does not exist |
|
||||
| `DuplicateDecisionError`| Decision ID already exists |
|
||||
| `SequenceConflictError` | Sequence number already used for this plan |
|
||||
| `ValidationError` | Required fields missing or empty |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Decision Domain Model](../reference/decision_model.md) — full field reference
|
||||
- [Decision Service Reference](../reference/decision_service.md) — service API details
|
||||
- [Decision Correction Reference](../reference/decision_correction.md) — correction modes
|
||||
- [Plan CLI Reference](../reference/plan_cli.md) — `agents plan tree` / `agents plan explain`
|
||||
- [Subplan Service](../reference/subplan_service.md) — spawn decisions
|
||||
@@ -19,19 +19,6 @@ classes, functions, and exceptions with signatures and usage examples.
|
||||
| [`cleveragents.providers`](providers.md) | AI provider registry — discovery, selection, LLM factory, and capability metadata |
|
||||
| [`cleveragents.tui`](tui.md) | Interactive Terminal UI — app, persona system, input routing, slash commands, session export/import |
|
||||
|
||||
## Plan Intelligence (v3.2.0 / v3.3.0)
|
||||
|
||||
The following pages document the CLI commands and Python APIs introduced in
|
||||
v3.2.0 (Decisions & Invariants) and v3.3.0 (Corrections, Subplans &
|
||||
Checkpoints).
|
||||
|
||||
| Page | Description |
|
||||
|------|-------------|
|
||||
| [Decisions](decisions.md) | `agents plan tree`, `agents plan explain`, decision recording during Strategize, decision data model and Python API |
|
||||
| [Invariants](invariants.md) | `agents invariant add/list/remove`, invariant scopes, enforcement, data model and Python API |
|
||||
| [Checkpoints](checkpoints.md) | `agents plan checkpoint list`, `agents plan rollback`, automatic triggers, data model and Python API |
|
||||
| [Plan Corrections](plan-corrections.md) | `agents plan correct --mode=revert\|append`, correction data model, subplan system overview and Python API |
|
||||
|
||||
> **Note:** Internal modules (prefixed with `_`) and implementation details
|
||||
> not listed in a module's `__all__` are considered private and may change
|
||||
> without notice.
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
# Invariants API Reference
|
||||
|
||||
Invariants are natural-language constraints on plan execution. They flow into
|
||||
the decision tree during the Strategize phase and are reconciled by the
|
||||
Invariant Reconciliation Actor before any strategy work begins.
|
||||
|
||||
> **Version:** Introduced in v3.2.0.
|
||||
> **See also:** [`docs/reference/invariants.md`](../reference/invariants.md)
|
||||
|
||||
---
|
||||
|
||||
## Concept and Purpose
|
||||
|
||||
An **invariant** is a constraint that must hold throughout plan execution.
|
||||
Examples:
|
||||
|
||||
- `"Never delete production data"`
|
||||
- `"All API changes need tests"`
|
||||
- `"Use Python 3.13 only"`
|
||||
|
||||
Invariants are evaluated at the start of the Strategize phase. If any
|
||||
invariant is violated during execution, an `INVARIANT_VIOLATED` event is
|
||||
emitted and the phase transition is blocked.
|
||||
|
||||
### Scope Hierarchy
|
||||
|
||||
| Scope | Description |
|
||||
|-----------|----------------------------------------------------------|
|
||||
| `GLOBAL` | Applies to every plan in the system |
|
||||
| `PROJECT` | Applies to plans targeting a specific project |
|
||||
| `ACTION` | Defined in an action template; promoted on `plan use` |
|
||||
| `PLAN` | Attached directly to a specific plan |
|
||||
|
||||
### Merge Precedence
|
||||
|
||||
When computing the effective invariant set for a plan:
|
||||
|
||||
```
|
||||
plan > project > global
|
||||
```
|
||||
|
||||
Action-level invariants are promoted to plan scope when `plan use` is called.
|
||||
Invariants are de-duplicated by text (case-insensitive); when the same
|
||||
constraint appears at multiple scopes, only the highest-precedence copy is kept.
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### `agents invariant add`
|
||||
|
||||
Creates a new invariant at the specified scope.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents invariant add [SCOPE_FLAG] <TEXT> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Scope Flags (mutually exclusive)
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `--global` | Create a global invariant (applies to all plans) |
|
||||
| `--project NAME` | Create a project-scoped invariant |
|
||||
| `--plan PLAN_ID` | Create a plan-scoped invariant |
|
||||
| `--action NAME` | Create an action-scoped invariant |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Global invariant (applies to every plan)
|
||||
agents invariant add --global "Never delete production data"
|
||||
|
||||
# Project-scoped invariant
|
||||
agents invariant add --project myapp "All API changes need tests"
|
||||
|
||||
# Plan-specific invariant
|
||||
agents invariant add --plan 01HXYZ... "Use Python 3.13 only"
|
||||
|
||||
# Action-scoped invariant
|
||||
agents invariant add --action local/code-coverage "Minimum 80% coverage"
|
||||
|
||||
# JSON output
|
||||
agents invariant add --global "No hardcoded secrets" --format json
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
On success, the command prints the created invariant's ID and text:
|
||||
|
||||
```
|
||||
✓ OK Invariant 01HABC... created
|
||||
Scope: global
|
||||
Text: Never delete production data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `agents invariant list`
|
||||
|
||||
Displays invariants, optionally filtered by scope or matched by regex.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents invariant list [REGEX] [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------------|---------------------------------------------------------------|
|
||||
| `--global` | Show only global invariants |
|
||||
| `--project NAME` | Show only invariants for the specified project |
|
||||
| `--plan PLAN_ID` | Show only invariants for the specified plan |
|
||||
| `--action NAME` | Show only invariants for the specified action |
|
||||
| `--effective` | Show the merged effective set (requires `--project`) |
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich` |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# List all active invariants
|
||||
agents invariant list
|
||||
|
||||
# Filter by scope
|
||||
agents invariant list --global
|
||||
agents invariant list --project myapp
|
||||
|
||||
# Show merged effective set for a project
|
||||
agents invariant list --effective --project myapp
|
||||
|
||||
# Filter by regex
|
||||
agents invariant list "data.*safe"
|
||||
|
||||
# JSON output
|
||||
agents invariant list --format json
|
||||
```
|
||||
|
||||
#### Output (rich)
|
||||
|
||||
The rich renderer displays a table with columns:
|
||||
|
||||
| Column | Description |
|
||||
|-------------|---------------------------------------------------|
|
||||
| ID | Truncated invariant ULID |
|
||||
| Scope | `global`, `project`, `action`, or `plan` |
|
||||
| Scope Target| Project/plan/action name (if applicable) |
|
||||
| Text | Invariant constraint text |
|
||||
| Created | Creation timestamp |
|
||||
|
||||
---
|
||||
|
||||
### `agents invariant remove`
|
||||
|
||||
Removes an invariant by ID.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents invariant remove [--yes|-y] <INVARIANT_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------|-------------------------------------------------------|
|
||||
| `--yes`, `-y` | Skip the interactive confirmation prompt |
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# With confirmation prompt
|
||||
agents invariant remove 01HXYZ...
|
||||
|
||||
# Skip confirmation (useful in scripts)
|
||||
agents invariant remove --yes 01HXYZ...
|
||||
```
|
||||
|
||||
#### Notes
|
||||
|
||||
- Removing a `GLOBAL` or `PROJECT` invariant does not retroactively affect
|
||||
plans that have already been strategized — the invariant was already
|
||||
evaluated and recorded in the decision tree.
|
||||
- Removing a `PLAN`-scoped invariant from a plan that is still in the
|
||||
Strategize phase will affect the next reconciliation run.
|
||||
|
||||
---
|
||||
|
||||
## Invariant Data Model
|
||||
|
||||
**Module:** `cleveragents.domain.models.core.invariant`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---------------|---------------|-----------------------------------------------------|
|
||||
| `id` | `str` (ULID) | Auto-generated unique identifier |
|
||||
| `name` | `str` | Short human-readable label (optional) |
|
||||
| `description` | `str` | The constraint text (required, non-empty) |
|
||||
| `scope` | `InvariantScope` | `GLOBAL`, `PROJECT`, `ACTION`, or `PLAN` |
|
||||
| `scope_target`| `str \| None` | Project name, plan ID, or action name (scope-dependent) |
|
||||
| `created_at` | `datetime` | UTC creation timestamp |
|
||||
| `is_active` | `bool` | Whether the invariant is currently enforced |
|
||||
|
||||
### `InvariantScope` Enum
|
||||
|
||||
```python
|
||||
from cleveragents.domain.models.core.invariant import InvariantScope
|
||||
|
||||
InvariantScope.GLOBAL # "global"
|
||||
InvariantScope.PROJECT # "project"
|
||||
InvariantScope.ACTION # "action"
|
||||
InvariantScope.PLAN # "plan"
|
||||
```
|
||||
|
||||
### Enforcement Record
|
||||
|
||||
When the Invariant Reconciliation Actor evaluates an invariant, it creates an
|
||||
`InvariantEnforcementRecord`:
|
||||
|
||||
| Field | Description |
|
||||
|------------------|--------------------------------------------------|
|
||||
| `invariant_id` | ULID of the invariant |
|
||||
| `enforced` | Whether the invariant was successfully enforced |
|
||||
| `actor_response` | Response text from the reconciliation actor |
|
||||
| `decision_id` | ULID of the associated `invariant_enforced` decision |
|
||||
|
||||
### Violation Model
|
||||
|
||||
When an invariant is violated during execution, an `InvariantViolation` is
|
||||
created:
|
||||
|
||||
| Field | Description |
|
||||
|-----------------|---------------------------------------------------|
|
||||
| `invariant_id` | ULID of the violated invariant |
|
||||
| `violated_text` | The invariant text that was violated |
|
||||
| `severity` | `error`, `warning`, or `info` |
|
||||
| `details` | Additional violation context |
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### `InvariantService`
|
||||
|
||||
**Module:** `cleveragents.application.services.invariant_service`
|
||||
|
||||
```python
|
||||
from cleveragents.application.services.invariant_service import InvariantService
|
||||
from cleveragents.domain.models.core.invariant import InvariantScope
|
||||
|
||||
svc = InvariantService(unit_of_work=uow)
|
||||
|
||||
# Create a global invariant
|
||||
inv = svc.create_invariant(
|
||||
description="Never delete production data",
|
||||
scope=InvariantScope.GLOBAL,
|
||||
)
|
||||
|
||||
# Create a project-scoped invariant
|
||||
inv = svc.create_invariant(
|
||||
description="All API changes need tests",
|
||||
scope=InvariantScope.PROJECT,
|
||||
scope_target="myapp",
|
||||
)
|
||||
|
||||
# List all invariants
|
||||
all_invariants = svc.list_invariants()
|
||||
|
||||
# List by scope
|
||||
global_invariants = svc.list_invariants(scope=InvariantScope.GLOBAL)
|
||||
project_invariants = svc.list_invariants(
|
||||
scope=InvariantScope.PROJECT,
|
||||
scope_target="myapp",
|
||||
)
|
||||
|
||||
# Get the merged effective set for a project
|
||||
effective = svc.get_effective_invariants(project_name="myapp")
|
||||
|
||||
# Remove an invariant
|
||||
svc.remove_invariant(invariant_id="01HXYZ...")
|
||||
```
|
||||
|
||||
### `InvariantReconciliationActor`
|
||||
|
||||
The reconciliation actor runs automatically at every plan phase transition.
|
||||
It is wired into the plan lifecycle and does not need to be invoked manually.
|
||||
|
||||
```python
|
||||
# The actor is resolved from the DI container:
|
||||
from cleveragents.di import get_container
|
||||
|
||||
container = get_container()
|
||||
actor = container.invariant_reconciliation_actor()
|
||||
|
||||
# Manually trigger reconciliation (advanced use):
|
||||
result = await actor.reconcile(plan=plan, context=ctx)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Invariants Reference](../reference/invariants.md) — scope hierarchy, merge precedence, enforcement
|
||||
- [Decision Model](../reference/decision_model.md) — `invariant_enforced` decision type
|
||||
- [Plan CLI Reference](../reference/plan_cli.md) — `--invariant` flag on `agents plan use`
|
||||
- [Automation Profiles](../reference/automation_profiles.md) — profile-level invariant interaction
|
||||
@@ -1,354 +0,0 @@
|
||||
# Plan Corrections API Reference
|
||||
|
||||
The plan correction subsystem allows operators to modify a plan's decision tree
|
||||
after execution by either **reverting** a subtree of decisions or **appending**
|
||||
new decisions as a child plan. This enables targeted re-execution without
|
||||
discarding the entire plan.
|
||||
|
||||
> **Version:** Introduced in v3.3.0. Correction attempts tracking added in v3.8.0.
|
||||
> **See also:** [`docs/reference/decision_correction.md`](../reference/decision_correction.md),
|
||||
> [`docs/reference/subplans.md`](../reference/subplans.md),
|
||||
> [`docs/reference/subplan_service.md`](../reference/subplan_service.md)
|
||||
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
### Correction Modes
|
||||
|
||||
#### Revert Mode
|
||||
|
||||
Invalidates the targeted decision and every descendant reachable via BFS
|
||||
traversal. Associated artifacts are archived and affected child plans are
|
||||
rolled back.
|
||||
|
||||
```text
|
||||
┌─── D1 (target) ◄── revert starts here
|
||||
│ │
|
||||
│ ┌──┴──┐
|
||||
│ D2 D3 ← all invalidated
|
||||
│ │
|
||||
│ D4 ← also invalidated
|
||||
```
|
||||
|
||||
Use revert when the original decision was wrong and you want to re-execute
|
||||
from that point with different guidance.
|
||||
|
||||
#### Append Mode
|
||||
|
||||
Preserves the original decision and spawns a **new child plan** rooted at the
|
||||
target node. The child plan carries the operator's guidance and produces
|
||||
additional decisions without disturbing the existing tree.
|
||||
|
||||
```text
|
||||
D1 (target)
|
||||
│
|
||||
┌──┴──────────┐
|
||||
D2 (original) CP-new ← child plan appended
|
||||
```
|
||||
|
||||
Use append when the original decision was correct but you want to add
|
||||
supplementary work without recomputing.
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
### `agents plan correct --mode=revert`
|
||||
|
||||
Re-executes from a targeted decision point by invalidating the decision subtree
|
||||
and re-running from that node with new guidance.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan correct --mode=revert <PLAN_ID> <DECISION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|---------------|-------------------------------------------------------|
|
||||
| `PLAN_ID` | ULID of the plan to correct |
|
||||
| `DECISION_ID` | ULID of the decision to revert from |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-----------------------|-------------------------------------------------------|
|
||||
| `--mode` | Correction mode: `revert` or `append` (required) |
|
||||
| `--guidance TEXT` | Operator guidance for the re-execution (required) |
|
||||
| `--dry-run` | Preview impact without making changes |
|
||||
| `--yes`, `-y` | Skip the interactive confirmation prompt |
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Revert a decision and re-execute with guidance
|
||||
agents plan correct --mode=revert \
|
||||
--guidance "Use a safer migration approach with explicit rollback" \
|
||||
01HPLAN... 01HDECISION...
|
||||
|
||||
# Preview impact before committing
|
||||
agents plan correct --mode=revert --dry-run \
|
||||
--guidance "Use a safer migration approach" \
|
||||
01HPLAN... 01HDECISION...
|
||||
|
||||
# Skip confirmation prompt
|
||||
agents plan correct --mode=revert --yes \
|
||||
--guidance "Use a safer migration approach" \
|
||||
01HPLAN... 01HDECISION...
|
||||
```
|
||||
|
||||
#### Dry-Run Output
|
||||
|
||||
A dry-run report includes:
|
||||
|
||||
- **impact** — Full `CorrectionImpact` with affected decisions, files, child
|
||||
plans, estimated cost, and risk level
|
||||
- **decisions_to_invalidate** — Decision IDs that *would* be marked invalid
|
||||
- **child_plans_to_rollback** — Child plans that *would* be rolled back
|
||||
- **estimated_recompute_time_seconds** — Wall-clock estimate
|
||||
- **warnings** — Human-readable cautions (e.g., high risk)
|
||||
|
||||
#### Risk Classification
|
||||
|
||||
| Affected Decisions | Risk Level |
|
||||
|--------------------|------------|
|
||||
| ≤ 3 | low |
|
||||
| 4 – 10 | medium |
|
||||
| > 10 | high |
|
||||
|
||||
---
|
||||
|
||||
### `agents plan correct --mode=append`
|
||||
|
||||
Adds guidance to a plan by spawning a new child plan rooted at the target
|
||||
decision, without recomputing the existing decision tree.
|
||||
|
||||
#### Synopsis
|
||||
|
||||
```bash
|
||||
agents plan correct --mode=append <PLAN_ID> <DECISION_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|---------------|-------------------------------------------------------|
|
||||
| `PLAN_ID` | ULID of the plan to correct |
|
||||
| `DECISION_ID` | ULID of the decision to append from |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description |
|
||||
|-----------------------|-------------------------------------------------------|
|
||||
| `--mode` | Correction mode: `revert` or `append` (required) |
|
||||
| `--guidance TEXT` | Operator guidance for the child plan (required) |
|
||||
| `--dry-run` | Preview impact without making changes |
|
||||
| `--yes`, `-y` | Skip the interactive confirmation prompt |
|
||||
| `--format`, `-f` | Output format: `json`, `yaml`, `plain`, `table`, `rich`|
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Append a child plan with additional guidance
|
||||
agents plan correct --mode=append \
|
||||
--guidance "Also add integration tests for the new endpoint" \
|
||||
01HPLAN... 01HDECISION...
|
||||
|
||||
# Preview what would be created
|
||||
agents plan correct --mode=append --dry-run \
|
||||
--guidance "Also add integration tests" \
|
||||
01HPLAN... 01HDECISION...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Correction Status Lifecycle
|
||||
|
||||
```text
|
||||
PENDING → ANALYZING → EXECUTING → APPLIED
|
||||
→ FAILED
|
||||
PENDING → CANCELLED
|
||||
ANALYZING → CANCELLED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Correction Data Model
|
||||
|
||||
**Module:** `cleveragents.domain.models.core.correction`
|
||||
|
||||
| Field | Type | Description |
|
||||
|------------------------|-------------------|-------------------------------------------------|
|
||||
| `correction_id` | `str` (ULID) | Unique identifier |
|
||||
| `plan_id` | `str` (ULID) | Plan being corrected |
|
||||
| `target_decision_id` | `str` (ULID) | Decision being corrected |
|
||||
| `mode` | `CorrectionMode` | `revert` or `append` |
|
||||
| `guidance` | `str` | Operator guidance text (1–10,000 chars) |
|
||||
| `status` | `CorrectionStatus`| Current status (see lifecycle above) |
|
||||
| `created_at` | `datetime` | UTC creation timestamp |
|
||||
| `completed_at` | `datetime \| None`| UTC completion timestamp (terminal states only) |
|
||||
|
||||
### `CorrectionAttemptRecord` (v3.8.0+)
|
||||
|
||||
Each execution of a correction is tracked as a `CorrectionAttemptRecord`.
|
||||
Multiple attempts may exist for a single correction (e.g., if a first attempt
|
||||
fails and the operator retries).
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------------------|-------------------------|-------------------------------------------------|
|
||||
| `id` | `str` (ULID) | Unique identifier for this attempt |
|
||||
| `correction_id` | `str` (ULID) | Parent correction ULID |
|
||||
| `plan_id` | `str` (ULID) | Plan this correction targets |
|
||||
| `original_decision_id` | `str` (ULID) | Decision being corrected |
|
||||
| `new_decision_id` | `str \| None` | Replacement decision (set on success) |
|
||||
| `mode` | `CorrectionMode` | `revert` or `append` |
|
||||
| `state` | `CorrectionAttemptState`| `pending`, `executing`, `complete`, or `failed` |
|
||||
| `guidance` | `str` | Operator guidance text |
|
||||
| `created_at` | `datetime` | UTC creation timestamp |
|
||||
| `completed_at` | `datetime \| None` | UTC completion timestamp |
|
||||
| `archived_artifacts_path`| `str \| None` | Path to archived artifacts (revert mode) |
|
||||
|
||||
---
|
||||
|
||||
## Subplan System Overview
|
||||
|
||||
Subplans allow a parent plan to decompose work into coordinated child plans.
|
||||
Each child plan runs in its own sandbox, and results are merged back into the
|
||||
parent plan using a configurable merge strategy.
|
||||
|
||||
### How Subplans Are Spawned
|
||||
|
||||
During the **Strategize** phase, the strategy actor may decide to decompose
|
||||
work by recording `subplan_spawn` or `subplan_parallel_spawn` decisions. The
|
||||
`SubplanService` then:
|
||||
|
||||
1. Extracts spawn decisions from the `DecisionService`
|
||||
2. Builds `SpawnEntry` objects from those decisions
|
||||
3. Validates the spawn request (resource scopes, merge strategy, parallelism)
|
||||
4. Creates `SubplanStatus` and `SpawnMetadata` for each entry
|
||||
5. Returns the result for the caller to persist on the parent plan
|
||||
|
||||
### Execution Modes
|
||||
|
||||
| Mode | Description |
|
||||
|-----------------------|--------------------------------------------------|
|
||||
| `sequential` | Execute one at a time in order |
|
||||
| `parallel` | Execute concurrently (up to `max_parallel`) |
|
||||
| `dependency_ordered` | Respect DAG dependencies via topological sort |
|
||||
|
||||
### Merge Strategies
|
||||
|
||||
After subplans complete, their sandbox outputs are merged:
|
||||
|
||||
| Strategy | Description |
|
||||
|-----------------------|--------------------------------------------------|
|
||||
| `git_three_way` | Three-way merge via `git merge-file` |
|
||||
| `sequential_apply` | Apply changes in completion order |
|
||||
| `fail_on_conflict` | Raise `MergeConflictError` on any conflict |
|
||||
| `last_wins` | Final subplan's output overwrites earlier ones |
|
||||
|
||||
### Subplan Configuration
|
||||
|
||||
```yaml
|
||||
subplan_config:
|
||||
execution_mode: parallel # sequential | parallel | dependency_ordered
|
||||
merge_strategy: git_three_way # git_three_way | sequential_apply | fail_on_conflict | last_wins
|
||||
max_parallel: 5 # 1-50, for parallel mode
|
||||
fail_fast: false # stop all on first failure
|
||||
timeout_per_subplan_seconds: ~ # optional per-subplan timeout
|
||||
retry_failed: true # auto-retry failed subplans
|
||||
max_retries: 2 # 0-5, max retry attempts
|
||||
```
|
||||
|
||||
### Checkpoint Integration
|
||||
|
||||
Before the first subplan execution attempt, the `SubplanExecutionService`
|
||||
automatically creates a checkpoint (the `on_subplan_spawn` trigger). This
|
||||
allows rolling back to the pre-spawn state if subplan execution fails
|
||||
catastrophically.
|
||||
|
||||
---
|
||||
|
||||
## Python API
|
||||
|
||||
### `CorrectionService`
|
||||
|
||||
**Module:** `cleveragents.application.services.correction_service`
|
||||
|
||||
```python
|
||||
from cleveragents.application.services.correction_service import CorrectionService
|
||||
from cleveragents.domain.models.core.correction import CorrectionMode
|
||||
|
||||
svc = CorrectionService(
|
||||
decision_service=decision_svc,
|
||||
plan_service=plan_svc,
|
||||
checkpoint_service=checkpoint_svc, # optional
|
||||
)
|
||||
|
||||
# Create a correction request
|
||||
correction = svc.request_correction(
|
||||
plan_id="01HPLAN...",
|
||||
target_decision_id="01HDECISION...",
|
||||
mode=CorrectionMode.REVERT,
|
||||
guidance="Use a safer migration approach with explicit rollback",
|
||||
)
|
||||
|
||||
# Analyze impact (BFS over decision tree)
|
||||
impact = svc.analyze_impact(correction_id=correction.correction_id)
|
||||
# impact.affected_decisions, impact.affected_files, impact.risk_level
|
||||
|
||||
# Generate a dry-run report (no side effects)
|
||||
report = svc.generate_dry_run_report(correction_id=correction.correction_id)
|
||||
|
||||
# Execute the correction
|
||||
result = svc.execute_correction(correction_id=correction.correction_id)
|
||||
|
||||
# List corrections for a plan
|
||||
corrections = svc.list_corrections(plan_id="01HPLAN...")
|
||||
|
||||
# Cancel a pending correction
|
||||
svc.cancel_correction(correction_id=correction.correction_id)
|
||||
```
|
||||
|
||||
### `SubplanService`
|
||||
|
||||
**Module:** `cleveragents.application.services.subplan_service`
|
||||
|
||||
```python
|
||||
from cleveragents.application.services.subplan_service import SubplanService
|
||||
|
||||
svc = SubplanService(
|
||||
decision_service=decision_svc,
|
||||
config=plan.subplan_config,
|
||||
)
|
||||
|
||||
# Get spawn decisions for the parent plan
|
||||
decisions = svc.get_spawn_decisions(parent_plan_id="01HPLAN...")
|
||||
|
||||
# Build spawn entries from decisions
|
||||
entries = svc.build_spawn_entries(decisions)
|
||||
|
||||
# Spawn child plans (validates first)
|
||||
result = svc.spawn(
|
||||
parent_plan=parent_plan,
|
||||
config=parent_plan.subplan_config,
|
||||
spawn_entries=entries,
|
||||
available_resources=frozenset(known_resource_ids),
|
||||
)
|
||||
# result.spawned_statuses, result.metadata, result.total_spawned
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Decision Correction Reference](../reference/decision_correction.md) — full correction reference
|
||||
- [Subplans: Execution and Merge](../reference/subplans.md) — subplan execution modes and merge strategies
|
||||
- [Subplan Service Reference](../reference/subplan_service.md) — spawn workflow details
|
||||
- [Checkpoints API Reference](checkpoints.md) — checkpoint and rollback
|
||||
- [Decisions API Reference](decisions.md) — decision tree and recording
|
||||
- [Plan CLI Reference](../reference/plan_cli.md) — plan commands overview
|
||||
@@ -0,0 +1,521 @@
|
||||
# Developer Setup Guide
|
||||
|
||||
This guide walks you through setting up a local development environment for
|
||||
**cleveragents-core** from scratch. By the end you will have a fully working
|
||||
environment with all dependencies installed, tests passing, and pre-commit hooks
|
||||
active.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Install the following tools before cloning the repository.
|
||||
|
||||
### Python 3.13+
|
||||
|
||||
CleverAgents requires **Python 3.13** or later.
|
||||
|
||||
```bash
|
||||
# Verify your installed version
|
||||
python3 --version # must print Python 3.13.x or higher
|
||||
```
|
||||
|
||||
Recommended installation methods:
|
||||
|
||||
| Platform | Method |
|
||||
|----------|--------|
|
||||
| macOS | `brew install python@3.13` or [python.org](https://www.python.org/downloads/) |
|
||||
| Linux (Debian/Ubuntu) | `sudo apt install python3.13 python3.13-venv python3.13-dev` |
|
||||
| Linux (Fedora/RHEL) | `sudo dnf install python3.13` |
|
||||
| Windows | [python.org installer](https://www.python.org/downloads/) (add to PATH) |
|
||||
| Any | [pyenv](https://github.com/pyenv/pyenv): `pyenv install 3.13` |
|
||||
|
||||
### uv
|
||||
|
||||
[uv](https://docs.astral.sh/uv/) is the project's fast Python package manager and
|
||||
virtual-environment tool.
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Windows (PowerShell)
|
||||
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
|
||||
# Verify
|
||||
uv --version
|
||||
```
|
||||
|
||||
### nox
|
||||
|
||||
[nox](https://nox.thea.codes/) is the project's task runner. All test, lint,
|
||||
type-check, and build commands are executed through `nox` sessions — **never**
|
||||
invoke `behave`, `ruff`, `pyright`, or other tools directly.
|
||||
|
||||
```bash
|
||||
pip install nox
|
||||
# or, using uv:
|
||||
uv tool install nox
|
||||
```
|
||||
|
||||
### Git
|
||||
|
||||
Git 2.30+ is required. Most systems ship a recent enough version.
|
||||
|
||||
```bash
|
||||
git --version # must be 2.30 or later
|
||||
```
|
||||
|
||||
### Commitizen (optional but recommended)
|
||||
|
||||
[Commitizen](https://commitizen.github.io/cz-cli/) guides you through writing
|
||||
[Conventional Changelog](https://github.com/conventional-changelog/conventional-changelog-eslint/blob/master/convention.md)-compliant
|
||||
commit messages interactively. All commits **must** follow this standard.
|
||||
|
||||
```bash
|
||||
npm install -g commitizen@2.8.6 cz-customizable@4.0.0
|
||||
```
|
||||
|
||||
> **Note:** npm is only needed for Commitizen. Node.js is not otherwise required
|
||||
> to develop or run cleveragents-core.
|
||||
|
||||
### pre-commit
|
||||
|
||||
[pre-commit](https://pre-commit.com/) runs automated checks before each commit.
|
||||
It is installed as part of the dev dependencies (see [Getting Started](#2-getting-started))
|
||||
but the CLI must also be available on your PATH.
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
# or, using uv:
|
||||
uv tool install pre-commit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Getting Started
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://git.cleverthis.com/cleveragents/cleveragents-core.git
|
||||
cd cleveragents-core
|
||||
```
|
||||
|
||||
### Create and Activate a Virtual Environment
|
||||
|
||||
```bash
|
||||
# Create a venv with Python 3.13
|
||||
uv venv --python 3.13
|
||||
|
||||
# Activate (macOS / Linux)
|
||||
source .venv/bin/activate
|
||||
|
||||
# Activate (Windows PowerShell)
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
### Install All Dependencies
|
||||
|
||||
Install the package in editable mode together with all development and test
|
||||
extras:
|
||||
|
||||
```bash
|
||||
uv pip install -e ".[dev,tests,docs]"
|
||||
```
|
||||
|
||||
This installs:
|
||||
|
||||
- **Runtime** dependencies (typer, pydantic, langchain, etc.)
|
||||
- **Dev** extras: ruff, pyright, behave, pytest, pre-commit, bandit, semgrep,
|
||||
vulture, radon
|
||||
- **Tests** extras: behave, slipcover, asv, robotframework, robotframework-pabot
|
||||
- **Docs** extras: mkdocs, mkdocs-material, mkdocstrings
|
||||
|
||||
### Install pre-commit Hooks
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
This installs Git hooks that run linting and formatting checks automatically on
|
||||
`git commit`.
|
||||
|
||||
### Verify the Installation
|
||||
|
||||
Run the full default nox pipeline to confirm everything is working:
|
||||
|
||||
```bash
|
||||
nox
|
||||
```
|
||||
|
||||
This executes (in order): `lint` → `typecheck` → `unit_tests` →
|
||||
`integration_tests` → `coverage_report`. A clean run with all sessions passing
|
||||
means your environment is correctly set up.
|
||||
|
||||
---
|
||||
|
||||
## 3. Development Workflow
|
||||
|
||||
All quality-gate commands are run through `nox`. The `noxfile.py` in the
|
||||
repository root defines every available session.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all default sessions
|
||||
nox
|
||||
|
||||
# Unit tests only (Behave / BDD)
|
||||
nox -s unit_tests
|
||||
|
||||
# Integration tests only (Robot Framework)
|
||||
nox -s integration_tests
|
||||
|
||||
# Coverage report — enforces ≥ 97% threshold
|
||||
nox -s coverage_report
|
||||
|
||||
# Run a single Behave feature file
|
||||
nox -s unit_tests -- features/plan_model.feature
|
||||
|
||||
# Run scenarios with a specific tag
|
||||
nox -s unit_tests -- --tags=@smoke
|
||||
|
||||
# Run a single Robot suite
|
||||
nox -s integration_tests -- --suite robot/plan_persistence_e2e.robot
|
||||
|
||||
# Performance benchmarks (ASV)
|
||||
nox -s benchmark
|
||||
```
|
||||
|
||||
> See [Testing Guide](testing.md) for full details on the testing strategy,
|
||||
> coverage requirements, and test suite structure.
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Check for lint violations (ruff)
|
||||
nox -s lint
|
||||
|
||||
# Auto-fix safe violations
|
||||
nox -s lint -- --fix
|
||||
|
||||
# Check formatting only (no changes)
|
||||
nox -s format -- --check
|
||||
|
||||
# Apply formatting
|
||||
nox -s format
|
||||
```
|
||||
|
||||
Ruff is configured in `pyproject.toml` under `[tool.ruff]`. The project targets
|
||||
Python 3.13 with an 88-character line length and enforces rules from the `E`,
|
||||
`F`, `W`, `B`, `UP`, `I`, `SIM`, and `RUF` rule sets.
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
# Run pyright type checker
|
||||
nox -s typecheck
|
||||
```
|
||||
|
||||
Pyright is configured in `pyproject.toml` under `[tool.pyright]`. All source
|
||||
files under `src/` must pass strict type checking. Do not add `# type: ignore`
|
||||
comments without a documented justification.
|
||||
|
||||
### Security Scanning
|
||||
|
||||
```bash
|
||||
# Run bandit + vulture
|
||||
nox -s security_scan
|
||||
|
||||
# Complexity metrics (radon)
|
||||
nox -s complexity
|
||||
```
|
||||
|
||||
### Building Documentation
|
||||
|
||||
```bash
|
||||
# Build the MkDocs site locally
|
||||
nox -s docs
|
||||
|
||||
# Serve docs with live reload at http://127.0.0.1:8000
|
||||
nox -s docs -- serve
|
||||
```
|
||||
|
||||
### Full Quality Gate (pre-PR checklist)
|
||||
|
||||
Before opening a pull request, run the complete quality gate:
|
||||
|
||||
```bash
|
||||
nox
|
||||
```
|
||||
|
||||
All sessions must pass. The CI pipeline enforces the same checks and will block
|
||||
merges on any failure.
|
||||
|
||||
---
|
||||
|
||||
## 4. Commit Guidelines
|
||||
|
||||
### Conventional Changelog Format
|
||||
|
||||
Every commit **must** follow the
|
||||
[Conventional Changelog standard](https://github.com/conventional-changelog/conventional-changelog-eslint/blob/master/convention.md).
|
||||
The format is:
|
||||
|
||||
```
|
||||
<type>(<scope>): <short summary>
|
||||
|
||||
<optional body — wrap at 72 characters>
|
||||
|
||||
<optional footer — ISSUES CLOSED: #N>
|
||||
```
|
||||
|
||||
**Allowed types:**
|
||||
|
||||
| Type | When to use |
|
||||
|------|-------------|
|
||||
| `feat` | New feature or user-visible behaviour |
|
||||
| `fix` | Bug fix |
|
||||
| `docs` | Documentation only |
|
||||
| `refactor` | Code restructuring without behaviour change |
|
||||
| `test` | Adding or updating tests |
|
||||
| `chore` | Build system, tooling, dependency updates |
|
||||
| `perf` | Performance improvement |
|
||||
| `ci` | CI/CD pipeline changes |
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
fix(concurrency): guard execute_plan with advisory lock
|
||||
|
||||
LockService was implemented but never wired into the plan execution
|
||||
path, leaving execute_plan() unprotected against concurrent calls.
|
||||
|
||||
ISSUES CLOSED: #7989
|
||||
```
|
||||
|
||||
### Using Commitizen (recommended)
|
||||
|
||||
```bash
|
||||
# Instead of `git commit`, use:
|
||||
git cz
|
||||
```
|
||||
|
||||
Commitizen walks you through an interactive prompt to build a correctly
|
||||
formatted commit message. All CleverThis repositories ship a local
|
||||
`cz-customizable` configuration.
|
||||
|
||||
### Atomic Commits
|
||||
|
||||
- **One logical change per commit.** Never bundle unrelated changes.
|
||||
- **Include tests with the change.** Feature + tests are one logical unit.
|
||||
- **Include documentation with the change.** Update relevant docs in the same commit.
|
||||
- **Each commit must build and pass all tests.** The repository must be in a
|
||||
working state at every commit in history.
|
||||
|
||||
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full commit quality policy.
|
||||
|
||||
### Branch Naming
|
||||
|
||||
Use the following prefixes:
|
||||
|
||||
| Prefix | Purpose |
|
||||
|--------|---------|
|
||||
| `feat/<issue>-<slug>` | New feature |
|
||||
| `fix/<issue>-<slug>` | Bug fix |
|
||||
| `docs/<slug>` | Documentation |
|
||||
| `refactor/<slug>` | Refactoring |
|
||||
| `chore/<slug>` | Tooling / maintenance |
|
||||
| `test/<slug>` | Test-only changes |
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
feat/issue-1234-plan-correction-rollback
|
||||
fix/7989-lock-service-concurrency
|
||||
docs/developer-setup-guide
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Devcontainer Setup
|
||||
|
||||
The repository ships a [Dev Container](https://containers.dev/) configuration
|
||||
that provides a fully pre-configured development environment with Python 3.13,
|
||||
uv, nox, and all dependencies pre-installed. This is the **recommended** setup
|
||||
for contributors who want zero-friction onboarding.
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (macOS /
|
||||
Windows) or Docker Engine (Linux)
|
||||
- [VS Code](https://code.visualstudio.com/) with the
|
||||
[Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers),
|
||||
**or** the [devcontainer CLI](https://github.com/devcontainers/cli)
|
||||
|
||||
### Opening in VS Code
|
||||
|
||||
1. Open the repository folder in VS Code.
|
||||
2. When prompted *"Reopen in Container"*, click **Reopen in Container**.
|
||||
Alternatively: open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and
|
||||
run **Dev Containers: Reopen in Container**.
|
||||
3. VS Code builds the container image and installs all extensions. This takes a
|
||||
few minutes on first run; subsequent opens are fast.
|
||||
4. A terminal inside the container is ready to use. All tools (`uv`, `nox`,
|
||||
`git`, `pre-commit`) are on the PATH.
|
||||
|
||||
### Using the devcontainer CLI
|
||||
|
||||
```bash
|
||||
# Start the container
|
||||
devcontainer up --workspace-folder .
|
||||
|
||||
# Execute a command inside the container
|
||||
devcontainer exec --workspace-folder . nox -s unit_tests
|
||||
```
|
||||
|
||||
### Named Configurations
|
||||
|
||||
The repository may define multiple named devcontainer configurations (e.g.,
|
||||
`default`, `docs-only`). To select one in VS Code, use
|
||||
**Dev Containers: Reopen in Container…** and choose the desired configuration
|
||||
from the list.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Sensitive configuration (API keys, database URLs) should be placed in a
|
||||
`.env` file at the repository root. The devcontainer automatically mounts
|
||||
this file. **Never commit `.env` to version control.**
|
||||
|
||||
Common variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `CLEVERAGENTS_DATABASE_URL` | SQLite or PostgreSQL connection string |
|
||||
| `CLEVERAGENTS_HOME` | Override the default data directory |
|
||||
| `CLEVERAGENTS_TESTING_USE_MOCK_AI` | Set to `1` to use mock AI in tests |
|
||||
| `OPENAI_API_KEY` | OpenAI provider key (optional) |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic provider key (optional) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
### `python3 --version` shows < 3.13
|
||||
|
||||
Your system Python is too old. Install Python 3.13 via pyenv, your OS package
|
||||
manager, or [python.org](https://www.python.org/downloads/), then recreate the
|
||||
virtual environment:
|
||||
|
||||
```bash
|
||||
uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
uv pip install -e ".[dev,tests,docs]"
|
||||
```
|
||||
|
||||
### `nox` command not found
|
||||
|
||||
Install nox into your system Python or via uv tools:
|
||||
|
||||
```bash
|
||||
pip install nox
|
||||
# or
|
||||
uv tool install nox
|
||||
```
|
||||
|
||||
Ensure the tool installation directory is on your `PATH` (uv prints the path
|
||||
after installation).
|
||||
|
||||
### `pre-commit` hooks not running
|
||||
|
||||
Reinstall the hooks:
|
||||
|
||||
```bash
|
||||
pre-commit install --overwrite
|
||||
```
|
||||
|
||||
If hooks still do not run, verify that `.git/hooks/pre-commit` exists and is
|
||||
executable.
|
||||
|
||||
### Import errors after installing dependencies
|
||||
|
||||
Ensure you activated the virtual environment before running any commands:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate # macOS / Linux
|
||||
.venv\Scripts\Activate.ps1 # Windows
|
||||
```
|
||||
|
||||
Then verify the package is installed in editable mode:
|
||||
|
||||
```bash
|
||||
pip show cleveragents
|
||||
```
|
||||
|
||||
### `nox -s unit_tests` fails with `AmbiguousStep`
|
||||
|
||||
Two Behave step files define the same step pattern. Check the error message for
|
||||
the conflicting step text, then either rename one step or add a unique prefix.
|
||||
See the [Testing Guide](testing.md#unit-tests-behave) for step naming conventions.
|
||||
|
||||
### Coverage below 97%
|
||||
|
||||
Run the HTML coverage report to identify uncovered lines:
|
||||
|
||||
```bash
|
||||
nox -s coverage_report
|
||||
open build/htmlcov/index.html
|
||||
```
|
||||
|
||||
Write Behave scenarios targeting the uncovered code paths. Do **not** lower the
|
||||
threshold or add `# pragma: no cover` without explicit team agreement.
|
||||
|
||||
### `pyright` reports errors in generated or third-party stubs
|
||||
|
||||
Check `pyproject.toml` under `[tool.pyright]` for the `exclude` list. Generated
|
||||
stubs under `docs/reference/contracts/stubs/` are excluded by the ruff
|
||||
configuration and should also be excluded from pyright. Add a targeted
|
||||
`# type: ignore` with a comment only as a last resort.
|
||||
|
||||
### Robot Framework `Resource file does not exist`
|
||||
|
||||
Always use `${CURDIR}/` prefix for `Resource` imports:
|
||||
|
||||
```robot
|
||||
Resource ${CURDIR}/common.resource
|
||||
```
|
||||
|
||||
Bare paths are resolved relative to the working directory, not the test file.
|
||||
|
||||
### Devcontainer fails to start
|
||||
|
||||
1. Ensure Docker is running (`docker info`).
|
||||
2. Try **Dev Containers: Rebuild Container** from the VS Code Command Palette.
|
||||
3. Check the Docker build log in the VS Code Output panel for errors.
|
||||
4. If the issue persists, delete the container image and rebuild:
|
||||
```bash
|
||||
docker system prune -f
|
||||
devcontainer up --workspace-folder . --remove-existing-container
|
||||
```
|
||||
|
||||
### SQLite in-memory tests lose data between steps
|
||||
|
||||
When writing Behave tests with `sqlite:///:memory:`, use a single shared
|
||||
`Session` across all repository calls in a scenario — do not create a new
|
||||
session per call. See the [Testing Guide](testing.md#troubleshooting) for the
|
||||
correct pattern.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Testing Guide](testing.md) — Full testing strategy, coverage requirements,
|
||||
and suite documentation
|
||||
- [CI/CD Pipeline](ci-cd.md) — Forgejo CI job definitions and pipeline stages
|
||||
- [Quality Automation](quality-automation.md) — Automated quality gates and
|
||||
enforcement
|
||||
- [CONTRIBUTING.md](../../CONTRIBUTING.md) — Commit standards, code of conduct,
|
||||
and contribution workflow
|
||||
- [Architecture Decision Records](../adr/index.md) — Key design decisions
|
||||
(ADR-003 DI, ADR-005 Tech Stack, ADR-009 Project Model, etc.)
|
||||
@@ -258,6 +258,15 @@ Feature: Subplan Execution and Merge
|
||||
Then the first subplan should be errored
|
||||
And the remaining subplans should have CANCELLED status
|
||||
|
||||
@parallel @cancel_status
|
||||
Scenario: Parallel fail_fast marks in-flight futures as CANCELLED
|
||||
Given a parent plan with 3 subplans in parallel mode with fail_fast
|
||||
And the subplan executor will block for 1 seconds
|
||||
And the first subplan will fail with "ValidationError: schema mismatch"
|
||||
When the subplans are executed
|
||||
Then the first subplan should be errored
|
||||
And the remaining subplans should have CANCELLED status
|
||||
|
||||
# --- Dependency-ordered concurrent execution ---
|
||||
|
||||
@dependency_ordered @concurrent
|
||||
|
||||
@@ -30,6 +30,7 @@ nav:
|
||||
- ACMS Context Hydration: modules/context-hydration.md
|
||||
- Git Worktree Sandbox: modules/git-worktree-sandbox.md
|
||||
- Development:
|
||||
- Setup Guide: development/setup.md
|
||||
- Agent System Specification: development/agent-system-specification.md
|
||||
- CI/CD Pipeline: development/ci-cd.md
|
||||
- Quality Automation: development/quality-automation.md
|
||||
|
||||
@@ -55,6 +55,7 @@ from cleveragents.application.services.fix_then_revalidate import (
|
||||
FixThenRevalidateOrchestrator,
|
||||
)
|
||||
from cleveragents.application.services.invariant_service import InvariantService
|
||||
from cleveragents.application.services.lock_service import LockService
|
||||
from cleveragents.application.services.multi_project_service import (
|
||||
MultiProjectService,
|
||||
)
|
||||
@@ -267,6 +268,24 @@ def _build_session_factory(database_url: str) -> sessionmaker[Session]:
|
||||
return sessionmaker(bind=engine, expire_on_commit=False)
|
||||
|
||||
|
||||
def _build_lock_service(
|
||||
database_url: str,
|
||||
) -> LockService:
|
||||
"""Build a LockService with a session factory from the database URL.
|
||||
|
||||
Registered as a Singleton so that all callers within a process share
|
||||
the same advisory-lock state. Follows the same ``_build_*`` pattern
|
||||
used by other DB-backed services (see ``_build_session_factory``,
|
||||
``_build_resource_registry_service``, etc.).
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
engine = create_engine(database_url, echo=False)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
return LockService(session_factory=factory)
|
||||
|
||||
|
||||
def _build_resource_registry_service(
|
||||
database_url: str,
|
||||
) -> ResourceRegistryService:
|
||||
@@ -657,6 +676,12 @@ class Container(containers.DeclarativeContainer):
|
||||
InvariantService,
|
||||
)
|
||||
|
||||
# Lock Service - Singleton (shared advisory-lock state per process, #7989)
|
||||
lock_service = providers.Singleton(
|
||||
_build_lock_service,
|
||||
database_url=database_url,
|
||||
)
|
||||
|
||||
# Plan Lifecycle Service - Factory (v3 four-phase lifecycle)
|
||||
plan_lifecycle_service = providers.Factory(
|
||||
PlanLifecycleService,
|
||||
@@ -665,6 +690,7 @@ class Container(containers.DeclarativeContainer):
|
||||
decision_service=decision_service,
|
||||
event_bus=event_bus,
|
||||
invariant_service=invariant_service,
|
||||
lock_service=lock_service,
|
||||
)
|
||||
|
||||
# Checkpoint Service - database-backed via CheckpointRepository
|
||||
|
||||
@@ -54,6 +54,7 @@ from __future__ import annotations
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import uuid4
|
||||
|
||||
import structlog
|
||||
from ulid import ULID
|
||||
@@ -101,6 +102,7 @@ if TYPE_CHECKING:
|
||||
ErrorPatternService,
|
||||
)
|
||||
from cleveragents.application.services.invariant_service import InvariantService
|
||||
from cleveragents.application.services.lock_service import LockService
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
||||
from cleveragents.infrastructure.events.protocol import EventBus
|
||||
@@ -190,6 +192,7 @@ class PlanLifecycleService:
|
||||
error_pattern_service: ErrorPatternService | None = None,
|
||||
config_service: ConfigService | None = None,
|
||||
invariant_service: InvariantService | None = None,
|
||||
lock_service: LockService | None = None,
|
||||
):
|
||||
"""Initialize the plan lifecycle service.
|
||||
|
||||
@@ -225,6 +228,13 @@ class PlanLifecycleService:
|
||||
Reconciliation Actor is auto-invoked at each phase
|
||||
transition to verify invariants hold. When ``None``,
|
||||
reconciliation is silently skipped.
|
||||
lock_service: Optional :class:`LockService` for plan-level
|
||||
advisory locking. When provided, ``execute_plan`` and
|
||||
``apply_plan`` acquire a plan lock before performing
|
||||
the phase transition and release it in a ``finally``
|
||||
block, preventing concurrent modifications to the same
|
||||
plan. When ``None``, locking is silently skipped for
|
||||
backward compatibility with existing tests.
|
||||
"""
|
||||
self.settings = settings
|
||||
self.unit_of_work = unit_of_work
|
||||
@@ -234,6 +244,7 @@ class PlanLifecycleService:
|
||||
self.error_pattern_service = error_pattern_service
|
||||
self._config_service = config_service
|
||||
self.invariant_service = invariant_service
|
||||
self._lock_service = lock_service
|
||||
self._logger = logger.bind(service="plan_lifecycle")
|
||||
self.preflight_guardrail = PlanPreflightGuardrail()
|
||||
self._subscribe_correction_reconciliation()
|
||||
@@ -1497,6 +1508,11 @@ class PlanLifecycleService:
|
||||
|
||||
This is the 'execute' command.
|
||||
|
||||
A plan-level advisory lock is acquired before the transition and
|
||||
released in a ``finally`` block so that concurrent callers on the
|
||||
same ``plan_id`` receive a ``LockConflictError`` rather than
|
||||
silently racing into the same phase transition.
|
||||
|
||||
Args:
|
||||
plan_id: The plan ULID
|
||||
|
||||
@@ -1507,67 +1523,91 @@ class PlanLifecycleService:
|
||||
NotFoundError: If plan not found
|
||||
InvalidPhaseTransitionError: If transition is not valid
|
||||
PlanNotReadyError: If plan is not ready for transition
|
||||
LockConflictError: If another session holds the plan lock
|
||||
"""
|
||||
plan = self.get_plan(plan_id)
|
||||
# Generate a unique owner_id for this invocation to ensure that
|
||||
# concurrent sessions cannot re-entrantly acquire the same lock.
|
||||
# The LockService treats owner_id as the caller identity and allows
|
||||
# re-entrant acquisition for the same owner; using a unique UUID per
|
||||
# invocation ensures that concurrent sessions present different owners
|
||||
# and thus trigger LockConflictError when attempting to acquire the
|
||||
# same plan lock.
|
||||
owner_id: str = str(uuid4())
|
||||
|
||||
# Validate transition
|
||||
if not can_transition(plan.phase, PlanPhase.EXECUTE):
|
||||
raise InvalidPhaseTransitionError(plan.phase, PlanPhase.EXECUTE)
|
||||
|
||||
# Must be in COMPLETE state to transition
|
||||
if plan.state != ProcessingState.COMPLETE:
|
||||
raise PlanNotReadyError(
|
||||
plan_id, plan.phase, plan.state or ProcessingState.QUEUED
|
||||
if self._lock_service is not None:
|
||||
self._lock_service.acquire(
|
||||
owner_id=owner_id,
|
||||
resource_type="plan",
|
||||
resource_id=plan_id,
|
||||
)
|
||||
try:
|
||||
plan = self.get_plan(plan_id)
|
||||
|
||||
# Run estimation actor if configured (informational only)
|
||||
self._run_estimation(plan)
|
||||
# Validate transition
|
||||
if not can_transition(plan.phase, PlanPhase.EXECUTE):
|
||||
raise InvalidPhaseTransitionError(plan.phase, PlanPhase.EXECUTE)
|
||||
|
||||
# Layer 4: Consult Error Pattern Database for preventive guidance
|
||||
self._consult_error_patterns(plan)
|
||||
# Must be in COMPLETE state to transition
|
||||
if plan.state != ProcessingState.COMPLETE:
|
||||
raise PlanNotReadyError(
|
||||
plan_id, plan.phase, plan.state or ProcessingState.QUEUED
|
||||
)
|
||||
|
||||
# Invariant Reconciliation: verify invariants before Execute
|
||||
self._run_invariant_reconciliation(plan)
|
||||
# Run estimation actor if configured (informational only)
|
||||
self._run_estimation(plan)
|
||||
|
||||
# Transition to Execute phase — set processing_state first so that
|
||||
# the phase-state validator sees QUEUED (valid in any phase) when
|
||||
# the phase assignment triggers re-validation.
|
||||
plan.processing_state = ProcessingState.QUEUED
|
||||
plan.phase = PlanPhase.EXECUTE
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
# Layer 4: Consult Error Pattern Database for preventive guidance
|
||||
self._consult_error_patterns(plan)
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan transitioned to Execute",
|
||||
plan_id=plan_id,
|
||||
phase=plan.phase.value,
|
||||
)
|
||||
if self.event_bus is not None:
|
||||
try:
|
||||
self.event_bus.emit(
|
||||
DomainEvent(
|
||||
event_type=EventType.PLAN_PHASE_CHANGED,
|
||||
plan_id=plan_id,
|
||||
details={
|
||||
"phase": plan.phase.value,
|
||||
"processing_state": plan.processing_state.value
|
||||
if plan.processing_state
|
||||
else None,
|
||||
},
|
||||
# Invariant Reconciliation: verify invariants before Execute
|
||||
self._run_invariant_reconciliation(plan)
|
||||
|
||||
# Transition to Execute phase — set processing_state first so that
|
||||
# the phase-state validator sees QUEUED (valid in any phase) when
|
||||
# the phase assignment triggers re-validation.
|
||||
plan.processing_state = ProcessingState.QUEUED
|
||||
plan.phase = PlanPhase.EXECUTE
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan transitioned to Execute",
|
||||
plan_id=plan_id,
|
||||
phase=plan.phase.value,
|
||||
)
|
||||
if self.event_bus is not None:
|
||||
try:
|
||||
self.event_bus.emit(
|
||||
DomainEvent(
|
||||
event_type=EventType.PLAN_PHASE_CHANGED,
|
||||
plan_id=plan_id,
|
||||
details={
|
||||
"phase": plan.phase.value,
|
||||
"processing_state": plan.processing_state.value
|
||||
if plan.processing_state
|
||||
else None,
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
self._logger.warning(
|
||||
"event_bus_emit_failed",
|
||||
event_type="PLAN_PHASE_CHANGED",
|
||||
plan_id=plan_id,
|
||||
exc_info=True,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
self._logger.warning(
|
||||
"event_bus_emit_failed",
|
||||
event_type="PLAN_PHASE_CHANGED",
|
||||
plan_id=plan_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Enqueue async job when async execution is enabled
|
||||
self._maybe_enqueue_async_job(plan_id, "execute")
|
||||
# Enqueue async job when async execution is enabled
|
||||
self._maybe_enqueue_async_job(plan_id, "execute")
|
||||
|
||||
return plan
|
||||
return plan
|
||||
finally:
|
||||
if self._lock_service is not None:
|
||||
self._lock_service.release(
|
||||
owner_id=owner_id,
|
||||
resource_type="plan",
|
||||
resource_id=plan_id,
|
||||
)
|
||||
|
||||
def start_execute(self, plan_id: str) -> Plan:
|
||||
"""Start the Execute phase processing."""
|
||||
@@ -1681,6 +1721,11 @@ class PlanLifecycleService:
|
||||
|
||||
This is the 'apply' command.
|
||||
|
||||
A plan-level advisory lock is acquired before the transition and
|
||||
released in a ``finally`` block so that concurrent callers on the
|
||||
same ``plan_id`` receive a ``LockConflictError`` rather than
|
||||
silently racing into the same phase transition.
|
||||
|
||||
Args:
|
||||
plan_id: The plan ULID
|
||||
|
||||
@@ -1691,41 +1736,65 @@ class PlanLifecycleService:
|
||||
NotFoundError: If plan not found
|
||||
InvalidPhaseTransitionError: If transition is not valid
|
||||
PlanNotReadyError: If plan is not ready for transition
|
||||
LockConflictError: If another session holds the plan lock
|
||||
"""
|
||||
plan = self.get_plan(plan_id)
|
||||
# Generate a unique owner_id for this invocation to ensure that
|
||||
# concurrent sessions cannot re-entrantly acquire the same lock.
|
||||
# The LockService treats owner_id as the caller identity and allows
|
||||
# re-entrant acquisition for the same owner; using a unique UUID per
|
||||
# invocation ensures that concurrent sessions present different owners
|
||||
# and thus trigger LockConflictError when attempting to acquire the
|
||||
# same plan lock.
|
||||
owner_id: str = str(uuid4())
|
||||
|
||||
# Validate transition
|
||||
if not can_transition(plan.phase, PlanPhase.APPLY):
|
||||
raise InvalidPhaseTransitionError(plan.phase, PlanPhase.APPLY)
|
||||
if self._lock_service is not None:
|
||||
self._lock_service.acquire(
|
||||
owner_id=owner_id,
|
||||
resource_type="plan",
|
||||
resource_id=plan_id,
|
||||
)
|
||||
try:
|
||||
plan = self.get_plan(plan_id)
|
||||
|
||||
# Must be in COMPLETE state to transition
|
||||
if plan.state != ProcessingState.COMPLETE:
|
||||
raise PlanNotReadyError(
|
||||
plan_id, plan.phase, plan.state or ProcessingState.QUEUED
|
||||
# Validate transition
|
||||
if not can_transition(plan.phase, PlanPhase.APPLY):
|
||||
raise InvalidPhaseTransitionError(plan.phase, PlanPhase.APPLY)
|
||||
|
||||
# Must be in COMPLETE state to transition
|
||||
if plan.state != ProcessingState.COMPLETE:
|
||||
raise PlanNotReadyError(
|
||||
plan_id, plan.phase, plan.state or ProcessingState.QUEUED
|
||||
)
|
||||
|
||||
# Invariant Reconciliation: verify invariants before Apply
|
||||
self._run_invariant_reconciliation(plan)
|
||||
|
||||
# Transition to Apply phase — set processing_state first so that
|
||||
# the phase-state validator sees QUEUED (valid in any phase) when
|
||||
# the phase assignment triggers re-validation.
|
||||
plan.processing_state = ProcessingState.QUEUED
|
||||
plan.phase = PlanPhase.APPLY
|
||||
plan.timestamps.apply_started_at = datetime.now()
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan transitioned to Apply",
|
||||
plan_id=plan_id,
|
||||
phase=plan.phase.value,
|
||||
)
|
||||
|
||||
# Invariant Reconciliation: verify invariants before Apply
|
||||
self._run_invariant_reconciliation(plan)
|
||||
# Enqueue async job when async execution is enabled
|
||||
self._maybe_enqueue_async_job(plan_id, "apply")
|
||||
|
||||
# Transition to Apply phase — set processing_state first so that
|
||||
# the phase-state validator sees QUEUED (valid in any phase) when
|
||||
# the phase assignment triggers re-validation.
|
||||
plan.processing_state = ProcessingState.QUEUED
|
||||
plan.phase = PlanPhase.APPLY
|
||||
plan.timestamps.apply_started_at = datetime.now()
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan transitioned to Apply",
|
||||
plan_id=plan_id,
|
||||
phase=plan.phase.value,
|
||||
)
|
||||
|
||||
# Enqueue async job when async execution is enabled
|
||||
self._maybe_enqueue_async_job(plan_id, "apply")
|
||||
|
||||
return plan
|
||||
return plan
|
||||
finally:
|
||||
if self._lock_service is not None:
|
||||
self._lock_service.release(
|
||||
owner_id=owner_id,
|
||||
resource_type="plan",
|
||||
resource_id=plan_id,
|
||||
)
|
||||
|
||||
def start_apply(self, plan_id: str) -> Plan:
|
||||
"""Start the Apply phase processing."""
|
||||
|
||||
@@ -301,6 +301,7 @@ class SubplanExecutionService:
|
||||
completion_order: list[str] = []
|
||||
stop_flag = False
|
||||
timeout = self._config.timeout_per_subplan_seconds
|
||||
status_map = {status.subplan_id: status for status in statuses}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
||||
future_to_id: dict[Future[tuple[SubplanStatus, dict[str, str]]], str] = {}
|
||||
@@ -315,20 +316,26 @@ class SubplanExecutionService:
|
||||
|
||||
for future in as_completed(future_to_id):
|
||||
subplan_id = future_to_id[future]
|
||||
original_status = status_map[subplan_id]
|
||||
try:
|
||||
result_status, output = future.result()
|
||||
except CancelledError:
|
||||
result_status = self._cancel_status(
|
||||
next(s for s in statuses if s.subplan_id == subplan_id)
|
||||
)
|
||||
result_status = self._cancel_status(original_status)
|
||||
output = {}
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
result_status = self._error_status(
|
||||
next(s for s in statuses if s.subplan_id == subplan_id),
|
||||
original_status,
|
||||
str(exc),
|
||||
)
|
||||
output = {}
|
||||
|
||||
if stop_flag and result_status.status not in (
|
||||
ProcessingState.ERRORED,
|
||||
ProcessingState.CANCELLED,
|
||||
):
|
||||
result_status = self._cancel_status(original_status)
|
||||
output = {}
|
||||
|
||||
results_map[subplan_id] = (result_status, output)
|
||||
completion_order.append(subplan_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user