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:
@@ -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:
|
||||
|
||||
@@ -90,7 +90,7 @@ def _check_intent_preconditions(
|
||||
spec_intent: IntentSpec, task: Any, ctx: Context
|
||||
) -> Decision | None:
|
||||
"""Verb-level extra_preconditions gate.
|
||||
|
||||
|
||||
If the first failing precondition has rejection_kind='not_authorized',
|
||||
return Decision.reject(kind='not_authorized').
|
||||
All other failures return Decision.tracing_gap.
|
||||
@@ -102,12 +102,11 @@ def _check_intent_preconditions(
|
||||
]
|
||||
if not missing:
|
||||
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
|
||||
if first_missing.rejection_kind == "not_authorized":
|
||||
return Decision.reject(
|
||||
@@ -115,12 +114,9 @@ def _check_intent_preconditions(
|
||||
message=first_missing.remediate,
|
||||
remediate=first_missing.remediate,
|
||||
)
|
||||
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -94,11 +96,12 @@ def test_example() -> None:
|
||||
# ✅ GOOD: Annotation-typed class preserves union type
|
||||
class _TaskWithNoNotes:
|
||||
"""Variant where notes starts as None (no prior state)."""
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user