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:
Renzo F
2026-07-26 15:07:28 +02:00
committed by GitHub
co-authored by Renn F
parent 401f8a2cc9
commit 879afc14a4
84 changed files with 932 additions and 567 deletions
+2 -8
View File
@@ -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.
+6 -6
View File
@@ -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
}
```
+3 -3
View File
@@ -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
View File
@@ -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.",
)
```
+9 -7
View File
@@ -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.
+16 -8
View File
@@ -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
+1 -1
View File
@@ -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)
+12 -4
View File
@@ -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
+5 -4
View File
@@ -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`.
+4 -4
View File
@@ -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!
+5
View File
@@ -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
+6 -4
View File
@@ -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]
```
+7 -4
View File
@@ -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)
+7 -7
View File
@@ -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
View File
@@ -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
)
```
+3 -3
View File
@@ -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.)
+5 -2
View File
@@ -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?")
```
+67 -58
View File
@@ -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.
+9 -7
View File
@@ -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
+1 -1
View File
@@ -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.
+6 -11
View File
@@ -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"]
)
```
+4 -2
View File
@@ -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
+6 -4
View File
@@ -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`.
+3 -3
View File
@@ -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)