mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Board Program LEARN context, ruff 0.16, and verb-rejection observability (#700)
* fix(board): LEARN decisions name the item, not its per-cycle index A cycle's reject reasons are rendered into the NEXT cycle's exploration prompt, but the ref recorded alongside each reason was the item's stored id (item-0/item-1) — a per-cycle index that means something different every cycle and appears nowhere the explorer can resolve. The reason survived the loop; what it was about did not. Record the item's title instead, via a shared learn_ref() helper (falls back to the id when title-less, and reads target_task_title for Scales, whose items name the live task they mutate). * chore(lint): satisfy ruff 0.16 — keyword-only signatures and markdown formatting The dev toolchain resolved ruff 0.16.0, which stabilises PLR0917 (too many positional arguments) and formats python code blocks inside markdown. Both fired repo-wide and neither had anything to do with the code they flagged. - 36 signatures gain a `*` so their tail arguments are keyword-only, and the 104 call sites that passed them positionally are converted. mypy was the safety net for the static ones; the full suite caught nine more that only bind at runtime (the MCP tool functions, whose real callers already pass named JSON arguments). - 28 markdown files reformatted by 0.16's code-block formatter. - One RUF036 (`None` mid-union) autofixed in the GitLab provider. * fix(gateway): log the reason when a verb rejects A rejected envelope rides an HTTP 200, its body is never logged, and there is no trace table — so in the access log a verb an agent could not satisfy looks identical to one that worked. On 2026-07-25 four Board Programs (Periscope, Sentinel, Scales, Barfly) each POSTed their propose verb three or four times, persisted nothing, and left their exploration tasks PENDING; the reason was unrecoverable afterwards, from the logs or from the agents' own transcripts. Log error/message/remediate/missing plus the calling agent at envelope_to_response — the one chokepoint every v1 flow and do route returns through. Success envelopes stay silent. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -312,19 +312,19 @@ Major tasks are escalated to CEO for final approval:
|
||||
|
||||
```python
|
||||
# Git configuration (all tasks follow git workflow)
|
||||
task_type: TaskType # code, documentation, research, planning, design, administrative
|
||||
project_id: UUID # Project this task works on (required)
|
||||
branch_name: str # Branch for this task (auto-created on claim)
|
||||
work_session_id: UUID # Active work session
|
||||
task_type: TaskType # code, documentation, research, planning, design, administrative
|
||||
project_id: UUID # Project this task works on (required)
|
||||
branch_name: str # Branch for this task (auto-created on claim)
|
||||
work_session_id: UUID # Active work session
|
||||
|
||||
# PR tracking (parallel execution in awaiting_documentation)
|
||||
pr_number: int # GitHub/GitLab PR number
|
||||
pr_url: str # Full URL to PR
|
||||
docs_complete: bool # Documenter has finished
|
||||
pr_created: bool # Developer has created PR
|
||||
pr_number: int # GitHub/GitLab PR number
|
||||
pr_url: str # Full URL to PR
|
||||
docs_complete: bool # Documenter has finished
|
||||
pr_created: bool # Developer has created PR
|
||||
|
||||
# Commits linked to task
|
||||
commits: list[CommitRef] # All commits made for this task
|
||||
commits: list[CommitRef] # All commits made for this task
|
||||
```
|
||||
|
||||
## Communication Model
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
When casting SQLAlchemy `Mapped[UUID]` primary-key columns to the runtime `uuid.UUID` type for typing purposes, use the string-literal form:
|
||||
|
||||
```python
|
||||
cast('UUID', child.id)
|
||||
cast("UUID", child.id)
|
||||
```
|
||||
|
||||
not the runtime symbol form:
|
||||
|
||||
@@ -43,10 +43,7 @@ Cell Members → Cell PM → Main PM → Product Owner → CEO
|
||||
## Escalation Tool
|
||||
|
||||
```python
|
||||
escalate_up(
|
||||
task_id="uuid-here",
|
||||
reason="Need clarification on requirements"
|
||||
)
|
||||
escalate_up(task_id="uuid-here", reason="Need clarification on requirements")
|
||||
```
|
||||
|
||||
Auto-routes to your escalation target. You CANNOT choose a different target. `escalate_up` is a PM verb (Cell PM / Main PM); cell members (devs, QA, documenters) signal blockers with `i_am_blocked(task_id, reason)`, which their Cell PM resolves.
|
||||
@@ -54,10 +51,7 @@ Auto-routes to your escalation target. You CANNOT choose a different target. `es
|
||||
## CEO Escalation (Main PM / Board Only)
|
||||
|
||||
```python
|
||||
escalate_to_ceo(
|
||||
task_id="uuid-here",
|
||||
reason="Major feature ready for approval"
|
||||
)
|
||||
escalate_to_ceo(task_id="uuid-here", reason="Major feature ready for approval")
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
@@ -104,8 +104,7 @@ def _check_intent_preconditions(
|
||||
return None
|
||||
|
||||
first_missing = next(
|
||||
p for p in spec_intent.extra_preconditions
|
||||
if p.missing_token == missing[0]
|
||||
p for p in spec_intent.extra_preconditions if p.missing_token == missing[0]
|
||||
)
|
||||
|
||||
# Check the rejection_kind of the first failing precondition
|
||||
@@ -117,10 +116,7 @@ def _check_intent_preconditions(
|
||||
)
|
||||
|
||||
# Default: tracing_gap with missing tokens
|
||||
return Decision.tracing_gap(
|
||||
missing=missing,
|
||||
remediate=first_missing.remediate
|
||||
)
|
||||
return Decision.tracing_gap(missing=missing, remediate=first_missing.remediate)
|
||||
```
|
||||
|
||||
The key insight: **Only the first failing precondition's `rejection_kind` is checked.** This ensures ownership gates are checked early (they usually are in the preconditions list) so unowned tasks fail fast with `not_authorized` instead of collecting other tracing gaps.
|
||||
|
||||
@@ -17,13 +17,13 @@ Each takes `findings: list[dict]` — a list of structured findings. The legacy
|
||||
|
||||
```python
|
||||
{
|
||||
"file": "roboco/api/routes/rate_limit.py", # optional; repo-relative, no ".."
|
||||
"line": 88, # optional; >= 1
|
||||
"severity": "blocker", # required: blocker | major | minor | nit
|
||||
"file": "roboco/api/routes/rate_limit.py", # optional; repo-relative, no ".."
|
||||
"line": 88, # optional; >= 1
|
||||
"severity": "blocker", # required: blocker | major | minor | nit
|
||||
"criterion": "<acceptance-criterion id or exact text>", # optional
|
||||
"expected": "429 on the 101st request", # required, <=300 chars
|
||||
"actual": "the 100th request also 429s", # required, <=300 chars
|
||||
"fix": "use > not >= on the window limit", # optional, <=500 chars — describe the change, never a literal patch
|
||||
"expected": "429 on the 101st request", # required, <=300 chars
|
||||
"actual": "the 100th request also 429s", # required, <=300 chars
|
||||
"fix": "use > not >= on the window limit", # optional, <=500 chars — describe the change, never a literal patch
|
||||
"evidence": "<failing test output / CI lines / diff hunk>", # optional, <=2000 chars
|
||||
}
|
||||
```
|
||||
|
||||
@@ -129,9 +129,9 @@ Biweekly cron, org-scoped. Playbook curation is otherwise reactive — you only
|
||||
propose_playbook_drafts(
|
||||
drafts=[
|
||||
{
|
||||
"title": "...", # <=200 chars, must not duplicate an existing playbook (case-insensitive)
|
||||
"body": "...", # <=4000 chars, the procedure itself
|
||||
"pattern_evidence": "...", # REQUIRED, <=500 chars — which repeated journal/learning pattern justifies this
|
||||
"title": "...", # <=200 chars, must not duplicate an existing playbook (case-insensitive)
|
||||
"body": "...", # <=4000 chars, the procedure itself
|
||||
"pattern_evidence": "...", # REQUIRED, <=500 chars — which repeated journal/learning pattern justifies this
|
||||
},
|
||||
# 1-3 drafts
|
||||
],
|
||||
|
||||
+20
-10
@@ -149,9 +149,9 @@ Each finding is inserted onto the task's revision-findings ledger (`origin=pm`)
|
||||
## Monitoring Your Cell
|
||||
|
||||
```python
|
||||
triage() # surfaces tasks waiting on you
|
||||
roboco_git_status(...) # workspace state
|
||||
roboco_git_log(...) # cell branch history
|
||||
triage() # surfaces tasks waiting on you
|
||||
roboco_git_status(...) # workspace state
|
||||
roboco_git_log(...) # cell branch history
|
||||
note(text="...", scope="reflect") # journal observations
|
||||
```
|
||||
|
||||
@@ -159,12 +159,20 @@ note(text="...", scope="reflect") # journal observations
|
||||
|
||||
```python
|
||||
# Cross-cell coordination
|
||||
dm(recipient="fe-pm", text="Need to align on shared schema; task X.",
|
||||
task_id="...", skill="api_design")
|
||||
dm(
|
||||
recipient="fe-pm",
|
||||
text="Need to align on shared schema; task X.",
|
||||
task_id="...",
|
||||
skill="api_design",
|
||||
)
|
||||
|
||||
# Ack-required notification (PMs / Board only)
|
||||
notify(target="be-dev-1", text="Please prioritise task X by EOD.",
|
||||
priority="high", task_id="...")
|
||||
notify(
|
||||
target="be-dev-1",
|
||||
text="Please prioritise task X by EOD.",
|
||||
priority="high",
|
||||
task_id="...",
|
||||
)
|
||||
```
|
||||
|
||||
## Assembling + Submitting Finished Work
|
||||
@@ -206,7 +214,9 @@ Use `escalate_up(task_id, reason)` when:
|
||||
- A non-cell agent is blocking you
|
||||
|
||||
```python
|
||||
escalate_up(task_id="<task>",
|
||||
reason="Frontend cell needs the new auth endpoint we own; "
|
||||
"they're blocked. Want to confirm priority swap.")
|
||||
escalate_up(
|
||||
task_id="<task>",
|
||||
reason="Frontend cell needs the new auth endpoint we own; "
|
||||
"they're blocked. Want to confirm priority swap.",
|
||||
)
|
||||
```
|
||||
|
||||
@@ -68,13 +68,15 @@ roboco_kb_search("similar documentation")
|
||||
Use `roboco_docs_write()` — handles paths and deduplication automatically:
|
||||
|
||||
```python
|
||||
roboco_docs_write({
|
||||
"task_id": "your-task-uuid",
|
||||
"filename": "feature-api.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "Feature API Documentation",
|
||||
"content": "# Feature API\n\n..."
|
||||
})
|
||||
roboco_docs_write(
|
||||
{
|
||||
"task_id": "your-task-uuid",
|
||||
"filename": "feature-api.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "Feature API Documentation",
|
||||
"content": "# Feature API\n\n...",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**SMART DEDUPLICATION**: RAG searches for similar existing docs.
|
||||
|
||||
@@ -69,9 +69,9 @@ propose_market_brief(
|
||||
{"claim": "...", "source_url": "https://...", "relevance": "..."},
|
||||
# 1-7 findings, source_url REQUIRED per finding — an uncited claim is rejected
|
||||
],
|
||||
threats=["..."], # optional, up to 5
|
||||
opportunities=["..."], # optional, up to 5
|
||||
positioning_note="...", # optional
|
||||
threats=["..."], # optional, up to 5
|
||||
opportunities=["..."], # optional, up to 5
|
||||
positioning_note="...", # optional
|
||||
)
|
||||
```
|
||||
|
||||
@@ -99,8 +99,12 @@ Quarterly cron, project-scoped (`projects.board_programs` contains `"mirror"`).
|
||||
propose_messaging_fixes(
|
||||
items=[
|
||||
{
|
||||
"title": "...", "description": "...", "acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-website", "team": "backend", "priority": 2,
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-website",
|
||||
"team": "backend",
|
||||
"priority": 2,
|
||||
"evidence": "BOTH the drifted claim and the reality it contradicts — REQUIRED",
|
||||
},
|
||||
# 1-5 items
|
||||
@@ -138,8 +142,8 @@ Cron every 2 days, org-scoped. The task carries a set of SCREENED candidate X co
|
||||
propose_conversation_replies(
|
||||
items=[
|
||||
{
|
||||
"tweet_id": "...", # REQUIRED — must be one of the candidate ids verbatim
|
||||
"reply_body": "...", # your voice, <=280 chars, no invented facts
|
||||
"tweet_id": "...", # REQUIRED — must be one of the candidate ids verbatim
|
||||
"reply_body": "...", # your voice, <=280 chars, no invented facts
|
||||
"rationale": "why this conversation is worth replying to", # REQUIRED
|
||||
},
|
||||
# up to 5 items
|
||||
@@ -166,7 +170,11 @@ The CEO acts via the panel/UI; you idle until the CEO decides.
|
||||
## A2A
|
||||
|
||||
```python
|
||||
dm(recipient="product-owner", text="Market analysis for the launch — ...", task_id="...")
|
||||
dm(
|
||||
recipient="product-owner",
|
||||
text="Market analysis for the launch — ...",
|
||||
task_id="...",
|
||||
)
|
||||
```
|
||||
|
||||
Skills: market_analysis
|
||||
|
||||
@@ -68,7 +68,7 @@ notify(target="be-pm", text="New initiative assigned — see task", task_id=subt
|
||||
|
||||
Monitor via:
|
||||
```python
|
||||
triage_all() # actionable tasks across all teams (Main PM only)
|
||||
triage_all() # actionable tasks across all teams (Main PM only)
|
||||
```
|
||||
|
||||
## Tool Surface (per-spawn manifest)
|
||||
|
||||
@@ -107,8 +107,12 @@ Biweekly cron, project-scoped (`projects.board_programs` contains `"spackle"`).
|
||||
propose_gap_fill(
|
||||
items=[
|
||||
{
|
||||
"title": "...", "description": "...", "acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-api", "team": "backend", "priority": 2,
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-api",
|
||||
"team": "backend",
|
||||
"priority": 2,
|
||||
"evidence": "BOTH sides of the gap — REQUIRED",
|
||||
},
|
||||
# 1-5 items
|
||||
@@ -146,8 +150,12 @@ Event-triggered only (a release-publish hook, or the CEO's "run now") — no cro
|
||||
propose_friction_fixes(
|
||||
items=[
|
||||
{
|
||||
"title": "...", "description": "...", "acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-api", "team": "frontend", "priority": 2,
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"acceptance_criteria": ["..."],
|
||||
"project_slug": "roboco-api",
|
||||
"team": "frontend",
|
||||
"priority": 2,
|
||||
"evidence": "the walked path (which pages, which clicks) — prose only, never a screenshot — REQUIRED",
|
||||
},
|
||||
# 1-5 items
|
||||
|
||||
@@ -143,10 +143,11 @@ The system blocks QA from reviewing their own dev work. The `original_developer`
|
||||
`escalate_up` is **not** in your manifest. Use `dm` to your Cell PM if something needs attention beyond pass/fail:
|
||||
|
||||
```python
|
||||
dm(recipient="be-pm",
|
||||
text="Task X — security concern, can you take a look before we "
|
||||
"merge?",
|
||||
task_id="...")
|
||||
dm(
|
||||
recipient="be-pm",
|
||||
text="Task X — security concern, can you take a look before we merge?",
|
||||
task_id="...",
|
||||
)
|
||||
```
|
||||
|
||||
For an external blocker (test environment broken, can't reproduce, missing infra), use `i_am_blocked(task_id, reason="...")` — your Cell PM is notified and `unblock`s you. If the work itself is wrong, `fail(task_id, findings=[...])` with the full context is the right move; the Cell PM picks it up from `needs_revision`.
|
||||
|
||||
@@ -33,12 +33,11 @@ All functions MUST have type hints:
|
||||
|
||||
```python
|
||||
# Good
|
||||
async def fetch_user(user_id: UUID) -> User | None:
|
||||
...
|
||||
async def fetch_user(user_id: UUID) -> User | None: ...
|
||||
|
||||
|
||||
# Bad - no type hints
|
||||
def fetch_user(user_id):
|
||||
...
|
||||
def fetch_user(user_id): ...
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
@@ -79,6 +78,7 @@ ALL I/O operations must be async:
|
||||
async def fetch_user(user_id: str) -> User:
|
||||
return await db.users.get(user_id)
|
||||
|
||||
|
||||
# Bad - blocking
|
||||
def fetch_user(user_id: str) -> User:
|
||||
return db.users.get(user_id) # Blocks!
|
||||
|
||||
@@ -29,12 +29,15 @@ Define domain-specific exceptions:
|
||||
class TaskError(Exception):
|
||||
"""Base exception for task operations."""
|
||||
|
||||
|
||||
class TaskNotFoundError(TaskError):
|
||||
"""Task does not exist."""
|
||||
|
||||
|
||||
class TaskAlreadyClaimedError(TaskError):
|
||||
"""Task is already claimed."""
|
||||
|
||||
|
||||
# Usage
|
||||
if task is None:
|
||||
raise TaskNotFoundError(f"Task {task_id} not found")
|
||||
@@ -62,6 +65,7 @@ Use structlog, NEVER print:
|
||||
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Good
|
||||
@@ -86,6 +90,7 @@ async def create_task(request: TaskCreate) -> TaskResponse:
|
||||
# Pydantic validates automatically
|
||||
...
|
||||
|
||||
|
||||
# Internal service - trust validated data
|
||||
async def process_task(task: Task) -> None:
|
||||
# No need to re-validate
|
||||
|
||||
@@ -12,6 +12,7 @@ DATABASE_URL = "postgresql://user:password@host/db"
|
||||
# Good - environment variables
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
api_key: str
|
||||
database_url: str
|
||||
@@ -27,9 +28,7 @@ NEVER use string concatenation for SQL:
|
||||
query = f"SELECT * FROM users WHERE id = '{user_id}'"
|
||||
|
||||
# Good - parameterized query
|
||||
result = await session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
result = await session.execute(select(User).where(User.id == user_id))
|
||||
```
|
||||
|
||||
## Command Injection Prevention
|
||||
@@ -39,10 +38,12 @@ NEVER pass user input directly to shell:
|
||||
```python
|
||||
# Bad - command injection
|
||||
import os
|
||||
|
||||
os.system(f"process_file {filename}")
|
||||
|
||||
# Good - use subprocess with list
|
||||
import subprocess
|
||||
|
||||
subprocess.run(["process_file", filename], check=True)
|
||||
```
|
||||
|
||||
@@ -56,6 +57,7 @@ result = eval(user_input)
|
||||
|
||||
# Good - safe parsing
|
||||
import ast
|
||||
|
||||
result = ast.literal_eval(user_input) # Only literals
|
||||
```
|
||||
|
||||
@@ -68,7 +70,7 @@ import hashlib
|
||||
|
||||
content_hash = hashlib.md5(
|
||||
content.encode(),
|
||||
usedforsecurity=False # Required flag
|
||||
usedforsecurity=False, # Required flag
|
||||
).hexdigest()[:12]
|
||||
```
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ Use pytest-asyncio:
|
||||
```python
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_user() -> None:
|
||||
user = await fetch_user("test-123")
|
||||
@@ -48,11 +49,12 @@ Use factory-boy for test data:
|
||||
```python
|
||||
from factory import Factory, Faker, LazyAttribute
|
||||
|
||||
|
||||
class TaskFactory(Factory):
|
||||
class Meta:
|
||||
model = Task
|
||||
|
||||
title = Faker('sentence')
|
||||
title = Faker("sentence")
|
||||
status = TaskStatus.PENDING
|
||||
created_at = LazyAttribute(lambda _: datetime.now(UTC))
|
||||
```
|
||||
@@ -84,7 +86,7 @@ When you assign `None` to an attribute inside a test function, mypy narrows that
|
||||
def test_example() -> None:
|
||||
t = _Task()
|
||||
t.notes = None # mypy narrows type to None
|
||||
process(t) # Even though process may write to t.notes
|
||||
process(t) # Even though process may write to t.notes
|
||||
assert t.notes is not None # [unreachable] — mypy sees this as always False
|
||||
```
|
||||
|
||||
@@ -99,6 +101,7 @@ class _TaskWithNoNotes:
|
||||
self.id = uuid4()
|
||||
self.notes: dict[str, Any] | None = None # Declared as union, not narrowed
|
||||
|
||||
|
||||
def test_example() -> None:
|
||||
t = _TaskWithNoNotes() # Use the helper instead
|
||||
process(t)
|
||||
|
||||
@@ -6,10 +6,10 @@ A2A is direct peer-to-peer messaging between agents. There is **no** `roboco_age
|
||||
|
||||
```python
|
||||
dm(
|
||||
recipient="be-qa", # target agent slug
|
||||
recipient="be-qa", # target agent slug
|
||||
text="Please review my changes",
|
||||
task_id="abc123...", # auto-filled from your active task if omitted
|
||||
skill=None, # optional skill slug to scope the conversation
|
||||
task_id="abc123...", # auto-filled from your active task if omitted
|
||||
skill=None, # optional skill slug to scope the conversation
|
||||
)
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@ Same-cell peers (e.g. `be-dev-1` alongside `be-dev-2`/`be-qa`/`be-doc`/`be-pm`)
|
||||
When another agent messages you, your claim briefing surfaces it under `unread_a2a` — each entry shows the sender and a preview of their latest incoming message. To read the full bodies (and clear them), call:
|
||||
|
||||
```python
|
||||
read_a2a() # -> {"messages": [{from_agent, content, created_at}, ...]}
|
||||
read_a2a() # -> {"messages": [{from_agent, content, created_at}, ...]}
|
||||
```
|
||||
|
||||
`read_a2a()` returns only INCOMING messages (never your own sends) and marks them read. `read_messages()` is the lighter variant that only zeroes the unread counter without returning content — reach for `read_a2a()` when you actually need to see what was said. Either clears `i_am_idle()`'s unread-A2A soft-block.
|
||||
@@ -45,9 +45,9 @@ read_a2a() # -> {"messages": [{from_agent, content, created_at}, ...]}
|
||||
Formal, ack-required notifications are a separate inbox — see `docs/rag/tools/messaging-tools.md`:
|
||||
|
||||
```python
|
||||
notify_list(unread_only=True) # list pending items
|
||||
notify_get(notification_id) # read one (marks it read)
|
||||
notify_ack(notification_id) # acknowledge after handling
|
||||
notify_list(unread_only=True) # list pending items
|
||||
notify_get(notification_id) # read one (marks it read)
|
||||
notify_ack(notification_id) # acknowledge after handling
|
||||
```
|
||||
|
||||
## When to use A2A
|
||||
|
||||
+22
-38
@@ -16,31 +16,24 @@ roboco_kb_search(
|
||||
query="rate limiting redis",
|
||||
top_k=5,
|
||||
project="roboco-api",
|
||||
index_types=["code", "docs"]
|
||||
index_types=["code", "docs"],
|
||||
)
|
||||
```
|
||||
|
||||
## AI-Generated Answers
|
||||
|
||||
```python
|
||||
roboco_rag_query(
|
||||
query="How does authentication work?",
|
||||
top_k=5
|
||||
)
|
||||
roboco_rag_query(query="How does authentication work?", top_k=5)
|
||||
```
|
||||
|
||||
## Mentor (Conversational)
|
||||
|
||||
```python
|
||||
response = roboco_ask_mentor(
|
||||
question="How do I handle auth?",
|
||||
domain="coding"
|
||||
)
|
||||
response = roboco_ask_mentor(question="How do I handle auth?", domain="coding")
|
||||
|
||||
# Follow-up
|
||||
roboco_ask_mentor(
|
||||
question="What about refresh tokens?",
|
||||
conversation_id=response["conversation_id"]
|
||||
question="What about refresh tokens?", conversation_id=response["conversation_id"]
|
||||
)
|
||||
```
|
||||
|
||||
@@ -48,13 +41,15 @@ roboco_ask_mentor(
|
||||
|
||||
```python
|
||||
# Write/update documentation (auto-dedup via RAG)
|
||||
roboco_docs_write({
|
||||
"task_id": "task-uuid",
|
||||
"filename": "api-endpoints.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "API Endpoints",
|
||||
"content": "# API Endpoints\n\n..."
|
||||
})
|
||||
roboco_docs_write(
|
||||
{
|
||||
"task_id": "task-uuid",
|
||||
"filename": "api-endpoints.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "API Endpoints",
|
||||
"content": "# API Endpoints\n\n...",
|
||||
}
|
||||
)
|
||||
|
||||
# List docs for a task
|
||||
roboco_docs_list(task_id="task-uuid")
|
||||
@@ -69,33 +64,24 @@ roboco_docs_read(path="backend/api/endpoints.md")
|
||||
|
||||
```python
|
||||
# Index code (PM, Developer)
|
||||
roboco_kb_index_code(
|
||||
sources=["src/**/*.py"],
|
||||
project="roboco-api"
|
||||
)
|
||||
roboco_kb_index_code(sources=["src/**/*.py"], project="roboco-api")
|
||||
|
||||
# Index docs (PM, Documenter) - for bulk/explicit indexing
|
||||
# Note: roboco_docs_write() auto-indexes when writing
|
||||
roboco_kb_index_docs(
|
||||
sources=["docs/**/*.md"],
|
||||
project="roboco-api"
|
||||
)
|
||||
roboco_kb_index_docs(sources=["docs/**/*.md"], project="roboco-api")
|
||||
```
|
||||
|
||||
## Error Tracking
|
||||
|
||||
```python
|
||||
# Search for similar errors
|
||||
roboco_search_error(
|
||||
error_message="Redis connection timed out",
|
||||
context="startup"
|
||||
)
|
||||
roboco_search_error(error_message="Redis connection timed out", context="startup")
|
||||
|
||||
# Record solution
|
||||
roboco_record_error_solution(
|
||||
error_message="Redis connection timed out",
|
||||
solution="Added retry with backoff",
|
||||
worked=True
|
||||
worked=True,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -106,11 +92,9 @@ roboco_record_error_solution(
|
||||
roboco_check_decision(topic="session storage")
|
||||
|
||||
# Record decision
|
||||
roboco_record_decision(params={
|
||||
topic: "Session storage",
|
||||
decision: "Use Redis",
|
||||
rationale: "Sub-ms reads"
|
||||
})
|
||||
roboco_record_decision(
|
||||
params={topic: "Session storage", decision: "Use Redis", rationale: "Sub-ms reads"}
|
||||
)
|
||||
```
|
||||
|
||||
## Standards & Validation
|
||||
@@ -135,7 +119,7 @@ def create_user(email, password):
|
||||
user = User(email=email, password=password)
|
||||
db.add(user)
|
||||
return user
|
||||
"""
|
||||
""",
|
||||
)
|
||||
```
|
||||
|
||||
@@ -172,7 +156,7 @@ def create_user(email, password):
|
||||
roboco_review_code(
|
||||
code="def handle(...):",
|
||||
file_path="src/api/auth.py",
|
||||
change_type="modify" # add, modify, delete
|
||||
change_type="modify", # add, modify, delete
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ notify(target="be-dev-1", text="Task ready for you", priority="normal", task_id=
|
||||
Every role with an inbox gets these (so `i_am_idle()` doesn't soft-block on unread items):
|
||||
|
||||
```python
|
||||
notify_list(unread_only=True, limit=20) # your inbox
|
||||
notify_get(notification_id) # read one (marks it read)
|
||||
notify_ack(notification_id) # acknowledge after handling
|
||||
notify_list(unread_only=True, limit=20) # your inbox
|
||||
notify_get(notification_id) # read one (marks it read)
|
||||
notify_ack(notification_id) # acknowledge after handling
|
||||
```
|
||||
|
||||
When `i_am_idle()` reports unread A2A or @mentions, clear A2A with `read_a2a()` (see `a2a-tools.md`) and clear notifications with list -> get -> ack, then idle again. (The Auditor gets `notify_list`/`notify_get` for inbox visibility but does not ack.)
|
||||
|
||||
@@ -31,8 +31,11 @@ There is **no** `roboco_git_commit / _push / _checkout / _create_pr / _merge_pr`
|
||||
To learn how a project's codebase is laid out or how a subsystem works, query the knowledge base rather than a project tool:
|
||||
|
||||
```python
|
||||
roboco_kb_search(query="rate limiting redis", project="roboco-api",
|
||||
index_types=["code", "documentation"])
|
||||
roboco_kb_search(
|
||||
query="rate limiting redis",
|
||||
project="roboco-api",
|
||||
index_types=["code", "documentation"],
|
||||
)
|
||||
roboco_ask_mentor(question="How is auth wired up in this project?")
|
||||
```
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@ The verbs below are grouped by who calls them.
|
||||
## Developer flow
|
||||
|
||||
```python
|
||||
give_me_work() # returns your most-actionable pending task
|
||||
give_me_work() # returns your most-actionable pending task
|
||||
i_will_work_on(task_id, plan="...")
|
||||
# claims + sets plan + starts; auto-creates and
|
||||
# checks out feature/{team}/{task-hierarchy}
|
||||
commit(message, files=None) # content tool — repeat per change (auto-pushed)
|
||||
open_pr(task_id) # pushes branch + opens the PR
|
||||
# claims + sets plan + starts; auto-creates and
|
||||
# checks out feature/{team}/{task-hierarchy}
|
||||
commit(message, files=None) # content tool — repeat per change (auto-pushed)
|
||||
open_pr(task_id) # pushes branch + opens the PR
|
||||
i_am_done(task_id, notes="", resolved_findings=None)
|
||||
# verifying -> awaiting_qa (PR must already be open);
|
||||
# on a bounced task, name every open ledger finding
|
||||
# via resolved_findings=[{finding_id, commit?, note?}]
|
||||
i_am_blocked(task_id, reason) # external dependency; cell PM unblocks
|
||||
unclaim(task_id) # release a claimed task back to the queue
|
||||
resume(task_id) # recover a paused task after compact/restart
|
||||
i_am_idle() # no work in your queue right now
|
||||
# verifying -> awaiting_qa (PR must already be open);
|
||||
# on a bounced task, name every open ledger finding
|
||||
# via resolved_findings=[{finding_id, commit?, note?}]
|
||||
i_am_blocked(task_id, reason) # external dependency; cell PM unblocks
|
||||
unclaim(task_id) # release a claimed task back to the queue
|
||||
resume(task_id) # recover a paused task after compact/restart
|
||||
i_am_idle() # no work in your queue right now
|
||||
```
|
||||
|
||||
There is no separate claim / start / pause verb — `i_will_work_on` composes claim + set-plan + start atomically, and `i_am_done` composes verify + submit-qa. Branches are auto-created on `i_will_work_on`; do not checkout by hand — every root task branches from the project's env-ladder **head rung**, not a hardcoded `default_branch`/`master` string (see `CLAUDE.md` "Env-branches ladder"; a project with no declared ladder resolves this identically to its `default_branch`, so nothing changes unless the project opted in).
|
||||
@@ -48,11 +48,11 @@ The callable MCP tool names are `pass` / `fail` (`pass`/`fail` are reserved word
|
||||
## Documenter flow
|
||||
|
||||
```python
|
||||
give_me_work() # returns an awaiting_documentation task
|
||||
claim_doc_task(task_id) # claim the doc phase
|
||||
commit(message, files) # commit the doc files you write
|
||||
give_me_work() # returns an awaiting_documentation task
|
||||
claim_doc_task(task_id) # claim the doc phase
|
||||
commit(message, files) # commit the doc files you write
|
||||
i_documented(task_id, notes, files)
|
||||
# awaiting_documentation -> awaiting_pm_review
|
||||
# awaiting_documentation -> awaiting_pm_review
|
||||
```
|
||||
|
||||
Documentation tasks are **not** delegated — the lifecycle auto-creates the doc phase after a code task passes QA.
|
||||
@@ -60,33 +60,42 @@ Documentation tasks are **not** delegated — the lifecycle auto-creates the doc
|
||||
## Cell PM flow
|
||||
|
||||
```python
|
||||
triage() # list actionable tasks in your cell
|
||||
triage() # list actionable tasks in your cell
|
||||
i_will_plan(task_id, plan, approach)
|
||||
# claim + plan + start a parent task
|
||||
delegate(parent_task_id, title, description, assigned_to, team, task_type,
|
||||
nature, estimated_complexity, acceptance_criteria,
|
||||
covers_parent_criteria=[...])
|
||||
# create a subtask; covers_parent_criteria maps
|
||||
# it to the parent ACs it is responsible for —
|
||||
# REQUIRED whenever the parent has any acceptance
|
||||
# criteria (a ref that matches neither an AC id
|
||||
# nor exact text is rejected, naming the valid
|
||||
# criteria); omit only when the parent has none
|
||||
# claim + plan + start a parent task
|
||||
delegate(
|
||||
parent_task_id,
|
||||
title,
|
||||
description,
|
||||
assigned_to,
|
||||
team,
|
||||
task_type,
|
||||
nature,
|
||||
estimated_complexity,
|
||||
acceptance_criteria,
|
||||
covers_parent_criteria=[...],
|
||||
)
|
||||
# create a subtask; covers_parent_criteria maps
|
||||
# it to the parent ACs it is responsible for —
|
||||
# REQUIRED whenever the parent has any acceptance
|
||||
# criteria (a ref that matches neither an AC id
|
||||
# nor exact text is rejected, naming the valid
|
||||
# criteria); omit only when the parent has none
|
||||
reassign(task_id, assigned_to) # move a subtask to a different agent
|
||||
unblock(task_id, reason) # blocked -> in_progress (PM only); reason is
|
||||
# recorded as your journal:decision (no separate
|
||||
# note needed)
|
||||
unblock(task_id, reason) # blocked -> in_progress (PM only); reason is
|
||||
# recorded as your journal:decision (no separate
|
||||
# note needed)
|
||||
submit_up(task_id, notes, resolved_findings=None)
|
||||
# open cell->root PR; -> awaiting_pr_review
|
||||
# (the cell PR reviewer gates it; after pr_pass
|
||||
# the same Cell PM completes + merges); a re-submit
|
||||
# after pr_fail must resolve every open finding first
|
||||
complete(task_id, notes) # awaiting_pm_review -> completed (merges leaf PR)
|
||||
# open cell->root PR; -> awaiting_pr_review
|
||||
# (the cell PR reviewer gates it; after pr_pass
|
||||
# the same Cell PM completes + merges); a re-submit
|
||||
# after pr_fail must resolve every open finding first
|
||||
complete(task_id, notes) # awaiting_pm_review -> completed (merges leaf PR)
|
||||
request_changes(task_id, findings=[...])
|
||||
# reject a subtask's merge review -> needs_revision,
|
||||
# routed to whoever owns the revision; structured
|
||||
# findings persist to the ledger + render into pm_notes
|
||||
escalate_up(task_id, reason) # escalate to your escalation target
|
||||
# reject a subtask's merge review -> needs_revision,
|
||||
# routed to whoever owns the revision; structured
|
||||
# findings persist to the ledger + render into pm_notes
|
||||
escalate_up(task_id, reason) # escalate to your escalation target
|
||||
```
|
||||
|
||||
After `i_will_plan` and each `delegate`, the envelope includes a coverage view of the parent — `parent_ac_coverage` (per-criterion `id` / `text` / `claimed` / `verified`) and `unclaimed_parent_acs` (criteria no subtask covers yet). A parent cannot idle with unclaimed criteria, nor `complete` / `submit_up` / `escalate_to_ceo` until every criterion traces to a child that passed QA. `delegate` refusing a child with no `covers_parent_criteria` (above) is what puts every parent with acceptance criteria under this coverage discipline from its first subtask on — a decomposition can no longer opt out by never declaring. A rejection now includes a copy-pasteable corrected `delegate(...)` skeleton with the parent's real criteria inlined (an id when the parent has one, its exact quoted text otherwise) — retry with that shape verbatim rather than re-deriving the field's syntax. `i_will_plan`'s planning briefing also carries `collision_context` (in `context_briefing`, not `evidence`) surfacing any same-parent siblings that already collide on file globs or migrations, so you can sequence your delegation before you commit to it. See `docs/rag/workflows/task-planning.md`.
|
||||
@@ -98,15 +107,15 @@ After `i_will_plan` and each `delegate`, the envelope includes a coverage view o
|
||||
The Main PM shares most Cell PM verbs (`i_will_plan`, `delegate`, `complete`, `request_changes`, `unblock`, `triage`, `escalate_up`), **adds** the verbs below, and — unlike a Cell PM — has **no** `submit_up` or `reassign`. Its bubble-up verb is `submit_root` (the root analogue of the Cell PM's `submit_up`):
|
||||
|
||||
```python
|
||||
triage_all() # list actionable tasks across all teams
|
||||
triage_all() # list actionable tasks across all teams
|
||||
submit_root(task_id, notes, resolved_findings=None)
|
||||
# open root->master PR; -> awaiting_pr_review
|
||||
# (the main PR reviewer gates it; after pr_pass,
|
||||
# complete escalates to the CEO); a re-submit
|
||||
# after pr_fail must resolve every open finding first
|
||||
# open root->master PR; -> awaiting_pr_review
|
||||
# (the main PR reviewer gates it; after pr_pass,
|
||||
# complete escalates to the CEO); a re-submit
|
||||
# after pr_fail must resolve every open finding first
|
||||
escalate_to_ceo(task_id, reason)
|
||||
# awaiting_pm_review -> awaiting_ceo_approval
|
||||
give_me_work() # Main PM may also pull work directly
|
||||
# awaiting_pm_review -> awaiting_ceo_approval
|
||||
give_me_work() # Main PM may also pull work directly
|
||||
```
|
||||
|
||||
For a code root the Main PM **must** `submit_root` first — that opens the root→master PR and enters the in-path gate (`awaiting_pr_review`); only after the main reviewer `pr_pass`es it does `complete` escalate to the CEO. A branchless coordination root (product fan-out, no repo) skips the gate and is completed/escalated directly. The Main PM never merges to `master` — `complete` escalates and only the CEO merges the root→master PR.
|
||||
@@ -114,7 +123,7 @@ For a code root the Main PM **must** `submit_root` first — that opens the root
|
||||
## Board flow (Product Owner / Head of Marketing)
|
||||
|
||||
```python
|
||||
triage() # list actionable tasks in scope
|
||||
triage() # list actionable tasks in scope
|
||||
escalate_to_ceo(task_id, reason)
|
||||
i_am_idle()
|
||||
```
|
||||
@@ -126,7 +135,7 @@ The Product Owner additionally has `propose_roadmap(cycle_goal, items)` — a **
|
||||
## Auditor flow
|
||||
|
||||
```python
|
||||
triage() # read-only list of actionable tasks
|
||||
triage() # read-only list of actionable tasks
|
||||
i_am_idle()
|
||||
```
|
||||
|
||||
@@ -135,10 +144,10 @@ The Auditor is a silent observer: read-only `triage`, no `notify`, no claim/comp
|
||||
## PR Reviewer flow
|
||||
|
||||
```python
|
||||
give_me_work() # returns an inbound-PR review task
|
||||
claim_pr_review(task_id) # claim it (planless, branchless — read-only)
|
||||
post_pr_review(task_id, ...) # posts one change-request on the PR; task -> completed
|
||||
unclaim(task_id) # release a claimed inbound or gate review back to the pool
|
||||
give_me_work() # returns an inbound-PR review task
|
||||
claim_pr_review(task_id) # claim it (planless, branchless — read-only)
|
||||
post_pr_review(task_id, ...) # posts one change-request on the PR; task -> completed
|
||||
unclaim(task_id) # release a claimed inbound or gate review back to the pool
|
||||
i_am_idle()
|
||||
```
|
||||
|
||||
@@ -147,13 +156,13 @@ The PR Reviewer reviews inbound external/fork (and, behind a flag, internal) PRs
|
||||
The same role also runs the **in-path PR-review gate** on the org's own assembled delivery PRs — the merge-level review before the PM merges:
|
||||
|
||||
```python
|
||||
claim_gate_review(task_id) # claim an awaiting_pr_review task; returns the assembled
|
||||
# diff + collision_context (colliding siblings, if any) +
|
||||
# (on round >=2) prior_findings, the full ledger
|
||||
pr_pass(task_id, notes) # assembled PR is correct -> awaiting_pm_review (the PM merges)
|
||||
claim_gate_review(task_id) # claim an awaiting_pr_review task; returns the assembled
|
||||
# diff + collision_context (colliding siblings, if any) +
|
||||
# (on round >=2) prior_findings, the full ledger
|
||||
pr_pass(task_id, notes) # assembled PR is correct -> awaiting_pm_review (the PM merges)
|
||||
pr_fail(task_id, findings=[...])
|
||||
# send it back -> needs_revision, like a QA fail;
|
||||
# the deprecated issues=[str] shim still works this release
|
||||
# send it back -> needs_revision, like a QA fail;
|
||||
# the deprecated issues=[str] shim still works this release
|
||||
```
|
||||
|
||||
Both verdicts are also posted on the assembled PR itself as a review (server-side, bot account) so the decision is visible on the PR the PM merges: `pr_pass` → APPROVE, `pr_fail` → REQUEST_CHANGES — except the root→master PR, which only ever gets a plain COMMENT (only the CEO acts on `master`). On a GitLab-backed project `pr_fail` posts as a plain MR note instead (GitLab has no request-changes review primitive) — the task still goes to `needs_revision` normally regardless of forge.
|
||||
|
||||
@@ -94,13 +94,15 @@ Fix all issues before submitting.
|
||||
**Solution**: Use `roboco_docs_write()` - system handles paths automatically
|
||||
|
||||
```python
|
||||
roboco_docs_write({
|
||||
"task_id": "your-task-uuid",
|
||||
"filename": "feature.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "Feature Documentation",
|
||||
"content": "..."
|
||||
})
|
||||
roboco_docs_write(
|
||||
{
|
||||
"task_id": "your-task-uuid",
|
||||
"filename": "feature.md",
|
||||
"doc_type": "api", # api, qa, guide, readme, changelog, architecture, design
|
||||
"title": "Feature Documentation",
|
||||
"content": "...",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
- Team folder: Determined from your agent ID
|
||||
|
||||
@@ -36,7 +36,7 @@ CEO-initiated conversations may arrive and are replied to in-thread like any oth
|
||||
Your claim briefing surfaces incoming A2A under `unread_a2a` — each entry shows the sender and a preview of their latest message. To read the full bodies (and clear them):
|
||||
|
||||
```python
|
||||
read_a2a() # -> {"messages": [{from_agent, content, created_at}, ...]}
|
||||
read_a2a() # -> {"messages": [{from_agent, content, created_at}, ...]}
|
||||
```
|
||||
|
||||
`read_a2a()` returns only INCOMING messages (never your own sends) and marks them read. It also clears `i_am_idle()`'s unread-A2A soft-block.
|
||||
|
||||
@@ -25,9 +25,9 @@ It searches ALL knowledge sources and supports follow-up questions.
|
||||
```python
|
||||
roboco_kb_search(
|
||||
query="rate limiting redis implementation",
|
||||
top_k=5, # Results to return
|
||||
project="roboco-api", # Optional project filter
|
||||
index_types=["code", "docs"] # Filter by type
|
||||
top_k=5, # Results to return
|
||||
project="roboco-api", # Optional project filter
|
||||
index_types=["code", "docs"], # Filter by type
|
||||
)
|
||||
```
|
||||
|
||||
@@ -36,10 +36,7 @@ Returns similar content - not just keyword matches.
|
||||
## RAG Query (AI Answer)
|
||||
|
||||
```python
|
||||
roboco_rag_query(
|
||||
query="How does authentication work in this codebase?",
|
||||
top_k=5
|
||||
)
|
||||
roboco_rag_query(query="How does authentication work in this codebase?", top_k=5)
|
||||
```
|
||||
|
||||
Returns AI-synthesized answer with citations.
|
||||
@@ -54,14 +51,12 @@ Good for:
|
||||
```python
|
||||
# First question
|
||||
response = roboco_ask_mentor(
|
||||
question="How do I handle authentication?",
|
||||
domain="coding"
|
||||
question="How do I handle authentication?", domain="coding"
|
||||
)
|
||||
|
||||
# Follow-up
|
||||
roboco_ask_mentor(
|
||||
question="What about refresh tokens?",
|
||||
conversation_id=response["conversation_id"]
|
||||
question="What about refresh tokens?", conversation_id=response["conversation_id"]
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ You do **not** call any tool to create a PR. There is no `roboco_git_create_pr`
|
||||
|
||||
```python
|
||||
# 1. Make commits as you work (auto-pushes, no separate push step)
|
||||
commit(message="feat(api): add Redis rate limiter",
|
||||
files=["roboco/api/routes/rate.py", "tests/integration/test_rate.py"])
|
||||
commit(
|
||||
message="feat(api): add Redis rate limiter",
|
||||
files=["roboco/api/routes/rate.py", "tests/integration/test_rate.py"],
|
||||
)
|
||||
|
||||
# 2. Once acceptance criteria are implemented + tested, hand off to QA.
|
||||
# The choreographer opens the PR here, sets pr_number/pr_url on the
|
||||
|
||||
@@ -27,10 +27,12 @@ roboco_git_log(branch="<dev's branch>")
|
||||
# Frontend: pnpm test && pnpm lint && pnpm typecheck
|
||||
|
||||
# 5. Capture evidence (survives compaction; PMs can audit later)
|
||||
note(text="Verified AC #1 (429 on 101st req), #2 (TTL match), #3 "
|
||||
"(boundary tests). pytest 1635 passed; ruff clean; mypy clean.",
|
||||
scope="evidence",
|
||||
task_id="<task>")
|
||||
note(
|
||||
text="Verified AC #1 (429 on 101st req), #2 (TTL match), #3 "
|
||||
"(boundary tests). pytest 1635 passed; ruff clean; mypy clean.",
|
||||
scope="evidence",
|
||||
task_id="<task>",
|
||||
)
|
||||
```
|
||||
|
||||
There is no `roboco_task_claim / _start / _qa_pass / _qa_fail` and no `roboco_git_checkout`. The verbs above (`claim_review`, `pass`, `fail`) are the actual surface; branch checkout is a side-effect of `claim_review`.
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
give_me_work()
|
||||
|
||||
# 2. Claim it. The claim verb is role-specific:
|
||||
i_will_work_on(task_id) # Developer — claims + auto-creates the branch
|
||||
claim_review(task_id) # QA — claims + auto-checks-out the dev's branch
|
||||
claim_doc_task(task_id) # Documenter
|
||||
i_will_work_on(task_id) # Developer — claims + auto-creates the branch
|
||||
claim_review(task_id) # QA — claims + auto-checks-out the dev's branch
|
||||
claim_doc_task(task_id) # Documenter
|
||||
|
||||
# Result:
|
||||
# - status: claimed (then in_progress)
|
||||
|
||||
+25
-21
@@ -136,6 +136,7 @@ OrchestratorDep = Annotated[AgentOrchestrator, Depends(get_orchestrator)]
|
||||
|
||||
|
||||
async def get_current_agent_id(
|
||||
*,
|
||||
db: DbSession,
|
||||
response: Response,
|
||||
x_agent_id: Annotated[str | None, Header()] = None,
|
||||
@@ -150,13 +151,13 @@ async def get_current_agent_id(
|
||||
spoof and is rejected (see _cloud_auth_agent_context)."""
|
||||
if settings.cloud_auth_enabled:
|
||||
ctx = await _cloud_auth_agent_context(
|
||||
db,
|
||||
response,
|
||||
x_agent_id,
|
||||
x_agent_role,
|
||||
x_agent_team,
|
||||
x_agent_token,
|
||||
roboco_session,
|
||||
db=db,
|
||||
response=response,
|
||||
x_agent_id=x_agent_id,
|
||||
x_agent_role=x_agent_role,
|
||||
x_agent_team=x_agent_team,
|
||||
x_agent_token=x_agent_token,
|
||||
session_cookie=roboco_session,
|
||||
)
|
||||
return ctx.agent_id
|
||||
if not x_agent_id:
|
||||
@@ -172,6 +173,7 @@ CurrentAgentId = Annotated[UUID, Depends(get_current_agent_id)]
|
||||
|
||||
|
||||
async def get_current_agent_slug(
|
||||
*,
|
||||
db: DbSession,
|
||||
response: Response,
|
||||
x_agent_id: Annotated[str | None, Header()] = None,
|
||||
@@ -185,13 +187,13 @@ async def get_current_agent_slug(
|
||||
slug; a CEO cookie resolves to 'ceo')."""
|
||||
if settings.cloud_auth_enabled:
|
||||
ctx = await _cloud_auth_agent_context(
|
||||
db,
|
||||
response,
|
||||
x_agent_id,
|
||||
x_agent_role,
|
||||
x_agent_team,
|
||||
x_agent_token,
|
||||
roboco_session,
|
||||
db=db,
|
||||
response=response,
|
||||
x_agent_id=x_agent_id,
|
||||
x_agent_role=x_agent_role,
|
||||
x_agent_team=x_agent_team,
|
||||
x_agent_token=x_agent_token,
|
||||
session_cookie=roboco_session,
|
||||
)
|
||||
assert ctx.slug is not None # cloud-auth ctx always carries a slug
|
||||
return ctx.slug
|
||||
@@ -483,6 +485,7 @@ def _should_remint(token: str) -> bool:
|
||||
|
||||
|
||||
async def _cloud_auth_agent_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
response: Response,
|
||||
x_agent_id: str | None,
|
||||
@@ -545,6 +548,7 @@ async def _cloud_auth_agent_context(
|
||||
|
||||
|
||||
async def get_agent_context(
|
||||
*,
|
||||
db: DbSession,
|
||||
response: Response,
|
||||
x_agent_id: Annotated[str | None, Header()] = None,
|
||||
@@ -574,13 +578,13 @@ async def get_agent_context(
|
||||
db, x_agent_id, x_agent_role, x_agent_team, x_agent_token
|
||||
)
|
||||
return await _cloud_auth_agent_context(
|
||||
db,
|
||||
response,
|
||||
x_agent_id,
|
||||
x_agent_role,
|
||||
x_agent_team,
|
||||
x_agent_token,
|
||||
roboco_session,
|
||||
db=db,
|
||||
response=response,
|
||||
x_agent_id=x_agent_id,
|
||||
x_agent_role=x_agent_role,
|
||||
x_agent_team=x_agent_team,
|
||||
x_agent_token=x_agent_token,
|
||||
session_cookie=roboco_session,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -418,6 +418,7 @@ async def get_git_diff(
|
||||
|
||||
@router.get("/file", response_model=GitFileContentResponse)
|
||||
async def get_git_file(
|
||||
*,
|
||||
db: DbSession,
|
||||
agent: CurrentAgentContext,
|
||||
branch: str = Query(..., description="Task branch holding the file"),
|
||||
|
||||
@@ -728,6 +728,7 @@ async def list_tasks(
|
||||
|
||||
@router.get("/summary", response_model=list[TaskSummaryResponse])
|
||||
async def list_tasks_summary(
|
||||
*,
|
||||
db: DbSession,
|
||||
agent: CurrentAgentContext,
|
||||
team: Team | None = None,
|
||||
|
||||
@@ -9,10 +9,13 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Annotated, Any, cast
|
||||
|
||||
import structlog
|
||||
from fastapi import Depends, Header, HTTPException, params, status
|
||||
|
||||
from roboco.foundation.identity import Role
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import Request
|
||||
|
||||
@@ -108,4 +111,32 @@ def envelope_to_response(env: Envelope, request: Request) -> dict[str, Any]:
|
||||
cid = getattr(request.state, "correlation_id", None)
|
||||
if cid is not None and env.correlation_id is None:
|
||||
env.correlation_id = cid
|
||||
return env.as_dict()
|
||||
payload = env.as_dict()
|
||||
_log_rejection(payload, request)
|
||||
return payload
|
||||
|
||||
|
||||
def _log_rejection(payload: dict[str, Any], request: Request) -> None:
|
||||
"""Log a rejected envelope's reason at the single wire chokepoint.
|
||||
|
||||
Rejections used to leave NO server-side trace: the access log records
|
||||
``POST /api/v1/do/<verb> 200`` (an error envelope is still a 200), the
|
||||
envelope body is never logged, and there is no trace table — so a verb
|
||||
an agent could not satisfy was indistinguishable in the logs from one
|
||||
that succeeded. Four Board Programs died that way on 2026-07-25 and the
|
||||
reason was unrecoverable after the fact.
|
||||
"""
|
||||
error = payload.get("error")
|
||||
if not error:
|
||||
return
|
||||
logger.warning(
|
||||
"verb rejected",
|
||||
verb=request.url.path.rsplit("/", 1)[-1],
|
||||
error=error,
|
||||
detail=payload.get("message"),
|
||||
remediate=payload.get("remediate"),
|
||||
missing=payload.get("missing"),
|
||||
agent_id=request.headers.get("X-Agent-ID"),
|
||||
agent_role=request.headers.get("X-Agent-Role"),
|
||||
task_id=payload.get("task_id"),
|
||||
)
|
||||
|
||||
@@ -62,9 +62,9 @@ async def i_will_work_on(
|
||||
choreographer: _ChoreographerDep,
|
||||
) -> dict:
|
||||
env = await choreographer.i_will_work_on(
|
||||
x_agent_id,
|
||||
body.task_id,
|
||||
body.plan,
|
||||
agent_id=x_agent_id,
|
||||
task_id=body.task_id,
|
||||
plan=body.plan,
|
||||
steps=body.steps,
|
||||
technical_considerations=body.technical_considerations,
|
||||
risks=body.risks,
|
||||
|
||||
@@ -434,6 +434,7 @@ def _fast_forward_branch(stack: E2EStack, branch: str, *, onto: str) -> None:
|
||||
|
||||
|
||||
def _create_bench_task(
|
||||
*,
|
||||
stack: E2EStack,
|
||||
project_id: UUID,
|
||||
dev_slug: str,
|
||||
@@ -1001,7 +1002,12 @@ class EvalRunner:
|
||||
_fast_forward_branch(env.stack, env.cell_branch, onto="master")
|
||||
started_at = datetime.now(UTC)
|
||||
task_id = _create_bench_task(
|
||||
env.stack, env.project_id, dev_slug, fixture, env.team, env.cell_id
|
||||
stack=env.stack,
|
||||
project_id=env.project_id,
|
||||
dev_slug=dev_slug,
|
||||
fixture=fixture,
|
||||
team=env.team,
|
||||
parent_task_id=env.cell_id,
|
||||
)
|
||||
|
||||
spawner = self._make_spawner(env.stack)
|
||||
|
||||
@@ -466,6 +466,7 @@ def commit(message: str, files: list[str] | None = None) -> dict[str, Any]:
|
||||
|
||||
|
||||
def note(
|
||||
*,
|
||||
text: str,
|
||||
scope: str = "note",
|
||||
task_id: str | None = None,
|
||||
@@ -691,6 +692,7 @@ def propose_friction_fixes(items: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def propose_feature_spotlight(
|
||||
*,
|
||||
feature_slug: str = "",
|
||||
feature_title: str = "",
|
||||
body: str = "",
|
||||
|
||||
@@ -538,6 +538,7 @@ def give_me_work() -> dict[str, Any]:
|
||||
|
||||
|
||||
def i_will_work_on(
|
||||
*,
|
||||
task_id: str,
|
||||
plan: str | None = None,
|
||||
steps: list[dict[str, str]] | None = None,
|
||||
@@ -909,6 +910,7 @@ def waive_finding(finding_id: str, note: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
def i_will_plan(
|
||||
*,
|
||||
task_id: str,
|
||||
plan: str,
|
||||
approach: str = "",
|
||||
@@ -949,6 +951,7 @@ def i_will_plan(
|
||||
|
||||
|
||||
def delegate(
|
||||
*,
|
||||
parent_task_id: str,
|
||||
title: str,
|
||||
description: str,
|
||||
|
||||
+66
-64
@@ -95,67 +95,67 @@ in_progress ──► awaiting_pr_review
|
||||
|
||||
### TaskStatus (from `base.py`)
|
||||
```python
|
||||
BACKLOG = "backlog" # PM setup phase
|
||||
PENDING = "pending" # Ready for work
|
||||
CLAIMED = "claimed" # Agent claimed, not started
|
||||
IN_PROGRESS = "in_progress" # Active work
|
||||
BLOCKED = "blocked" # External blocker
|
||||
PAUSED = "paused" # Temporary pause
|
||||
VERIFYING = "verifying" # Self-verification
|
||||
NEEDS_REVISION = "needs_revision" # QA/PM requested changes
|
||||
AWAITING_QA = "awaiting_qa" # Ready for QA review
|
||||
BACKLOG = "backlog" # PM setup phase
|
||||
PENDING = "pending" # Ready for work
|
||||
CLAIMED = "claimed" # Agent claimed, not started
|
||||
IN_PROGRESS = "in_progress" # Active work
|
||||
BLOCKED = "blocked" # External blocker
|
||||
PAUSED = "paused" # Temporary pause
|
||||
VERIFYING = "verifying" # Self-verification
|
||||
NEEDS_REVISION = "needs_revision" # QA/PM requested changes
|
||||
AWAITING_QA = "awaiting_qa" # Ready for QA review
|
||||
AWAITING_DOCUMENTATION = "awaiting_documentation" # Ready for docs
|
||||
AWAITING_PM_REVIEW = "awaiting_pm_review" # Ready for PM review
|
||||
AWAITING_CEO_APPROVAL = "awaiting_ceo_approval" # Major task, CEO decides
|
||||
COMPLETED = "completed" # Done
|
||||
CANCELLED = "cancelled" # Cancelled
|
||||
AWAITING_PM_REVIEW = "awaiting_pm_review" # Ready for PM review
|
||||
AWAITING_CEO_APPROVAL = "awaiting_ceo_approval" # Major task, CEO decides
|
||||
COMPLETED = "completed" # Done
|
||||
CANCELLED = "cancelled" # Cancelled
|
||||
```
|
||||
|
||||
### AgentRole
|
||||
```python
|
||||
CEO = "ceo" # Executive (Human)
|
||||
PRODUCT_OWNER = "product_owner" # Board
|
||||
HEAD_MARKETING = "head_marketing" # Board
|
||||
AUDITOR = "auditor" # Board (Silent observer)
|
||||
MAIN_PM = "main_pm" # Management (coordinates all cells)
|
||||
CELL_PM = "cell_pm" # Cell management
|
||||
DEVELOPER = "developer" # Cell member
|
||||
QA = "qa" # Cell member
|
||||
DOCUMENTER = "documenter" # Cell member
|
||||
CEO = "ceo" # Executive (Human)
|
||||
PRODUCT_OWNER = "product_owner" # Board
|
||||
HEAD_MARKETING = "head_marketing" # Board
|
||||
AUDITOR = "auditor" # Board (Silent observer)
|
||||
MAIN_PM = "main_pm" # Management (coordinates all cells)
|
||||
CELL_PM = "cell_pm" # Cell management
|
||||
DEVELOPER = "developer" # Cell member
|
||||
QA = "qa" # Cell member
|
||||
DOCUMENTER = "documenter" # Cell member
|
||||
```
|
||||
|
||||
### Team
|
||||
```python
|
||||
BACKEND = "backend" # Backend cell
|
||||
FRONTEND = "frontend" # Frontend cell
|
||||
UX_UI = "ux_ui" # UX/UI cell
|
||||
BOARD = "board" # Board level (no cell)
|
||||
BACKEND = "backend" # Backend cell
|
||||
FRONTEND = "frontend" # Frontend cell
|
||||
UX_UI = "ux_ui" # UX/UI cell
|
||||
BOARD = "board" # Board level (no cell)
|
||||
```
|
||||
|
||||
### WorkSessionStatus (from `work_session.py`)
|
||||
```python
|
||||
ACTIVE = "active" # Work in progress
|
||||
COMPLETED = "completed" # PR merged
|
||||
ABANDONED = "abandoned" # Session cancelled
|
||||
ACTIVE = "active" # Work in progress
|
||||
COMPLETED = "completed" # PR merged
|
||||
ABANDONED = "abandoned" # Session cancelled
|
||||
```
|
||||
|
||||
### TaskType (from `base.py`)
|
||||
```python
|
||||
CODE = "code" # Source code changes
|
||||
DOCUMENTATION = "documentation" # Documentation updates
|
||||
RESEARCH = "research" # Research findings committed as notes
|
||||
PLANNING = "planning" # Plans/architecture committed as docs
|
||||
DESIGN = "design" # Designs/specs committed as assets
|
||||
ADMINISTRATIVE = "administrative" # Process docs committed
|
||||
CODE = "code" # Source code changes
|
||||
DOCUMENTATION = "documentation" # Documentation updates
|
||||
RESEARCH = "research" # Research findings committed as notes
|
||||
PLANNING = "planning" # Plans/architecture committed as docs
|
||||
DESIGN = "design" # Designs/specs committed as assets
|
||||
ADMINISTRATIVE = "administrative" # Process docs committed
|
||||
```
|
||||
|
||||
### BranchReason (from `project.py`)
|
||||
```python
|
||||
FEATURE = "feature" # New functionality
|
||||
BUG = "bug" # Bug fixes
|
||||
CHORE = "chore" # Maintenance
|
||||
DOCS = "docs" # Documentation
|
||||
HOTFIX = "hotfix" # Emergency fixes
|
||||
FEATURE = "feature" # New functionality
|
||||
BUG = "bug" # Bug fixes
|
||||
CHORE = "chore" # Maintenance
|
||||
DOCS = "docs" # Documentation
|
||||
HOTFIX = "hotfix" # Emergency fixes
|
||||
```
|
||||
|
||||
## Key Models
|
||||
@@ -173,7 +173,9 @@ class Task:
|
||||
assigned_to: UUID | None
|
||||
|
||||
# Task Type
|
||||
task_type: TaskType # code, documentation, research, planning, design, administrative
|
||||
task_type: (
|
||||
TaskType # code, documentation, research, planning, design, administrative
|
||||
)
|
||||
|
||||
# Project & Branch (all tasks follow git workflow, branch auto-created on claim)
|
||||
project_id: UUID
|
||||
@@ -181,33 +183,33 @@ class Task:
|
||||
work_session_id: UUID | None
|
||||
|
||||
# PR Tracking (set during AWAITING_DOCUMENTATION parallel phase)
|
||||
pr_number: int | None # GitHub/GitLab PR number
|
||||
pr_url: str | None # Full URL to PR
|
||||
pr_number: int | None # GitHub/GitLab PR number
|
||||
pr_url: str | None # Full URL to PR
|
||||
|
||||
# Parallel Execution Tracking (for AWAITING_DOCUMENTATION phase)
|
||||
docs_complete: bool # Documenter has finished
|
||||
pr_created: bool # Developer has created PR
|
||||
docs_complete: bool # Documenter has finished
|
||||
pr_created: bool # Developer has created PR
|
||||
|
||||
# PM Approval Tracking
|
||||
pm_approvals: dict[str, bool] # {'main_pm': True, 'cell_pm': True}
|
||||
pm_approvals: dict[str, bool] # {'main_pm': True, 'cell_pm': True}
|
||||
|
||||
# Planning
|
||||
plan: TaskPlan | None
|
||||
estimated_complexity: Complexity
|
||||
|
||||
# Execution tracking
|
||||
commits: list[CommitRef] # Linked git commits
|
||||
checkpoints: list[Checkpoint] # Recovery points
|
||||
commits: list[CommitRef] # Linked git commits
|
||||
checkpoints: list[Checkpoint] # Recovery points
|
||||
progress_updates: list[ProgressUpdate]
|
||||
|
||||
# Documentation Notes
|
||||
dev_notes: str | None # Journey notes from developer
|
||||
qa_notes: str | None # QA feedback
|
||||
auditor_notes: str | None # Auditor observations
|
||||
quick_context: str | None # 2-3 sentences for quick context restoration
|
||||
dev_notes: str | None # Journey notes from developer
|
||||
qa_notes: str | None # QA feedback
|
||||
auditor_notes: str | None # Auditor observations
|
||||
quick_context: str | None # 2-3 sentences for quick context restoration
|
||||
|
||||
# Proactive Knowledge Context (injected when task is claimed)
|
||||
proactive_context: dict | None # RAG context: similar tasks, learnings, patterns
|
||||
proactive_context: dict | None # RAG context: similar tasks, learnings, patterns
|
||||
```
|
||||
|
||||
### Project (`project.py`)
|
||||
@@ -215,24 +217,24 @@ class Task:
|
||||
class Project:
|
||||
id: UUID
|
||||
name: str
|
||||
slug: str # URL-safe identifier (e.g., 'roboco', 'roboco-panel')
|
||||
git_url: str # Git repository URL
|
||||
default_branch: str # e.g., "main"
|
||||
slug: str # URL-safe identifier (e.g., 'roboco', 'roboco-panel')
|
||||
git_url: str # Git repository URL
|
||||
default_branch: str # e.g., "main"
|
||||
protected_branches: list[str] # Cannot push directly
|
||||
|
||||
# CI/CD commands
|
||||
test_command: str | None # e.g., 'uv run pytest'
|
||||
lint_command: str | None # e.g., 'uv run ruff check .'
|
||||
format_command: str | None # e.g., 'uv run ruff format .'
|
||||
test_command: str | None # e.g., 'uv run pytest'
|
||||
lint_command: str | None # e.g., 'uv run ruff check .'
|
||||
format_command: str | None # e.g., 'uv run ruff format .'
|
||||
typecheck_command: str | None # e.g., 'uv run mypy src/'
|
||||
build_command: str | None # e.g., 'pnpm build'
|
||||
build_command: str | None # e.g., 'pnpm build'
|
||||
|
||||
# Access control
|
||||
assigned_cell: Team
|
||||
allowed_agents: list[UUID] | None # None = all agents in cell
|
||||
|
||||
# Runtime State (managed by workspace service)
|
||||
workspace_path: str | None # Legacy: now use WorkspaceService
|
||||
workspace_path: str | None # Legacy: now use WorkspaceService
|
||||
last_synced_at: datetime | None
|
||||
head_commit: str | None
|
||||
|
||||
@@ -255,13 +257,13 @@ class WorkSession:
|
||||
target_branch: str
|
||||
|
||||
# Audit trail
|
||||
commits: list[str] # Commit SHAs
|
||||
files_modified: list[str] # Changed files
|
||||
commits: list[str] # Commit SHAs
|
||||
files_modified: list[str] # Changed files
|
||||
|
||||
# PR tracking
|
||||
pr_number: int | None
|
||||
pr_url: str | None
|
||||
pr_status: str | None # open, merged, closed
|
||||
pr_status: str | None # open, merged, closed
|
||||
pr_created_at: datetime | None
|
||||
pr_merged_at: datetime | None
|
||||
merged_by: UUID | None
|
||||
|
||||
@@ -24,7 +24,7 @@ replaces.
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
@@ -253,6 +253,25 @@ _METRIC_PREDICATES: dict[str, Callable[[AsyncSession], Awaitable[bool]]] = {
|
||||
}
|
||||
|
||||
|
||||
def learn_ref(item: dict[str, Any], limit: int = 80) -> str:
|
||||
"""The ``item_ref`` a per-item approve/reject records for LEARN.
|
||||
|
||||
The item's own title, because the ref's only consumer is
|
||||
``_render_cycle``, whose output goes into the NEXT cycle's exploration
|
||||
prompt. Passing the stored ``id`` instead (``item-0``/``item-1``, a
|
||||
per-cycle index — see ``_normalize_roadmap_item``) rendered
|
||||
"rejected: item-1 — <reason>": the CEO's reason survived, but nothing
|
||||
said which proposal it was about, and the index means something
|
||||
different in every cycle. Falls back to the id when a title is missing.
|
||||
|
||||
``target_task_title`` covers Scales, whose items name the live task they
|
||||
mutate rather than carrying a draft title of their own.
|
||||
"""
|
||||
title = str(item.get("title") or item.get("target_task_title") or "").strip()
|
||||
ref = title or str(item.get("id") or "")
|
||||
return ref[:limit].rstrip() if len(ref) > limit else ref
|
||||
|
||||
|
||||
def _legacy_enabled(key: str) -> bool:
|
||||
"""The pre-registry flag(s) each program aliases while both exist."""
|
||||
if key == "roadmap":
|
||||
|
||||
@@ -23,6 +23,7 @@ from roboco.foundation.policy.board_programs import PROGRAMS, project_participat
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import DOGFOOD_ITEM_SOURCE, DOGFOOD_SOURCE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -101,7 +102,7 @@ class DogfoodService(BaseService):
|
||||
item["materialized_task_id"] = str(new_task.id)
|
||||
markers.set_friction_fixes(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return FrictionFixItemResult(
|
||||
status="approved",
|
||||
@@ -140,7 +141,7 @@ class DogfoodService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_friction_fixes(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return FrictionFixItemResult(
|
||||
status="rejected",
|
||||
@@ -230,7 +231,7 @@ class DogfoodService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors ``SpackleService._record_learn``."""
|
||||
@@ -239,7 +240,7 @@ class DogfoodService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"dogfood",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -641,7 +641,7 @@ class GitLabProvider(GitProvider):
|
||||
headers: dict[str, str],
|
||||
client: httpx.AsyncClient | None,
|
||||
timeout: float | None,
|
||||
) -> int | None | httpx.Response | ShapedResponse:
|
||||
) -> int | httpx.Response | ShapedResponse | None:
|
||||
"""The group's numeric id, ``None`` for a personal (404) namespace,
|
||||
or the raw error response for anything else."""
|
||||
group_resp = await self._send(
|
||||
|
||||
@@ -511,6 +511,7 @@ class Choreographer:
|
||||
|
||||
async def _handle_pm_reentry(
|
||||
self,
|
||||
*,
|
||||
ctx: _ClaimPlanStartContext,
|
||||
t: Any,
|
||||
pm_agent_id: UUID,
|
||||
@@ -1707,6 +1708,7 @@ class Choreographer:
|
||||
|
||||
async def i_will_work_on(
|
||||
self,
|
||||
*,
|
||||
agent_id: UUID,
|
||||
task_id: UUID,
|
||||
plan: str | None = None,
|
||||
@@ -1784,24 +1786,30 @@ class Choreographer:
|
||||
verb_name="i_will_work_on",
|
||||
)
|
||||
if reentry := await self._dev_reentry(
|
||||
ctx, t, agent_id, task_id, role_str, briefing
|
||||
ctx=ctx,
|
||||
t=t,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
):
|
||||
return reentry
|
||||
return await self._fresh_dev_claim(
|
||||
ctx,
|
||||
role,
|
||||
spec_ctx,
|
||||
agent,
|
||||
rich_plan,
|
||||
role_str,
|
||||
t,
|
||||
agent_id,
|
||||
task_id,
|
||||
briefing,
|
||||
ctx=ctx,
|
||||
role=role,
|
||||
spec_ctx=spec_ctx,
|
||||
agent=agent,
|
||||
rich_plan=rich_plan,
|
||||
role_str=role_str,
|
||||
t=t,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
briefing=briefing,
|
||||
)
|
||||
|
||||
async def _dev_reentry(
|
||||
self,
|
||||
*,
|
||||
ctx: _ClaimPlanStartContext,
|
||||
t: Any,
|
||||
agent_id: UUID,
|
||||
@@ -1838,6 +1846,7 @@ class Choreographer:
|
||||
|
||||
async def _fresh_dev_claim(
|
||||
self,
|
||||
*,
|
||||
ctx: _ClaimPlanStartContext,
|
||||
role: Any,
|
||||
spec_ctx: Any,
|
||||
@@ -2019,7 +2028,12 @@ class Choreographer:
|
||||
original_developer_slug=_extract_original_developer(t),
|
||||
)
|
||||
if rejection := self._open_pr_preflight_rejection(
|
||||
agent_id, task_id, t, role_str, briefing, spec_ctx
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
spec_ctx=spec_ctx,
|
||||
):
|
||||
return await self._emit_rejection(
|
||||
rejection, agent_id=agent_id, task_id=task_id, verb="open_pr"
|
||||
@@ -2041,6 +2055,7 @@ class Choreographer:
|
||||
|
||||
def _open_pr_preflight_rejection(
|
||||
self,
|
||||
*,
|
||||
agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
@@ -3811,6 +3826,7 @@ class Choreographer:
|
||||
|
||||
async def _run_i_am_blocked_intent(
|
||||
self,
|
||||
*,
|
||||
agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
@@ -3877,6 +3893,7 @@ class Choreographer:
|
||||
|
||||
async def _handle_rate_limited_parking(
|
||||
self,
|
||||
*,
|
||||
agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
@@ -4065,7 +4082,13 @@ class Choreographer:
|
||||
)
|
||||
|
||||
t, rejection = await self._run_i_am_blocked_intent(
|
||||
agent_id, task_id, t, agent, spec_ctx, role_str, briefing
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
t=t,
|
||||
agent=agent,
|
||||
spec_ctx=spec_ctx,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
@@ -4614,7 +4637,12 @@ class Choreographer:
|
||||
# protected-base. Returns the rejection (None when all clear) AND the
|
||||
# resolved base_branch the git op needs (empty when rejected).
|
||||
rejection, base_branch = await self._sync_branch_preflight_rejection(
|
||||
agent_id, task_id, t, agent, role_str, briefing
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
t=t,
|
||||
agent=agent,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
)
|
||||
if rejection is not None:
|
||||
return await self._emit_rejection(
|
||||
@@ -4707,6 +4735,7 @@ class Choreographer:
|
||||
|
||||
async def _sync_branch_preflight_rejection(
|
||||
self,
|
||||
*,
|
||||
agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
@@ -5279,7 +5308,12 @@ class Choreographer:
|
||||
# Re-entry check runs first — a respawned PM with thin args ("resume",
|
||||
# no sub_tasks) must short-circuit here before any gate.
|
||||
if reentry := await self._handle_pm_reentry(
|
||||
ctx, t, pm_agent_id, task_id, role_str, briefing
|
||||
ctx=ctx,
|
||||
t=t,
|
||||
pm_agent_id=pm_agent_id,
|
||||
task_id=task_id,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
):
|
||||
return reentry
|
||||
# Lifecycle spec gate runs BEFORE the sub_tasks gate so wrong-state
|
||||
@@ -7485,7 +7519,12 @@ class Choreographer:
|
||||
# instead of letting the failure re-block the task and
|
||||
# respawn the PM forever.
|
||||
return await self._resolve_merge_conflict_on_complete(
|
||||
pm_agent_id, task_id, t, target, notes, exc
|
||||
pm_agent_id=pm_agent_id,
|
||||
task_id=task_id,
|
||||
t=t,
|
||||
target=target,
|
||||
notes=notes,
|
||||
exc=exc,
|
||||
)
|
||||
return await self._finalize_cell_complete(
|
||||
pm_agent_id, task_id, t, notes, merge_commit
|
||||
@@ -7563,6 +7602,7 @@ class Choreographer:
|
||||
|
||||
async def _resolve_merge_conflict_on_complete(
|
||||
self,
|
||||
*,
|
||||
pm_agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
@@ -8466,7 +8506,13 @@ class Choreographer:
|
||||
# check — the ledger ids don't exist until after insert, below.
|
||||
notes = findings_lib.render_findings_summary([(None, f) for f in validated])
|
||||
rejection, spec_gate = await self._request_changes_spec_gate(
|
||||
pm_agent_id, task_id, t, agent, role_str, notes, []
|
||||
pm_agent_id=pm_agent_id,
|
||||
task_id=task_id,
|
||||
t=t,
|
||||
agent=agent,
|
||||
role_str=role_str,
|
||||
notes=notes,
|
||||
issues=[],
|
||||
)
|
||||
if rejection is not None:
|
||||
return rejection
|
||||
@@ -8510,6 +8556,7 @@ class Choreographer:
|
||||
|
||||
async def _request_changes_spec_gate(
|
||||
self,
|
||||
*,
|
||||
pm_agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
|
||||
@@ -484,18 +484,19 @@ class DocMixin(_Base):
|
||||
return push_rejection
|
||||
|
||||
return await self._finalize_documented(
|
||||
doc_agent_id,
|
||||
task_id,
|
||||
files,
|
||||
owned_task,
|
||||
agent,
|
||||
role_str,
|
||||
spec_ctx,
|
||||
briefing,
|
||||
doc_agent_id=doc_agent_id,
|
||||
task_id=task_id,
|
||||
files=files,
|
||||
owned_task=owned_task,
|
||||
agent=agent,
|
||||
role_str=role_str,
|
||||
spec_ctx=spec_ctx,
|
||||
briefing=briefing,
|
||||
)
|
||||
|
||||
async def _finalize_documented(
|
||||
self,
|
||||
*,
|
||||
doc_agent_id: UUID,
|
||||
task_id: UUID,
|
||||
files: list[str],
|
||||
|
||||
@@ -118,7 +118,12 @@ class PRGateMixin(_Base):
|
||||
role_str = self._role_str_for_agent(agent)
|
||||
briefing = await self._briefing_for(reviewer_agent_id, task_id, full=True)
|
||||
role = await self._gate_role_or_rejection(
|
||||
t, role_str, briefing, reviewer_agent_id, task_id, "claim_gate_review"
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
agent_id=reviewer_agent_id,
|
||||
task_id=task_id,
|
||||
verb="claim_gate_review",
|
||||
)
|
||||
if isinstance(role, Envelope):
|
||||
return role
|
||||
@@ -242,7 +247,12 @@ class PRGateMixin(_Base):
|
||||
role_str = self._role_str_for_agent(agent)
|
||||
briefing = await self._briefing_for(reviewer_agent_id, task_id)
|
||||
role = await self._gate_role_or_rejection(
|
||||
t, role_str, briefing, reviewer_agent_id, task_id, verb
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
agent_id=reviewer_agent_id,
|
||||
task_id=task_id,
|
||||
verb=verb,
|
||||
)
|
||||
if isinstance(role, Envelope):
|
||||
return role
|
||||
@@ -1095,6 +1105,7 @@ class PRGateMixin(_Base):
|
||||
|
||||
async def _gate_role_or_rejection(
|
||||
self,
|
||||
*,
|
||||
t: Any,
|
||||
role_str: str,
|
||||
briefing: dict[str, Any],
|
||||
|
||||
@@ -66,7 +66,12 @@ class PRReviewerMixin(_Base):
|
||||
role_str = str(agent.role) if agent is not None else "pr_reviewer"
|
||||
briefing = await self._briefing_for(reviewer_agent_id, task_id, full=True)
|
||||
role_or_rejection = await self._resolve_role(
|
||||
t, role_str, briefing, reviewer_agent_id, task_id, "claim_pr_review"
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
agent_id=reviewer_agent_id,
|
||||
task_id=task_id,
|
||||
verb="claim_pr_review",
|
||||
)
|
||||
if isinstance(role_or_rejection, Envelope):
|
||||
return role_or_rejection
|
||||
@@ -187,6 +192,7 @@ class PRReviewerMixin(_Base):
|
||||
|
||||
async def _post_review_side_effects(
|
||||
self,
|
||||
*,
|
||||
t: Any,
|
||||
slug: str | None,
|
||||
pr_number: int | None,
|
||||
@@ -280,12 +286,23 @@ class PRReviewerMixin(_Base):
|
||||
t = await runner.run_intent("post_pr_review", t, agent, spec_ctx)
|
||||
except Exception as exc:
|
||||
return await self._runner_failure(
|
||||
exc, t, role_str, briefing, reviewer_agent_id, task_id, "post_pr_review"
|
||||
exc=exc,
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
agent_id=reviewer_agent_id,
|
||||
task_id=task_id,
|
||||
verb="post_pr_review",
|
||||
)
|
||||
# Side-effects AFTER the DB transition (a2a.send pattern), both best-
|
||||
# effort: post the canonical review to GitHub + surface it to the CEO.
|
||||
await self._post_review_side_effects(
|
||||
t, slug, pr_number, post_body, event, task_id
|
||||
t=t,
|
||||
slug=slug,
|
||||
pr_number=pr_number,
|
||||
post_body=post_body,
|
||||
event=event,
|
||||
task_id=task_id,
|
||||
)
|
||||
return Envelope.ok(
|
||||
status=str(t.status),
|
||||
@@ -322,7 +339,12 @@ class PRReviewerMixin(_Base):
|
||||
verb="post_pr_review",
|
||||
)
|
||||
role_or_rejection = await self._resolve_role(
|
||||
t, role_str, briefing, reviewer_agent_id, task_id, "post_pr_review"
|
||||
t=t,
|
||||
role_str=role_str,
|
||||
briefing=briefing,
|
||||
agent_id=reviewer_agent_id,
|
||||
task_id=task_id,
|
||||
verb="post_pr_review",
|
||||
)
|
||||
if isinstance(role_or_rejection, Envelope):
|
||||
return role_or_rejection
|
||||
@@ -451,6 +473,7 @@ class PRReviewerMixin(_Base):
|
||||
|
||||
async def _resolve_role(
|
||||
self,
|
||||
*,
|
||||
t: Any,
|
||||
role_str: str,
|
||||
briefing: dict[str, Any],
|
||||
@@ -475,6 +498,7 @@ class PRReviewerMixin(_Base):
|
||||
|
||||
async def _runner_failure(
|
||||
self,
|
||||
*,
|
||||
exc: Exception,
|
||||
t: Any,
|
||||
role_str: str,
|
||||
|
||||
@@ -216,6 +216,7 @@ def _typed(value: Any, expected: type | tuple[type, ...], default: Any) -> Any:
|
||||
|
||||
|
||||
def _has_prior_work(
|
||||
*,
|
||||
commits: list,
|
||||
acceptance: list,
|
||||
highlights: list,
|
||||
@@ -292,18 +293,18 @@ def build_task_handoff(
|
||||
pm_review = _extract_pm_review(notes_structured)
|
||||
open_findings = list(open_findings or [])
|
||||
if not _has_prior_work(
|
||||
commits,
|
||||
acceptance,
|
||||
highlights,
|
||||
pr_number,
|
||||
dev_summary,
|
||||
completed_deps,
|
||||
pr_review,
|
||||
qa_review,
|
||||
pm_review,
|
||||
open_findings,
|
||||
description,
|
||||
parent_context,
|
||||
commits=commits,
|
||||
acceptance=acceptance,
|
||||
highlights=highlights,
|
||||
pr_number=pr_number,
|
||||
dev_summary=dev_summary,
|
||||
completed_deps=completed_deps,
|
||||
pr_review=pr_review,
|
||||
qa_review=qa_review,
|
||||
pm_review=pm_review,
|
||||
open_findings=open_findings,
|
||||
description=description,
|
||||
parent_context=parent_context,
|
||||
):
|
||||
return None
|
||||
handoff: dict[str, Any] = {
|
||||
|
||||
@@ -24,6 +24,7 @@ from roboco.foundation.policy.board_programs import PROGRAMS, project_participat
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import MIRROR_ITEM_SOURCE, MIRROR_SOURCE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -102,7 +103,7 @@ class MirrorService(BaseService):
|
||||
item["materialized_task_id"] = str(new_task.id)
|
||||
markers.set_messaging_fixes(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return MessagingFixItemResult(
|
||||
status="approved",
|
||||
@@ -141,7 +142,7 @@ class MirrorService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_messaging_fixes(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return MessagingFixItemResult(
|
||||
status="rejected",
|
||||
@@ -231,7 +232,7 @@ class MirrorService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors ``SpackleService._record_learn``."""
|
||||
@@ -240,7 +241,7 @@ class MirrorService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"mirror",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -170,6 +170,7 @@ class NotificationService:
|
||||
|
||||
async def send_oscillation_blocked_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
strikes: int,
|
||||
escalator: str | UUID | None,
|
||||
@@ -422,6 +423,7 @@ class NotificationService:
|
||||
|
||||
async def send_external_pr_reviewed_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
pr_number: int,
|
||||
pr_url: str,
|
||||
@@ -466,6 +468,7 @@ class NotificationService:
|
||||
|
||||
async def send_reassignment_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
previous_assignee: str | None,
|
||||
new_assignee: str | None,
|
||||
@@ -513,6 +516,7 @@ class NotificationService:
|
||||
|
||||
async def send_collision_sequencing_notification(
|
||||
self,
|
||||
*,
|
||||
held_back_task_id: str,
|
||||
blocking_task_id: str,
|
||||
held_back_assignee: str | None,
|
||||
@@ -557,6 +561,7 @@ class NotificationService:
|
||||
|
||||
async def send_unblock_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
restored_owner: str | None,
|
||||
from_agent: str | None = None,
|
||||
@@ -600,6 +605,7 @@ class NotificationService:
|
||||
|
||||
async def send_dependency_revival_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
assignee: str | None,
|
||||
completed_dependency_id: str,
|
||||
@@ -654,6 +660,7 @@ class NotificationService:
|
||||
|
||||
async def send_stale_claim_reaped_notification(
|
||||
self,
|
||||
*,
|
||||
task_id: str,
|
||||
reaped_agent: str | None,
|
||||
last_heartbeat: str | None = None,
|
||||
|
||||
@@ -25,6 +25,7 @@ from roboco.foundation.policy.board_programs import PROGRAMS, project_participat
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import PEST_CONTROL_ITEM_SOURCE, PEST_CONTROL_SOURCE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -103,7 +104,7 @@ class PestControlService(BaseService):
|
||||
item["materialized_task_id"] = str(new_task.id)
|
||||
markers.set_pest_hunt(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return PestHuntItemResult(
|
||||
status="approved",
|
||||
@@ -142,7 +143,7 @@ class PestControlService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_pest_hunt(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return PestHuntItemResult(
|
||||
status="rejected",
|
||||
@@ -231,7 +232,7 @@ class PestControlService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors ``RoadmapService._record_learn``."""
|
||||
@@ -240,7 +241,7 @@ class PestControlService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"pest_control",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -957,6 +957,7 @@ class PrompterService:
|
||||
|
||||
async def _rewrite_batch_children(
|
||||
self,
|
||||
*,
|
||||
umbrella: TaskTable,
|
||||
drafts: list[dict[str, Any]],
|
||||
children: list[TaskTable],
|
||||
@@ -1063,7 +1064,12 @@ class PrompterService:
|
||||
plan = self._sequence_drafts(drafts)
|
||||
wave_of = {idx: w for w, wave in enumerate(plan.waves) for idx in wave}
|
||||
task_of = await self._rewrite_batch_children(
|
||||
umbrella, drafts, children, wave_of, agent_id, agent_role
|
||||
umbrella=umbrella,
|
||||
drafts=drafts,
|
||||
children=children,
|
||||
wave_of=wave_of,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
)
|
||||
for a, b in plan.edges:
|
||||
await task_service.add_dependency(task_of[b], task_of[a])
|
||||
|
||||
@@ -22,6 +22,7 @@ from roboco.foundation.policy.board_programs import PROGRAMS, project_participat
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import ROADMAP_ITEM_SOURCE, ROADMAP_SOURCE, get_task_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -98,7 +99,7 @@ class RoadmapService(BaseService):
|
||||
item["materialized_task_id"] = str(new_task.id)
|
||||
markers.set_roadmap_cycle(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return RoadmapItemResult(
|
||||
status="approved",
|
||||
@@ -137,7 +138,7 @@ class RoadmapService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_roadmap_cycle(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return RoadmapItemResult(
|
||||
status="rejected",
|
||||
@@ -228,7 +229,7 @@ class RoadmapService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors the vault-writer best-effort seams.
|
||||
@@ -242,7 +243,7 @@ class RoadmapService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"roadmap",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -31,6 +31,7 @@ from uuid import UUID
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import SCALES_SOURCE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -107,7 +108,7 @@ class ScalesService(BaseService):
|
||||
item["executed_detail"] = executed_detail
|
||||
markers.set_rebalance_plan(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return RebalanceItemResult(
|
||||
status="approved",
|
||||
@@ -146,7 +147,7 @@ class ScalesService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_rebalance_plan(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return RebalanceItemResult(
|
||||
status="rejected",
|
||||
@@ -263,7 +264,7 @@ class ScalesService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors ``PestControlService._record_learn``."""
|
||||
@@ -272,7 +273,7 @@ class ScalesService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"scales",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -23,6 +23,7 @@ from roboco.foundation.policy.board_programs import PROGRAMS, project_participat
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.models.base import TaskStatus
|
||||
from roboco.services.base import BaseService
|
||||
from roboco.services.board_programs import learn_ref
|
||||
from roboco.services.task import SPACKLE_ITEM_SOURCE, SPACKLE_SOURCE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -101,7 +102,7 @@ class SpackleService(BaseService):
|
||||
item["materialized_task_id"] = str(new_task.id)
|
||||
markers.set_gap_fill(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "approved")
|
||||
await self._record_learn(task, learn_ref(item), "approved")
|
||||
await self.session.flush()
|
||||
return GapFillItemResult(
|
||||
status="approved",
|
||||
@@ -140,7 +141,7 @@ class SpackleService(BaseService):
|
||||
item["reject_reason"] = reason
|
||||
markers.set_gap_fill(task, payload)
|
||||
self._maybe_complete_cycle(task, payload)
|
||||
await self._record_learn(task, item_id, "rejected", reason)
|
||||
await self._record_learn(task, learn_ref(item), "rejected", reason)
|
||||
await self.session.flush()
|
||||
return GapFillItemResult(
|
||||
status="rejected",
|
||||
@@ -229,7 +230,7 @@ class SpackleService(BaseService):
|
||||
)
|
||||
|
||||
async def _record_learn(
|
||||
self, task: TaskTable, item_id: str, verdict: str, reason: str | None = None
|
||||
self, task: TaskTable, item_ref: str, verdict: str, reason: str | None = None
|
||||
) -> None:
|
||||
"""Best-effort LEARN: a record_decision failure must never break the
|
||||
CEO's approve/reject — mirrors ``PestControlService._record_learn``."""
|
||||
@@ -238,7 +239,7 @@ class SpackleService(BaseService):
|
||||
|
||||
await get_board_program_engine(self.session).record_decision(
|
||||
"spackle",
|
||||
item_id,
|
||||
item_ref,
|
||||
verdict,
|
||||
reason,
|
||||
exploration_task_id=cast("UUID", task.id),
|
||||
|
||||
@@ -127,7 +127,7 @@ async def test_i_will_work_on_matches_spec(
|
||||
# Per-role claim authority (CLAIM_RULES) is now enforced inside
|
||||
# spec.can_invoke_action when action == "claim", dispatched by
|
||||
# can_invoke_intent. The verb's single gate is can_invoke_intent.
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="my plan")
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, plan="my plan")
|
||||
body = env.as_dict()
|
||||
if expected.allowed:
|
||||
# Verb may still fail downstream of the gate (e.g. claim() returns
|
||||
|
||||
@@ -352,8 +352,8 @@ async def test_dev_can_claim_pending_task_via_gateway(
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -401,8 +401,8 @@ async def test_dev_full_chain_through_awaiting_qa(
|
||||
|
||||
# 1. Claim
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -480,8 +480,8 @@ async def test_full_chain_through_doc_handoff(
|
||||
|
||||
# Drive the dev side first (same as test_dev_full_chain_through_awaiting_qa).
|
||||
await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -420,8 +420,8 @@ async def test_dev_full_chain_through_awaiting_qa(
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -906,8 +906,8 @@ async def test_block_then_unblock_restore(
|
||||
|
||||
# Drive into in_progress via the real claim+start sequence.
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -969,8 +969,8 @@ async def test_pause_then_resume(
|
||||
c = _build_choreographer(db_session, task, task_service)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -723,8 +723,8 @@ async def test_i_am_done_full_chain_blocks_then_resolves(
|
||||
|
||||
# --- Round 1: no findings exist yet — i_am_done must pass untouched. ---
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -784,8 +784,8 @@ async def test_i_am_done_full_chain_blocks_then_resolves(
|
||||
# re-enforces the rich-plan gate for a developer — so the full plan is
|
||||
# supplied again here, same as the very first claim. ---
|
||||
env = await c.i_will_work_on(
|
||||
dev_agent.id,
|
||||
task.id,
|
||||
agent_id=dev_agent.id,
|
||||
task_id=task.id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -57,13 +57,7 @@ class _MockChoreographer:
|
||||
next="claim it",
|
||||
)
|
||||
|
||||
async def i_will_work_on(
|
||||
self,
|
||||
_agent_id: object,
|
||||
_task_id: object,
|
||||
_plan: object = None,
|
||||
**_kwargs: object,
|
||||
) -> Envelope:
|
||||
async def i_will_work_on(self, **_kwargs: object) -> Envelope:
|
||||
self._state["task_status"] = "in_progress"
|
||||
return Envelope.ok(
|
||||
status="in_progress",
|
||||
|
||||
@@ -77,9 +77,9 @@ async def test_i_will_work_on_dispatches_task_id() -> None:
|
||||
assert body["status"] == "in_progress"
|
||||
mock_chore.i_will_work_on.assert_awaited_once()
|
||||
call_args = mock_chore.i_will_work_on.call_args
|
||||
# second positional arg is task_id (UUID), third is plan
|
||||
assert str(call_args.args[1]) == _TASK_ID
|
||||
assert call_args.args[2] == "implement the feature"
|
||||
# the verb's params are keyword-only past agent_id, so read the kwargs
|
||||
assert str(call_args.kwargs["task_id"]) == _TASK_ID
|
||||
assert call_args.kwargs["plan"] == "implement the feature"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""A rejected envelope must leave a server-side trace.
|
||||
|
||||
An error envelope rides a 200, so the access log cannot distinguish a verb an
|
||||
agent could not satisfy from one that worked. Four Board Programs died that
|
||||
way on 2026-07-25 with no recoverable reason.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from roboco.api.routes.v1._role_dep import envelope_to_response
|
||||
from roboco.services.gateway.envelope import Envelope
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
|
||||
def _request(path: str) -> MagicMock:
|
||||
request = MagicMock()
|
||||
request.url.path = path
|
||||
request.state.correlation_id = "cid-1"
|
||||
request.headers = {"X-Agent-ID": "agent-7", "X-Agent-Role": "head_marketing"}
|
||||
return request
|
||||
|
||||
|
||||
def _captured(env: Envelope, path: str) -> list[Any]:
|
||||
"""``capture_logs`` rather than a hand-rolled processor swap — it is
|
||||
independent of whatever global structlog config the rest of the suite has
|
||||
already installed, which made the swap pass alone and fail in-suite."""
|
||||
with capture_logs() as entries:
|
||||
envelope_to_response(env=env, request=_request(path))
|
||||
return [e for e in entries if e.get("event") == "verb rejected"]
|
||||
|
||||
|
||||
def test_rejection_logs_reason_and_remediation() -> None:
|
||||
env = Envelope.invalid_state(
|
||||
message="finding 0 is missing 'source_url' — an uncited market claim is noise",
|
||||
remediate="provide the http(s) source URL finding 0's claim came from",
|
||||
context_briefing={},
|
||||
)
|
||||
entries = _captured(env, "/api/v1/do/propose_market_brief")
|
||||
|
||||
assert len(entries) == 1, entries
|
||||
logged = entries[0]
|
||||
assert logged["verb"] == "propose_market_brief"
|
||||
assert logged["error"]
|
||||
assert "source_url" in str(logged["detail"])
|
||||
assert "http(s) source URL" in str(logged["remediate"])
|
||||
assert logged["agent_id"] == "agent-7"
|
||||
assert logged["agent_role"] == "head_marketing"
|
||||
|
||||
|
||||
def test_success_envelope_logs_nothing() -> None:
|
||||
env = Envelope.ok(
|
||||
status="market_brief_proposed",
|
||||
task_id="t-1",
|
||||
next="i_am_idle()",
|
||||
context_briefing={},
|
||||
)
|
||||
assert _captured(env, "/api/v1/do/propose_market_brief") == []
|
||||
@@ -68,8 +68,8 @@ async def test_off_mode_ceo_header_spoof_still_works(
|
||||
new=AsyncMock(return_value=(aid, "ceo")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=CEO_AGENT_ID,
|
||||
x_agent_role="ceo",
|
||||
)
|
||||
@@ -89,8 +89,8 @@ async def test_off_mode_developer_header_trust_unchanged(
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
@@ -111,8 +111,8 @@ async def test_on_mode_spoofed_ceo_header_without_token_or_session_401(
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=CEO_AGENT_ID,
|
||||
x_agent_role="ceo",
|
||||
)
|
||||
@@ -127,7 +127,7 @@ async def test_on_mode_no_headers_no_cookie_401(
|
||||
401s without a valid session — never silently falls back to anything."""
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(MagicMock(), MagicMock())
|
||||
await get_agent_context(db=MagicMock(), response=MagicMock())
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -154,9 +154,7 @@ async def test_on_mode_valid_session_yields_ceo_context_and_slides_cookie(
|
||||
response = Response()
|
||||
|
||||
ctx = await get_agent_context(
|
||||
db_session,
|
||||
response,
|
||||
roboco_session=token,
|
||||
db=db_session, response=response, roboco_session=token
|
||||
)
|
||||
|
||||
assert ctx.role == AgentRole.CEO
|
||||
@@ -174,7 +172,9 @@ async def test_on_mode_invalid_session_cookie_401(
|
||||
) -> None:
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(db_session, Response(), roboco_session="not-a-real-jwt")
|
||||
await get_agent_context(
|
||||
db=db_session, response=Response(), roboco_session="not-a-real-jwt"
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ async def test_on_mode_agent_hmac_still_works(
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=str(aid),
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
@@ -212,8 +212,8 @@ async def test_on_mode_system_self_patch_still_works(
|
||||
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET)
|
||||
token = issue_agent_token(_SYSTEM_AGENT_ID, "system", "")
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=_SYSTEM_AGENT_ID,
|
||||
x_agent_role="system",
|
||||
x_agent_token=token,
|
||||
@@ -228,8 +228,8 @@ async def test_on_mode_forged_agent_token_401(monkeypatch: pytest.MonkeyPatch) -
|
||||
monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_token="forged",
|
||||
@@ -248,8 +248,8 @@ async def test_on_mode_spoofed_non_ceo_role_without_token_401(
|
||||
monkeypatch.setattr(settings, "cloud_auth_enabled", True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="main-pm",
|
||||
x_agent_role="main_pm",
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@ def test_do_server_attaches_correlation_id_header(
|
||||
) -> None:
|
||||
fake = _fake_client({"status": "noted"})
|
||||
with patch("httpx.Client", return_value=fake):
|
||||
do_module.note("hi")
|
||||
do_module.note(text="hi")
|
||||
_args, kwargs = fake.post.call_args
|
||||
headers = kwargs["headers"]
|
||||
assert headers["X-Agent-ID"] == "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
+16
-10
@@ -113,7 +113,9 @@ def test_get_orchestrator_raises_503_when_unset() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_id_raises_when_header_missing() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_current_agent_id(MagicMock(), MagicMock(), x_agent_id=None)
|
||||
await get_current_agent_id(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id=None
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@@ -125,7 +127,7 @@ async def test_get_current_agent_id_returns_uuid() -> None:
|
||||
new=AsyncMock(return_value=expected),
|
||||
):
|
||||
out = await get_current_agent_id(
|
||||
MagicMock(), MagicMock(), x_agent_id="be-dev-1"
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id="be-dev-1"
|
||||
)
|
||||
assert out == expected
|
||||
|
||||
@@ -133,13 +135,17 @@ async def test_get_current_agent_id_returns_uuid() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_slug_raises_when_header_missing() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_current_agent_slug(MagicMock(), MagicMock(), x_agent_id=None)
|
||||
await get_current_agent_slug(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id=None
|
||||
)
|
||||
assert exc.value.status_code == _HTTP_401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_agent_slug_returns_header() -> None:
|
||||
out = await get_current_agent_slug(MagicMock(), MagicMock(), x_agent_id="be-dev-1")
|
||||
out = await get_current_agent_slug(
|
||||
db=MagicMock(), response=MagicMock(), x_agent_id="be-dev-1"
|
||||
)
|
||||
assert out == "be-dev-1"
|
||||
|
||||
|
||||
@@ -494,8 +500,8 @@ def test_coerce_agent_team_empty_returns_none() -> None:
|
||||
async def test_get_agent_context_missing_id_raises() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id=None,
|
||||
x_agent_role="developer",
|
||||
)
|
||||
@@ -506,8 +512,8 @@ async def test_get_agent_context_missing_id_raises() -> None:
|
||||
async def test_get_agent_context_missing_role_raises() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role=None,
|
||||
)
|
||||
@@ -523,8 +529,8 @@ async def test_get_agent_context_happy_path(monkeypatch: pytest.MonkeyPatch) ->
|
||||
new=AsyncMock(return_value=(aid, "be-dev-1")),
|
||||
):
|
||||
ctx = await get_agent_context(
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
db=MagicMock(),
|
||||
response=MagicMock(),
|
||||
x_agent_id="be-dev-1",
|
||||
x_agent_role="developer",
|
||||
x_agent_team="backend",
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_pm_cannot_execute_code_writes_audit_row() -> None:
|
||||
deps = _make_deps(task=task_svc, audit=audit_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
audit_svc.log_event.assert_awaited()
|
||||
@@ -215,7 +215,7 @@ async def test_rejection_remediate_lands_in_audit_details() -> None:
|
||||
deps = _make_deps(task=task_svc, audit=audit_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
assert env.remediate
|
||||
|
||||
@@ -239,7 +239,9 @@ async def test_i_will_work_on_refuses_at_cap() -> None:
|
||||
task_svc.project_month_spend_usd = AsyncMock(return_value=999.0)
|
||||
c = Choreographer(_make_deps(task_svc))
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state", body
|
||||
assert "10.00" in body["message"] and "999.00" in body["message"]
|
||||
|
||||
@@ -131,7 +131,9 @@ async def test_i_will_work_on_blocks_when_agent_has_in_progress_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, target_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=target_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert str(other_id) in body["message"] or str(other_id) in body["remediate"]
|
||||
@@ -165,7 +167,7 @@ async def test_i_will_work_on_resumption_does_not_self_block() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, steps=_STEPS)
|
||||
assert env.error is None
|
||||
task_svc.start.assert_awaited_once_with(task_id, agent_id)
|
||||
|
||||
@@ -195,7 +197,9 @@ async def test_i_will_work_on_blocks_when_agent_has_paused_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, target_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=target_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert str(paused_id) in body["remediate"]
|
||||
@@ -226,7 +230,9 @@ async def test_cell_pm_cannot_claim_code_task_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(pm_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=pm_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
# Spec produces "role 'cell_pm' may not call 'i_will_work_on'".
|
||||
@@ -253,7 +259,9 @@ async def test_main_pm_cannot_claim_code_task_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(pm_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=pm_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
|
||||
@@ -403,7 +411,7 @@ async def test_developer_cannot_claim_qa_status_task() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=dev_id, task_id=task_id, steps=_STEPS)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_authorized"
|
||||
assert "developer" in body["message"]
|
||||
@@ -494,7 +502,9 @@ async def test_non_developer_role_cannot_claim_via_i_will_work_on() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(doc_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=doc_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
# Role-typed claim refuses with not_authorized
|
||||
assert body["error"] == "not_authorized"
|
||||
|
||||
@@ -149,8 +149,8 @@ async def test_dev_claim_acquires_lock_before_guard_read() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -167,8 +167,8 @@ async def test_i_will_work_on_pending_with_plan() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -213,7 +213,7 @@ async def test_i_will_work_on_pending_no_plan_returns_tracing_gap() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, plan=None)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
assert "plan" in body["missing"]
|
||||
@@ -263,8 +263,8 @@ async def test_i_will_work_on_needs_revision_re_starts() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -283,7 +283,7 @@ async def test_i_will_work_on_task_not_found_returns_not_found() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "not_found"
|
||||
|
||||
@@ -316,7 +316,7 @@ async def test_i_will_work_on_invalid_state_returns_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
# Spec produces "task is in 'completed', 'claim' requires: ..."
|
||||
@@ -376,8 +376,8 @@ async def test_i_will_work_on_blocks_when_journal_note_at_claim_missing() -> Non
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -173,8 +173,8 @@ async def test_i_will_work_on_pending_claim_raises_returns_invalid_state() -> No
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -196,8 +196,8 @@ async def test_i_will_work_on_pending_claim_returns_none_invalid_state() -> None
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -224,7 +224,9 @@ async def test_i_will_work_on_pending_no_plan_tracing_gap() -> None:
|
||||
task_svc.claim.return_value = claimed_task
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None, steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan=None, steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
|
||||
@@ -249,8 +251,8 @@ async def test_i_will_work_on_start_returns_none_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -279,8 +281,8 @@ async def test_needs_revision_branch_claim_fails_invalid_state() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -302,8 +304,8 @@ async def test_needs_revision_branch_start_fails() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -329,7 +331,9 @@ async def test_claimed_branch_returns_start_failed() -> None:
|
||||
task_svc.start.return_value = None
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
|
||||
@@ -350,7 +354,9 @@ async def test_i_will_work_on_in_progress_assigned_to_self_idempotent() -> None:
|
||||
task_svc.heartbeat = AsyncMock()
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
# No error — re-entry pass.
|
||||
assert "error" not in body or body.get("error") is None
|
||||
@@ -1095,7 +1101,9 @@ async def test_claimed_branch_already_active_guard() -> None:
|
||||
task_svc.list_in_progress_for_agent.return_value = [in_prog]
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="ok", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="ok", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
|
||||
@@ -1231,8 +1239,8 @@ async def test_i_will_work_on_envelope_carries_introspection_on_success() -> Non
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -1258,7 +1266,9 @@ async def test_i_will_work_on_envelope_carries_introspection_on_rejection() -> N
|
||||
task_svc = _wire_dev_task_svc(task_id, status="completed", assigned_to=agent_id)
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="x", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="x", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert body["current_state"] == "completed"
|
||||
@@ -1319,7 +1329,9 @@ async def test_i_will_work_on_missing_plan_does_not_claim_pending_task() -> None
|
||||
task_svc = _wire_dev_task_svc(task_id, status="pending")
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan=None, steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan=None, steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "tracing_gap"
|
||||
assert "plan" in body["missing"]
|
||||
@@ -1354,7 +1366,9 @@ async def test_i_will_work_on_claimed_with_no_plan_accepts_recovery_plan() -> No
|
||||
task_svc.start.return_value = started
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
env = await c.i_will_work_on(agent_id, task_id, plan="recovery plan", steps=_STEPS)
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=agent_id, task_id=task_id, plan="recovery plan", steps=_STEPS
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body["error"] is None, f"expected success, got {body}"
|
||||
task_svc.set_plan.assert_awaited_once()
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_i_will_work_on_pending_calls_claim_with_task_id_first() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -207,8 +207,8 @@ async def test_i_will_work_on_needs_revision_calls_start_with_task_id_first() ->
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -261,7 +261,7 @@ async def test_i_will_work_on_claimed_resumption_calls_start_with_task_id_first(
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
|
||||
env = await c.i_will_work_on(agent_id=agent_id, task_id=task_id, steps=_STEPS)
|
||||
|
||||
task_svc.start.assert_awaited_once_with(task_id, agent_id)
|
||||
assert env.error is None
|
||||
|
||||
@@ -115,7 +115,7 @@ async def test_dev_fresh_claim_without_steps_is_rejected() -> None:
|
||||
task_id = uuid4()
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, plan="do the thing")
|
||||
env = await c.i_will_work_on(agent_id=dev_id, task_id=task_id, plan="do the thing")
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "incomplete_input", body
|
||||
assert "steps" in (body.get("missing") or []), body
|
||||
@@ -128,8 +128,8 @@ async def test_dev_thin_step_description_is_rejected() -> None:
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_id,
|
||||
task_id,
|
||||
agent_id=dev_id,
|
||||
task_id=task_id,
|
||||
plan="do the thing",
|
||||
steps=[{"title": "Edit README", "description": "edit it"}],
|
||||
)
|
||||
@@ -162,7 +162,9 @@ async def test_dev_with_substantive_steps_passes_gate_and_persists_checklist() -
|
||||
{"title": "Edit README", "description": _GOOD_STEP_DESC},
|
||||
{"title": "Commit + open PR", "description": _GOOD_STEP_DESC},
|
||||
]
|
||||
env = await c.i_will_work_on(dev_id, task_id, **_full_plan_kwargs(steps_in))
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=dev_id, task_id=task_id, **_full_plan_kwargs(steps_in)
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body.get("error") != "incomplete_input", body
|
||||
# The full rich plan was layered into the panel-shaped dict and persisted,
|
||||
@@ -188,8 +190,8 @@ async def test_dev_fresh_claim_missing_considerations_and_risks_rejected() -> No
|
||||
c = Choreographer(_make_deps(task=_dev_task_svc(task_id)))
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
dev_id,
|
||||
task_id,
|
||||
agent_id=dev_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=[{"title": "Edit README", "description": _GOOD_STEP_DESC}],
|
||||
)
|
||||
@@ -210,7 +212,9 @@ async def test_dev_reentry_in_progress_short_circuits_before_steps_gate() -> Non
|
||||
svc.get.return_value.assigned_to = dev_id
|
||||
c = Choreographer(_make_deps(task=svc))
|
||||
|
||||
env = await c.i_will_work_on(dev_id, task_id, plan="resume: keep going")
|
||||
env = await c.i_will_work_on(
|
||||
agent_id=dev_id, task_id=task_id, plan="resume: keep going"
|
||||
)
|
||||
body = env.as_dict()
|
||||
assert body.get("error") is None, body
|
||||
assert body.get("status") == "in_progress", body
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_heartbeat_fires_on_rejection_not_authorized() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(aid, tid, plan="x")
|
||||
env = await c.i_will_work_on(agent_id=aid, task_id=tid, plan="x")
|
||||
|
||||
assert env.error == "not_authorized"
|
||||
task_svc.heartbeat.assert_awaited_with(tid)
|
||||
|
||||
@@ -86,7 +86,7 @@ async def test_i_will_work_on_calls_heartbeat() -> None:
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
await c.i_will_work_on(aid, tid, plan="go")
|
||||
await c.i_will_work_on(agent_id=aid, task_id=tid, plan="go")
|
||||
|
||||
task_svc.heartbeat.assert_awaited_with(tid)
|
||||
|
||||
|
||||
@@ -67,7 +67,12 @@ async def test_superseded_closes_pr_and_completes_without_merge(
|
||||
)
|
||||
|
||||
env = await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/frontend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/frontend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
git.close_pull_request.assert_awaited_once()
|
||||
@@ -100,7 +105,12 @@ async def test_rebased_retries_merge_and_completes(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
git.pr_merge.assert_awaited_once()
|
||||
@@ -133,7 +143,12 @@ async def test_genuine_conflict_escalates_to_ceo_and_does_not_loop(
|
||||
)
|
||||
|
||||
env = await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), tid, t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=tid,
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
@@ -171,7 +186,12 @@ async def test_diverged_rebase_outcome_escalates_rather_than_completing(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
@@ -198,7 +218,12 @@ async def test_unknown_rebase_outcome_escalates_rather_than_completing(
|
||||
)
|
||||
|
||||
await choreo._resolve_merge_conflict_on_complete(
|
||||
uuid4(), uuid4(), t, "feature/backend/root--cell", "notes", _EXC
|
||||
pm_agent_id=uuid4(),
|
||||
task_id=uuid4(),
|
||||
t=t,
|
||||
target="feature/backend/root--cell",
|
||||
notes="notes",
|
||||
exc=_EXC,
|
||||
)
|
||||
|
||||
task.admin_set_status.assert_awaited_once()
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_i_will_work_on_calls_ensure_work_session() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
@@ -290,8 +290,8 @@ async def test_ensure_work_session_not_called_when_start_fails() -> None:
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_will_work_on(
|
||||
agent_id,
|
||||
task_id,
|
||||
agent_id=agent_id,
|
||||
task_id=task_id,
|
||||
plan=_GOOD_PLAN,
|
||||
steps=_STEPS,
|
||||
technical_considerations=_GOOD_TC,
|
||||
|
||||
@@ -74,7 +74,7 @@ def test_note_default_scope_note(do_module: Any) -> None:
|
||||
fake_client.post.return_value = fake_response
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
do_module.note("hello world")
|
||||
do_module.note(text="hello world")
|
||||
|
||||
_args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"]["scope"] == "note"
|
||||
@@ -88,7 +88,7 @@ def test_note_with_scope_reflect(do_module: Any) -> None:
|
||||
fake_client.post.return_value = fake_response
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
do_module.note("did x", scope="reflect")
|
||||
do_module.note(text="did x", scope="reflect")
|
||||
|
||||
_args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"]["scope"] == "reflect"
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_flow_post_returns_envelope_on_400(flow_module: types.ModuleType) -> Non
|
||||
}
|
||||
client = _fake_client_with(400, body)
|
||||
with patch("httpx.Client", return_value=client):
|
||||
result = flow_module.i_will_work_on("task-id", plan="x")
|
||||
result = flow_module.i_will_work_on(task_id="task-id", plan="x")
|
||||
assert result["error"] == "not_authorized"
|
||||
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ def test_i_will_work_on_passes_plan(flow_module: types.ModuleType) -> None:
|
||||
fake_client = _make_fake_client({"status": "in_progress"})
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid", plan="my plan")
|
||||
flow_module.i_will_work_on(task_id="task-uuid", plan="my plan")
|
||||
|
||||
args, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
@@ -210,7 +210,7 @@ def test_i_will_work_on_plan_defaults_to_none(flow_module: types.ModuleType) ->
|
||||
fake_client = _make_fake_client({"status": "in_progress"})
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid")
|
||||
flow_module.i_will_work_on(task_id="task-uuid")
|
||||
|
||||
_, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
@@ -234,7 +234,7 @@ def test_i_will_work_on_passes_steps(flow_module: types.ModuleType) -> None:
|
||||
]
|
||||
|
||||
with patch("httpx.Client", return_value=fake_client):
|
||||
flow_module.i_will_work_on("task-uuid", plan="p", steps=steps)
|
||||
flow_module.i_will_work_on(task_id="task-uuid", plan="p", steps=steps)
|
||||
|
||||
_, kwargs = fake_client.post.call_args
|
||||
assert kwargs["json"] == {
|
||||
|
||||
@@ -471,6 +471,23 @@ async def test_prior_cycle_context_renders_rejections_with_reasons(
|
||||
assert "item-2 — too risky" in context
|
||||
|
||||
|
||||
def test_learn_ref_names_the_item_not_its_per_cycle_index() -> None:
|
||||
"""The ref reaches the next cycle's prompt, so it must say WHAT was
|
||||
decided — ``item-1`` means something different in every cycle."""
|
||||
assert bp_module.learn_ref({"id": "item-1", "title": "Fix the CLA gate"}) == (
|
||||
"Fix the CLA gate"
|
||||
)
|
||||
# Scales items name the live task they mutate instead of a draft title.
|
||||
assert bp_module.learn_ref(
|
||||
{"id": "item-0", "target_task_title": "Panel UX wave"}
|
||||
) == ("Panel UX wave")
|
||||
# A title-less item degrades to the old behaviour rather than an empty ref.
|
||||
assert bp_module.learn_ref({"id": "item-2"}) == "item-2"
|
||||
assert bp_module.learn_ref({"id": "item-3", "title": " "}) == "item-3"
|
||||
cap = 80
|
||||
assert len(bp_module.learn_ref({"id": "item-4", "title": "x" * 200})) == cap
|
||||
|
||||
|
||||
def test_originators_cover_exactly_the_registry() -> None:
|
||||
assert set(bp_module._ORIGINATORS) == set(PROGRAMS)
|
||||
|
||||
|
||||
@@ -418,8 +418,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -442,8 +445,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -421,8 +421,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -445,8 +448,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -415,8 +415,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -439,8 +442,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -413,8 +413,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -437,8 +440,10 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# Title, not index — the reject reason is only useful to the next cycle
|
||||
# if it says which proposal it was about.
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -528,8 +528,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Stale task",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -553,8 +556,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Stale task",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -418,8 +418,11 @@ async def test_approve_records_learn_decision(db_session: AsyncSession) -> None:
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_approved == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "approved",
|
||||
"reason": None,
|
||||
} in row.decisions
|
||||
@@ -442,8 +445,11 @@ async def test_reject_records_learn_decision_with_reason(
|
||||
)
|
||||
).scalar_one()
|
||||
assert row.items_rejected == ONE
|
||||
# The ref is the item's TITLE, not its per-cycle index: this row is
|
||||
# rendered into the next cycle's exploration prompt, where "item-0"
|
||||
# names nothing (see BoardProgramEngine.learn_ref).
|
||||
assert {
|
||||
"item_ref": "item-0",
|
||||
"item_ref": "Item 0",
|
||||
"verdict": "rejected",
|
||||
"reason": "not a priority",
|
||||
} in row.decisions
|
||||
|
||||
@@ -138,16 +138,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.118.0"
|
||||
version = "0.120.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -159,9 +159,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/ff/bbad57650babf07ae9761f1d974fcee73430387b5d3aa0ddb395234906e8/anthropic-0.118.0.tar.gz", hash = "sha256:acb3c43b7e7592cf635fa21930e67a99d32641d68ccd53f625b52f71d7870d55", size = 994705, upload-time = "2026-07-22T16:43:57.019Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/5c/3331da4fc009d448008a50c78d86cc929e8c937cd1442245ce3f80561c4e/anthropic-0.120.0.tar.gz", hash = "sha256:6ba6007dc9b00365b20f6101a6618f5196ac1ceef81512e4b5cc0e7436d4975d", size = 1008042, upload-time = "2026-07-24T16:32:52.384Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9f/d46e77054d7efb61c85ef5e8c7e95927cc89988b6491b46d22af60d63bda/anthropic-0.118.0-py3-none-any.whl", hash = "sha256:524d835869b8e374510b3bcc552e28dbdafc035a61d63fc0088c4035286ef8fe", size = 1010922, upload-time = "2026-07-22T16:43:55.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/8a/8522bdf809e1f95f0d9c936540987a3f6afba01d2921a2bf488dedf836a8/anthropic-0.120.0-py3-none-any.whl", hash = "sha256:591bd531563ec7b63a1e138f5c11f14cb94edda99623b349c2ce2ece8e08b8a5", size = 1022602, upload-time = "2026-07-24T16:32:50.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -420,11 +420,11 @@ filecache = [
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "7.1.5"
|
||||
version = "7.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/23/635f22bbd6478b02672432656a5f46775768e24b2715c2e8658b3d210602/cachetools-7.1.5.tar.gz", hash = "sha256:0def7134eba79e59448edaf5d2e3cc8e49978ab2fd56189c1efd19856b134ab5", size = 40500, upload-time = "2026-07-22T23:51:31.846Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/af/861ebc2e318a5c3300e3eb63bc4d30f3d70a46d13b360093728ac0705eed/cachetools-7.1.6.tar.gz", hash = "sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1", size = 40572, upload-time = "2026-07-23T22:47:53.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9b/8bd3cf22c5559a39e905b4884f773768f849091d91e374212d5a9ab66cd7/cachetools-7.1.5-py3-none-any.whl", hash = "sha256:900cda61ff34fafca2b7f78cfd2a91e2d4f9655c19309ef542ce0a04f91e6cd9", size = 16963, upload-time = "2026-07-22T23:51:30.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/f2/2086ba18a925a73586c4d4e61d25f4a6058e56fd00d77ce8f1d361ab4c9b/cachetools-7.1.6-py3-none-any.whl", hash = "sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096", size = 16954, upload-time = "2026-07-23T22:47:52.397Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -535,20 +535,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "claude-agent-sdk"
|
||||
version = "0.2.126"
|
||||
version = "0.2.128"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "mcp" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/33/30d51e1c53bb834fc4d51a39560c2e0cee0627767b2cc3b2001eb3b4ddb4/claude_agent_sdk-0.2.126.tar.gz", hash = "sha256:b0077f8da92f402032ec91b27499eb896aa97a9d78150ccd4a7840d39e70b9de", size = 304715, upload-time = "2026-07-22T21:40:34.678Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/e8/3a9622b31f9ee22274e13a620e5e75ac38454d14538391b2fe3bc7eb76dc/claude_agent_sdk-0.2.128.tar.gz", hash = "sha256:2ac7b2b3bc56ae9037fd284c8690d3dafab9493ecd28d8974bba79a418e1b800", size = 309369, upload-time = "2026-07-25T01:48:25.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/27/2624f73d65c24e2f2ebf8f4ef95f98a71bf382f3f622c439f096af8a66e8/claude_agent_sdk-0.2.126-py3-none-macosx_11_0_arm64.whl", hash = "sha256:edac88522b6fca74f1a84aa0d0f29caad4adda8aed04c3f08b86836e06c6c271", size = 74592742, upload-time = "2026-07-22T21:40:38.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/38/d774167d45c238f69a49734154236d1632ef7204153eafd13c126add32a3/claude_agent_sdk-0.2.126-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:8dd73c1a193afa133241d7346e89e5b60ebf45f6a8f2cc38c84c8aad794af61e", size = 79619741, upload-time = "2026-07-22T21:40:42.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f4/786a8c67b53677af27fdbe3b3a80ee57c38ca18f95d44ec03567f97a7495/claude_agent_sdk-0.2.126-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:239e6073486488ce78fe88681c05e819562f5c5e567f02574492c3be3d35684b", size = 84165405, upload-time = "2026-07-22T21:40:46.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/42/76603fcd11ab51713ec0138c21ffbb82f5f1afcab9f73729e7e23a19e055/claude_agent_sdk-0.2.126-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:79caa03c6612e268bee93e95170d6b9ffdfa4bb21373a2e9ba7a439d3fb2e8e8", size = 85204584, upload-time = "2026-07-22T21:40:50.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/95/2d6685e502a7708473f03710f34f2ad6d7b3968fdd6cc90091e4f341c34d/claude_agent_sdk-0.2.126-py3-none-win_amd64.whl", hash = "sha256:201299609bb045e7ca8c7d3ff5e8d5900da2dcf3faad742990efe659e4c215ae", size = 85114596, upload-time = "2026-07-22T21:40:55.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ff/03613a38a84285cd85f114fc26d4431f545d2b3b344eb8f69f9b0f18f3c6/claude_agent_sdk-0.2.128-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e47ee95be68cb07612fd5288a40f3307763da5a5adf66d6e03ea49dc0495c9c", size = 75183825, upload-time = "2026-07-25T01:48:30.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/df/2adbd3077f1a1cada39cd1508990d4008a8ac9ec74c801b24b1b302348b0/claude_agent_sdk-0.2.128-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:55e8fa28918af5620f391692efe80527ad9f2294cec14dadeea93c5161dbefc4", size = 80305667, upload-time = "2026-07-25T01:48:35.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/56/776a41af67a53d794b420dcd2f1075b585ed3725faa9827164f4a2e945dd/claude_agent_sdk-0.2.128-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:6c10cc1c5403b2b0b3d8deb79ad5271a8e49b9db6460193599b91f49f16b4cf7", size = 84617823, upload-time = "2026-07-25T01:48:39.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/1c/37044abbddf2141c4eeb6cc8e15a486491549a27283029d73db2c4f3c3b7/claude_agent_sdk-0.2.128-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:cc4a0f20337e227fe16f00edf7cbe109349707adb440fe51ca6c44f9f8d7d21a", size = 85663462, upload-time = "2026-07-25T01:48:43.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/c9/ffbd8113080f87a4cd7bbfc8b00a974ea7189e3f666b5d7a2903dec7b74f/claude_agent_sdk-0.2.128-py3-none-win_amd64.whl", hash = "sha256:37b87c8e75daa2a6dc74da6d788e0981a2e5e3a6be80cfa4c287abce2bf70e0b", size = 85566882, upload-time = "2026-07-25T01:48:48.635Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -804,19 +804,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.35.0"
|
||||
version = "40.36.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/a1/79278f517de8437a51af4cbce9cb86b0085fc22554e7ebffc6cb43bcb957/faker-40.35.0.tar.gz", hash = "sha256:13495348ec6f80d22c8c1654906ffeae336d485fea476544642ea24b120d9e13", size = 2025535, upload-time = "2026-07-22T17:18:42.589Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/d2/026af1e002bbc6df534d1f8262b18ec79a974f928e9290bfbfdfe7c7b2af/faker-40.36.0.tar.gz", hash = "sha256:754048c76c03afa7de83eee8f4bcee3cf668cbb7d995f54a4e9678db7f110308", size = 2025903, upload-time = "2026-07-24T21:11:33.088Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/c4/d217ea24d07fbdb00c81c83325fc814ee72ed93c70978ea5d4150604c305/faker-40.35.0-py3-none-any.whl", hash = "sha256:804224bc4ea6644256dd2ef5c951563bea318cb19be3505461c9c424c90181ec", size = 2062734, upload-time = "2026-07-22T17:18:40.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/9a/b947ed175ce9a0dcb070ccf3607f0ce8720cfb5ed1a36166a150b2acd5af/faker-40.36.0-py3-none-any.whl", hash = "sha256:82b9497d9cfe017048075bcf969298a74b1b6e39f5e4dad1211085d1133f7b62", size = 2062829, upload-time = "2026-07-24T21:11:31.37Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.139.2"
|
||||
version = "0.140.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
@@ -825,9 +825,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/95/d3f0ae10836324a2eab98a52b61210ac609f08200bf4bb0dc8132d32f78a/fastapi-0.139.2.tar.gz", hash = "sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e", size = 423428, upload-time = "2026-07-16T15:06:17.912Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/fb/fd7671137d9fa3df1d93a2f5111eb982709201724b29f211e4beb2d58688/fastapi-0.140.0.tar.gz", hash = "sha256:f338951b82fd74ca8f843163aec43ea1a1ce84d515415a50fa98fa25572a5544", size = 420968, upload-time = "2026-07-24T21:16:41.187Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/76/6d9e25ad88da9d3ff744bcdbec4736e38c2288611d43f673a5d9bfa27c07/fastapi-0.140.0-py3-none-any.whl", hash = "sha256:e951c0a0d9540bf5d9a2a9e078fd415da2ab7e312d435139e7d9e2e7fe9f0b23", size = 130863, upload-time = "2026-07-24T21:16:42.89Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1781,7 +1781,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.47.0"
|
||||
version = "2.48.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -1793,9 +1793,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/61/9aeef14de759306e85175126d3d6d56ee4f5072a9512c6c171d58d02a62d/openai-2.47.0.tar.gz", hash = "sha256:4e205548acd4304f235b86202269912e55bc88270b15d2a051fa2b53b90343a6", size = 1089906, upload-time = "2026-07-22T17:47:29.723Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/d4d1835488c0350424009dac5095b9a3e173bee12fd2e421ee27e2142c42/openai-2.48.0.tar.gz", hash = "sha256:231b1e7661dda14574986c2f71451e9d584b7fe69e0ee6480e12ed090b48fc16", size = 1093427, upload-time = "2026-07-23T20:15:50.402Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/69/26b032059273ad798d18fbcdbe369e871181841fd8bcb5caee32b7510039/openai-2.47.0-py3-none-any.whl", hash = "sha256:b3a1a7ad974092427ccb46d89f8852bdb67866680bcabeecc3ff5a3fdd71b15b", size = 1639987, upload-time = "2026-07-22T17:47:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/dcb891114e303c4379d4a498f10222e33eee540bcef4e1e493bd0af2b242/openai-2.48.0-py3-none-any.whl", hash = "sha256:c98df30aaaf93c51979f64d3e7c5b76464f8be0173368266229eb8fe6bd30f2c", size = 1648520, upload-time = "2026-07-23T20:15:48.052Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2725,27 +2725,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.22"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2967,14 +2967,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.69.0"
|
||||
version = "4.69.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/69/40407dfc835517f058b603dbf37a6df094d8582b015a51eddc988febbcb7/tqdm-4.69.0.tar.gz", hash = "sha256:700c5e85dcd5f009dd6222588a29180a193a748247a5d855b4d67db93d79a53b", size = 792569, upload-time = "2026-07-17T18:09:06.2Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/84/da0e5038228fa34dfd77c5026b173ed035d2a3ba31f1077590c013de2bff/tqdm-4.69.1.tar.gz", hash = "sha256:2be21080a0ce17e902c2f1baeb6a74bf551b67bbdfa4bc0109fad471d0b4cb0d", size = 793046, upload-time = "2026-07-24T14:22:02.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/50/5817619a0fca56aff06383dbfde7ae017b3ca383915b3f1e4713164273cf/tqdm-4.69.1-py3-none-any.whl", hash = "sha256:0a654b96f7a2660cceb615b56f307ec2bef96c515409014a429a561981ab52b4", size = 675452, upload-time = "2026-07-24T14:22:00.048Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3052,11 +3052,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.20260518"
|
||||
version = "6.0.12.20260724"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/6f/a28f44bcd56bebed42b028a2894c79853e2f5e6b5279e633cb3f287a05e7/types_pyyaml-6.0.12.20260724.tar.gz", hash = "sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b", size = 17893, upload-time = "2026-07-24T04:58:43.453Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/42/0337fefc615e20ee55d1c8f71b774a9b2b734a04669139c20753b27a2a3a/types_pyyaml-6.0.12.20260724-py3-none-any.whl", hash = "sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453", size = 20312, upload-time = "2026-07-24T04:58:42.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user