mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
60bd9b175d886de65aeff7cf315260c8904a6e43
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60bd9b175d |
fix(gateway): tighten sibling-dedup to cap spine-type concurrency
Smoke run 2026-05-11 (3rd attempt) caught the runaway-decomposition
pattern again, but with TWO different dev assignees so the old
same-assignee-same-type check missed it. Cell PM split one workflow
into "Execute Git Workflow: Branch, Edit, Commit, Push" (be-dev-1)
+ "Create PR with Task ID Linked to Parent Task" (be-dev-2), then
respawned and added a 3rd ("Commit and push smoke test change",
planning) — five tasks for what should be one dev hop.
The cell_pm.md prompt already forbids this pattern. The agents ignore
it. So we add the rule at the gate:
Rule 1 (spine-type concurrency cap): for task_type ∈
{code, planning, documentation}, a parent may have at most ONE
non-terminal subtask of that type — regardless of assignee. These
types are the spine of the lifecycle (dev → QA → doc → PM); the
chain is sequential and there's no merge story for parallel
siblings of the same spine type. PM must complete the existing
child first, or restructure into independent parents.
Rule 2 (same-assignee fallback): unchanged behavior for non-spine
types (research / design / administrative) — same assignee +
same type still rejects.
Error message names the existing sibling id so the PM doesn't
need to query separately, and remediate suggests either
"complete the existing one" or "split parent into two parents
for genuinely parallel work".
Quality: ruff + mypy clean, 417 unit tests pass.
|
||
|
|
6369184b72 |
docs(prompts): mark i_will_work_on plan param as required, not optional
Smoke run showed be-dev-1 repeatedly calling `i_will_work_on(task_id)` without `plan`, hitting `tracing_gap missing=['plan']` and retrying with the same payload. Root cause: prompt's verb table showed the signature as `plan=None` (optional default) while the gateway requires plan on every claim — including first claim. The dev followed the signature line, missed the workflow-table line that pairs it with `plan='...'`. Tightening the signature to `plan` (no default), explicit "REQUIRED even on first claim" callout, and a note that resume calls use `plan='resume: <next step>'`. |
||
|
|
92badfe6a0 |
docs(prompts): teach all roles the structured verb shapes (pre-gateway parity)
Counterpart to |
||
|
|
bcc748c8a3 |
fix: restore pre-gateway structured verb surfaces (5 fixes)
Smoke run 2026-05-11 showed five regressions stemming from the gateway consolidating multiple typed endpoints into thin verbs with collapsed signatures. The choreography is fine; the verb signatures lost the structured shape that pre-gateway forced agents to fill. Each fix restores a structured surface so the LLM's tool schema again carries the constraints that prevent the observed bugs. A) do_server: list valid channel slugs in say()/dm() docstrings. Stops invented channels (`backend`, `backend-dev`) — the LLM now sees the closed set in the tool schema. B) choreographer: add _delegate_sibling_dedup_guard. Rejects a delegate that would create a non-terminal sibling with the same assigned_to + task_type under the same parent — the dupe shape observed on smoke (Main PM creating two planning tasks for be-pm; Cell PM creating two code tasks for be-dev-1). C) choreographer: extend _validate_assignee_task_type to all roles. Devs may only get code|documentation|research (not planning/design/ administrative). QA gets code only. Documenters get documentation only. Catches the misroute observed on smoke (Cell PM gave be-dev-2 a 'research' coordination task that should have stayed with the PM). D) i_will_plan: thread approach / technical_considerations / risks / open_questions from MCP through to TaskService.set_plan as a TaskPlan-shaped dict. Empty default keeps back-compat. Panel's Plan tab now renders Approach / Sub-Tasks / Technical Considerations / Risks / Open Questions instead of an empty pane. E) note(): scope-specific structured fields restored. For 'decision' scope: context, options[], chosen, rationale, consequences. For 'reflect' scope: what_done, what_learned, what_struggled, next_steps. Rendered as markdown sections into the journal entry content so the Decisions and Reflections views show named blocks instead of a one-line phrase. Pre-gateway parity. Files changed: - roboco/mcp/do_server.py (A, E) - roboco/mcp/flow_server.py (D) - roboco/services/gateway/choreographer/_impl.py (B, C, D) - roboco/services/gateway/content_actions.py (E) - roboco/api/schemas/v2/flow.py (D) - roboco/api/schemas/v2/do.py (E) - roboco/api/routes/v2/flow_main_pm.py (D) - roboco/api/routes/v2/flow_cell_pm.py (D) - roboco/api/routes/v2/do.py (E) Quality: ruff + mypy clean. 89 unit tests pass on the touched surfaces. |
||
|
|
229797ffe3 |
fix: unblock smoke run (gateway envelope + alembic + redis + MCP)
Four bugs surfaced by the 2026-05-11 smoke run, all on the path from Main PM's first delegate to the cell PM accepting a subtask: - gateway: TaskCompletenessError from _create_subtask_from_inputs leaked through Starlette as a 500; agents retried in a tight loop because they never saw field_hints. Wrap the call in _create_subtask_and_envelope, catch the error, return Envelope.incomplete_input with the interrogation-pattern reply the upfront completeness check produces. - alembic: migration 012 used a 40-char revision id which exceeds alembic_version.version_num varchar(32). Upgrade fell back to create_all on every boot, silently skipping the migration. Rename to 012_align_agentrole_foundation (30 chars). File rename + revision string. - events/stream_bus: external Redis FLUSHALL while orchestrator is running (e.g. reset_runtime_state.sh) drops the consumer group; the listen loop then spams NOGROUP every block-cycle forever. Catch ResponseError with NOGROUP in the message and rebootstrap the group via _ensure_consumer_group, then continue. Self-heals without restart. - mcp/flow_server: delegate took body: dict with no schema, so the LLM invented values like nature='standard' and the SDK threw 'unhashable type: dict' on nested args. Flatten to typed top-level parameters with docstring listing valid enum values for team / task_type / nature / estimated_complexity. PLR0913 per-file ignore added for roboco/mcp/** because MCP tool signatures ARE the LLM contract — bundling into a dataclass would hide the enum hints that prevent the invention bug. |
||
|
|
207aaecd72 |
Feature: lifecycle canonical spec (#14)
* chore: clean make quality baseline on feature/lifecycle-canonical-spec
Three classes of pre-existing issues blocking `make quality`:
1. Alembic migrations 002/009/011 used runtime introspection
(op.get_bind() + inspect / bind.execute) without guarding for
offline (--sql) mode. `alembic upgrade head --sql` is part of
`make quality`; in offline mode `op.get_bind()` returns a
MockConnection with no inspection system, so the migrations
crashed before emitting their SQL stubs. Each migration now
short-circuits or simplifies in `context.is_offline_mode()` —
live-DB behavior is unchanged.
2. ruff format drift on three files left over from prior in-flight
edits (choreographer/_impl.py, content_actions.py, and one test
file). `ruff format` applied.
3. vulture flagged two unused `tb` parameters in async __aexit__
stubs in test_task_service_lifecycle_misc.py. The parameter is
protocol-required but unused by the body — renamed to `_tb`
(vulture treats underscore-prefixed names as intentionally unused).
`make quality` is now green from this branch's HEAD; subsequent
lifecycle-spec work can use it as the per-task gate.
* feat(lifecycle): canonical spec package + Role/Status/TaskType enums
Foundation for the canonical lifecycle/permissions module. Enums
mirror docs/internal/old/workflows/STATUS_TRANSITIONS.md +
PERMISSIONS.md. Tests pin enum membership against both the
predecessor canon and roboco.models.base.TaskType.
* feat(lifecycle): Decision dataclass with allow/reject/tracing_gap constructors
Single rejection shape every consumer maps to its native format
(Envelope, HTTP code, prompt hint). __post_init__ enforces the
allowed/rejection_kind invariants so a malformed Decision can't reach
a consumer.
* fix(lifecycle): tighten Decision invariants per Task 2 review
Two reviewer findings on the Task 2 Decision dataclass, addressed
in one commit:
1. The docstring promised `allowed=True ⇒ rejection_kind is None
AND missing == [] AND remediate is None`, but __post_init__ only
checked the rejection_kind half. A caller could construct an
allow-shaped Decision with stale missing/remediate fields and
sneak it past validation. Tighten __post_init__ to enforce the
full invariant. Add a regression test.
2. tracing_gap defensively copies the missing list (`list(missing)`)
to isolate the stored list from later caller-side mutation, but
no test pinned this. Add a regression test that mutates the source
list after construction and asserts the stored list is unchanged.
Issue 2 from the same review (mutable list vs tuple for `missing`)
is a broader design call deferred until consumers exist; the
defensive copy is sufficient until then.
* feat(lifecycle): Precondition/ActionSpec/IntentSpec/StatusTransition dataclasses
The four dataclasses that hold the canonical tables. ActionSpec and
StatusTransition are direct ports of pre-gateway PERMISSIONS.md +
STATUS_TRANSITIONS.md rows. IntentSpec is the gateway-only addition:
each gateway intent verb declares which atomic actions it composes.
* feat(lifecycle): _STATUS_TRANSITIONS table + STATUS_GRAPH view
Direct port of STATUS_TRANSITIONS.md. Every transition records its
trigger action and (optionally) a role constraint. STATUS_GRAPH is
the precomputed source→{targets} view callers use for reachability
checks.
* fix(lifecycle): pin role_constraint values + clarify Task-5 handoff
Two reviewer findings on Task 4 _STATUS_TRANSITIONS, addressed in
one commit:
1. The original Task-4 tests verified (source, target) pairs but
not role_constraint contents. A typo in a single role name (e.g.
forgetting MAIN_PM from escalate_to_ceo) would have slipped past
them silently. Add test_status_transitions_role_constraints_match_canon
pinning every non-None constraint and the cancel-block invariant.
2. role_constraint=None on the `claim` rows from PENDING and
NEEDS_REVISION was load-bearing — it is the explicit handoff
point between the StatusTransition table (state machine layer)
and CLAIM_RULES (per-role claim authority, lands in Task 5).
The original inline comment said this in passing; expand it so
the design choice is unmissable for a stranger reading just
spec.py.
* feat(lifecycle): _ATOMIC_ACTIONS + CLAIM_RULES + ROLE_TEAM_RULES tables
Direct port of PERMISSIONS.md. Every task management tool gets an
ActionSpec with allowed_roles, source_statuses, target_status,
self_review_block, and needs_team_match flags. CLAIM_RULES maps each
Role to the statuses they can claim from. ROLE_TEAM_RULES is the
per-slug team restriction.
* fix(lifecycle): tighten ActionSpec contracts per Task 5 review
Three reviewer findings on Task 5's _ATOMIC_ACTIONS table, addressed
in one commit:
1. set_plan.source_statuses widened to {CLAIMED, IN_PROGRESS} but
every existing caller (i_will_work_on / i_will_plan compositions)
runs set_plan while CLAIMED, between claim and start. Narrow to
{CLAIMED} only. If a future "edit plan mid-flight" feature lands,
widen explicitly with test coverage at that time.
2. needs_team_match was set True only on claim/qa_pass/qa_fail/
docs_complete. Defense-in-depth says every role-scoped task
action should re-assert team match (don't rely on the inheritance
chain through assigned_to alone). Flip to True on: start,
set_plan, block, pause, submit_verification, submit_qa,
submit_pm_review, complete, create_subtask. Leave False on
board/CEO actions and PM cross-cell interventions (unblock,
resume, cancel) where the cross-cell semantics are intentional.
3. claim.source_statuses is intentionally a SUPERSET of any single
role's CLAIM_RULES allowance (the table holds the union; CLAIM_RULES
holds the per-role authority). Add an inline comment above the
claim ActionSpec so a future reader doesn't conclude the two
tables disagree — they don't, they encode overlapping facts at
different grains.
* feat(lifecycle): _INTENT_VERBS table — every gateway verb declared
Each gateway intent verb is now a named composition of atomic actions
plus optional side effects. i_will_work_on = (claim, set_plan, start);
i_am_done = (submit_verification, submit_qa); open_pr is pure side
effects (push_branch, create_pr); etc.
* fix(lifecycle): widen block.allowed_roles to include QA + Documenter
Task 6 review caught a role-set inconsistency: i_am_blocked.allowed_roles
admits dev/QA/doc, but the underlying block.allowed_roles only allowed
dev+PM. Result: a QA or documenter calling i_am_blocked would pass the
IntentSpec gate and then be rejected by the composed ActionSpec gate
when Task 7 wires can_invoke_intent.
Widen block to include QA + Documenter. The semantic case is sound: a
QA reviewing a task can discover an external blocker; a documenter
writing docs may need PM intervention. Predecessor PERMISSIONS.md
restricted block to dev+PM, but with the gateway exposing i_am_blocked
to all worker roles, the underlying atomic must agree.
The deeper unclaim/escalate_up "imperative verb" concern from the same
review (composes=() but mutates state) is deferred to Task 8 where the
validator design lands.
* feat(lifecycle): public lookup functions + Context + preconditions
can_claim, can_invoke_action, can_invoke_intent, valid_next_verbs,
composed_actions_for, intents_for_role, status_after — the entire
public surface every consumer will use. Context carries the
caller-supplied state preconditions need (plan, journal-decision
flag, etc.). Preconditions for plan/commits/no_pr/ownership are
declared once and wired into the relevant IntentSpecs.
* fix(lifecycle): wire PRECONDITION_OWNERSHIP through Context.actor_id
Task 7 review found _p_owns_task reads agent.id but every call site
passes None for the agent arg. Result: getattr(None, "id", object())
returns a fresh sentinel, task.assigned_to == <sentinel> is always
False, and open_pr / i_am_done would reject every owner the moment
Task 9 wires consumers.
Fix: thread identity through Context.actor_id (new UUID field) and
rewrite _p_owns_task to read from the context. Both call sites already
pass the Context — no signature changes elsewhere. Add green-path
test exercising the owner-can-open-pr case the existing tests
missed (the Task 7 plan only tested precondition-failure paths,
which masked the bug).
Plus surface hygiene: STATUS_GRAPH, CLAIM_RULES, ROLE_TEAM_RULES,
and the four PRECONDITION_* constants are now in
roboco.lifecycle.__init__.__all__ so consumers in Tasks 8/9 don't
depend on the implicit `from roboco.lifecycle.spec import ...`
backdoor.
* feat(lifecycle): import-time self-consistency validators
10 validators run at module import; first failure raises
LifecycleSpecError and prevents the package from loading. Covers
status enum coverage, reachability, terminal exits, intent
compositions, status chain consistency, claim-rule role/status
coverage, self-review symmetry, team-rule slug existence, and
StatusTransition action references.
* fix(lifecycle): close validator gaps; resolve BACKLOG-claim and submit_qa IN_PROGRESS-shortcut ambiguity
Three reviewer follow-ups on Task 8's _validate.py, plus two real
data corrections the new action-target-reachability validator
surfaced.
1. Design spec §9 calls for "every ActionSpec.target_status, when
set, is reachable from each source_status via STATUS_GRAPH" —
missing from Task 8's 10 validators. Add
_check_action_target_reachable_from_source.
2. _check_role_team_rules_slugs verified slug existence in
AGENT_UUIDS but NOT that the cell team in ROLE_TEAM_RULES
matches the seed. Add _check_role_team_rules_team_match,
scoped to non-None entries only — None means "exempt from
team-match enforcement" (cross-cell roles), not "no team in
org chart".
3. test_validators_pass_on_real_spec was ceremonial. Add
test_run_all_validators_raises_on_unknown_intent_action,
a deliberate-break regression that monkeypatches _INTENT_VERBS
to inject a fake action and asserts LifecycleSpecError raises.
The new action-target-reachability validator caught two real
data inconsistencies between the predecessor canon docs and the
spec tables:
A. claim.source_statuses listed BACKLOG and CLAIM_RULES[*PM]
listed BACKLOG, but STATUS_GRAPH[BACKLOG] = {PENDING, CANCELLED}
only. Resolution: PMs use the explicit \`activate\` action to
move BACKLOG → PENDING, then claim from PENDING. Drop BACKLOG
from claim.source_statuses and CLAIM_RULES.
B. submit_qa.source_statuses listed IN_PROGRESS, but
STATUS_GRAPH[IN_PROGRESS] does NOT include AWAITING_QA. The
intent verb i_am_done composes (submit_verification, submit_qa)
which forces IN_PROGRESS → VERIFYING → AWAITING_QA — no
shortcut. Drop the stale IN_PROGRESS entry from
submit_qa.source_statuses.
Both corrections tighten the canonical state machine to a strict
no-skip transition graph. Pre-gateway PERMISSIONS.md/STATUS_TRANSITIONS.md
disagreements are resolved here; spec.py is the canon now.
* feat(gateway): Envelope.from_decision maps lifecycle Decisions to envelopes
Single shape adapter so verb bodies stop hand-composing rejection
envelopes. Each rejection_kind maps to a specific envelope flavor;
'self_review' folds into 'not_authorized' with a parenthetical hint;
constructing from an allow Decision raises (programmer error).
* feat(gateway): VerbRunner for atomic composed-action dispatch
Wraps spec.composed_actions_for(intent) in session.begin_nested()
so mid-sequence failures roll the DB back. Side effects run AFTER
the savepoint commits. Each atomic action name dispatches to a
TaskService method via a single, exhaustive _dispatch_atomic
mapping. New verbs slot in by adding an IntentSpec entry + a
_dispatch_atomic case if a new atomic is needed.
* refactor(gateway): i_will_work_on uses spec.can_invoke_intent + VerbRunner
Replace the bespoke status-branch dispatcher in i_will_work_on with the
spec-driven flow: load task -> load agent -> build spec.Context ->
spec.can_invoke_intent (and spec.can_claim for per-role status authority)
-> Envelope.from_decision on rejection -> VerbRunner.run_intent on success.
The _i_will_work_on_pending, _i_will_work_on_claimed,
_i_will_work_on_needs_revision, and _start_failed_envelope helpers are
removed; the runner replaces them. Two narrow verb-body re-entry blocks
remain for behaviors the spec does not yet model:
1. in_progress + same agent -> idempotent heartbeat-only return
2. claimed + same agent -> _resume_from_claimed (set_plan + start)
to recover from a stuck mid-claim crash without re-running claim
against a state the spec excludes.
The behavioral claim guards (already_active / paused / sibling_sequence)
also stay imperative for now -- they're not in the spec yet and migrate
into spec.extra_preconditions in a later task. Per-role claim authority
is enforced via spec.can_claim because the atomic claim action's
source_statuses are the union across roles; CLAIM_RULES narrows.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role x status x task_type='code') combo (112 rows) and
asserts the envelope error matches the spec's Decision (or can_claim's
Decision when the intent gate passes but per-role claim authority does
not). This is the contract that makes spec/verb drift impossible.
Existing tests updated where rejection-message text changed (the spec
now produces the messages, e.g. "role 'cell_pm' may not call
'i_will_work_on'" instead of "PM cannot execute code") or where the
spec's stricter view ("invalid_state" -> "not_authorized" for a dev
trying to claim awaiting_qa) is more accurate. Test fixtures were
updated to wire task.session.begin_nested as a proper async context
manager (required by VerbRunner) and to set agent_for().id so runner-
driven calls line up with assert_awaited_with(task_id, agent_id).
* refactor(lifecycle): push CLAIM_RULES enforcement into can_invoke_action
Task 11's i_will_work_on migration had to call spec.can_claim()
separately after spec.can_invoke_intent() because the claim action's
source_statuses is the union across all claim-eligible roles —
can_invoke_intent alone would let a developer pass for claiming
awaiting_qa (a QA-only state).
The retrofit pattern would repeat in every claim-composing verb
(i_will_plan, claim_review, claim_doc_task). Push the per-role
narrowing inside can_invoke_action when the action is "claim",
using the same not_authorized vs invalid_state disambiguation
can_claim already implemented (status-reserved-for-another-role
returns not_authorized; status-no-role-can-claim returns
invalid_state). Extracted the body to _check_claim_rules_narrow
to keep can_invoke_action under xenon's complexity threshold.
Update _i_will_work_on_gate to drop the redundant spec.can_claim
call. Update test_consumer_parity.py to assert only against
can_invoke_intent's Decision.
Tasks 12-22 will inherit the cleaner pattern: spec.can_invoke_intent
is the single gate; verb bodies don't need per-action retrofits.
* refactor(gateway): i_will_plan uses spec.can_invoke_intent + VerbRunner
Migrates i_will_plan to the spec-driven pattern Task 11 set up for
i_will_work_on. The verb body now: (1) loads task + agent, (2) builds
Context, (3) checks idempotent/recovery re-entry, (4) calls
spec.can_invoke_intent, (5) returns Envelope.from_decision on
rejection, (6) delegates composition to VerbRunner. The
_i_will_plan_* helpers are removed — the runner replaces them.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against every (role × status × task_type) combo and asserts the
envelope matches spec.Decision.
* refactor(gateway): delegate uses spec.can_invoke_intent for role/state gate
Migrates delegate to the spec-driven role/state gate. The chain
validation (main_pm->cell_pm, cell_pm->its team's devs), the
assignee-vs-task_type rule (Cell PMs receive planning-typed only),
the enum coercion, and the parent-lifecycle/cap guards STAY in the
verb body — they encode delegate-specific semantics the spec
doesn't model.
Parity test in tests/lifecycle/test_consumer_parity.py asserts the
spec's role+state rejection is correctly surfaced. Chain/assignee
rejections continue to be tested in test_choreographer_pm_extras.
* refactor(gateway): open_pr uses spec.can_invoke_intent + VerbRunner
Migrates open_pr to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS,
PRECONDITION_NO_PR) handle all three precondition checks; the verb
body delegates side-effect dispatch (push_branch, create_pr) to
VerbRunner.
Idempotent re-entry retained: an open_pr call against a task that
already has a PR (and the caller owns it) returns OK without
re-opening, rather than the tracing_gap the spec would otherwise
produce. This preserves agent ergonomics — two calls in a row
shouldn't surface a misleading "no_prior_pr" hint.
Parity test in tests/lifecycle/test_consumer_parity.py runs the verb
against representative (status x commits x pr_number) combos and
asserts the envelope matches spec.Decision.
* refactor(gateway): i_am_done uses spec.can_invoke_intent + VerbRunner
Migrates i_am_done to spec-driven gating. The spec's
extra_preconditions (PRECONDITION_OWNERSHIP, PRECONDITION_COMMITS)
handle ownership and commit-count checks; VerbRunner dispatches
the (submit_verification, submit_qa) atomic chain.
The tracing-gate preconditions (progress entry, journal:reflect,
acceptance criteria) and the field-level submit-qa gates stay in
the verb body — they model gates the spec doesn't yet cover.
Defense-in-depth: those gates run after the spec accepts the
ownership/commits checks.
Parity test in tests/lifecycle/test_consumer_parity.py runs the
verb against (role × status × ownership × commits) and asserts
the envelope matches spec.Decision.
* refactor(gateway): i_am_blocked uses spec.can_invoke_intent + VerbRunner
Migrates i_am_blocked to spec-driven gating. The journal:struggle
write stays in the verb body (it's a side effect outside the
lifecycle action). VerbRunner dispatches the `block` atomic action
via task_service.escalate.
Parity test in tests/lifecycle/test_consumer_parity.py.
* refactor(gateway): unclaim and resume use spec.can_invoke_intent
Migrates both verbs to the spec-driven gate. unclaim's verb body
keeps its dispatch (task.unclaim_for_agent) because composes=();
resume goes through VerbRunner with composes=("resume",).
The reassignment-rejection branch (introduced in
|
||
|
|
091e4076a2 |
fix(gateway): reject Cell-PM-assigned subtasks that aren't task_type=planning
Bug B from the 2026-05-09 smoke run. main-pm called delegate(assigned_to='be-pm', task_type='code'). The chain validator let it through (be-pm IS in main-pm's allowed targets), the schema let it through (task_type='code' is a valid enum value), and the subtask got created mis-typed. Task 0 made it cosmetically work because PMs can now plan code-typed parents — but the model is wrong: a Cell PM owns the PLANNING of the slice; the code execution is what they delegate to devs. New gate in _delegate_static_guards: when assignee is a Cell PM (be-pm/fe-pm/ux-pm), task_type MUST be 'planning'. Returns invalid_state with a remediate hint pointing at the right type. Devs are unrestricted (could be code OR documentation, depending on the slice). Tests: 3139 passing (+ 2 regression tests pinning the rule), 100% coverage, ruff clean. |
||
|
|
73e1e96851 | Many fixes and cleanups | ||
|
|
d819c28893 |
fix(gateway): make i_will_work_on plan-precondition atomic; recover claimed-no-plan
Bug A from the 2026-05-09 smoke run. be-dev-1 called i_will_work_on without `plan` on a pending task; claim() ran first (transitioned to `claimed`), then the plan check failed → tracing_gap. The natural retry path then dispatched to `_i_will_work_on_claimed`, which had no plan-recovery logic and called start() against a still-plan-less task, returning `start failed` forever. The dev kept looping; the parent escalated up; the whole slice ended `blocked`. Two changes (Task-5 atomicity pattern applied to i_will_work_on): 1. `_i_will_work_on_pending`: move the plan precondition BEFORE `claim()`. A missing-plan first call now returns tracing_gap with the task untouched in `pending`, so the agent's retry-with-plan succeeds cleanly. 2. `_i_will_work_on_claimed`: now accepts `plan` and calls set_plan before start() if the task has no plan yet. Recovery path for any already-stuck task (e.g. left over from the earlier image, or an orchestrator restart that left a partial claim). Also wires `plan` through the dispatcher to the claimed branch. Tests: 3137 passing (3135 + 2 regression tests pinning the atomic invariant), 100% coverage, ruff clean. |
||
|
|
b601441da7 |
fix(audit): record actor's actual role from agents.role at write time
The 2026-05-08 trace caught an audit row with actor=main-pm but
agent_role=cell_pm — the caller had supplied the verb's *expected*
role rather than the actor's actual role. Forensics work that joined
audit_log on agent_role would silently miscategorize the row.
Fix: AuditService now reads the actor's role directly from
agents.role at write time via the new _resolve_actor_role_from_db
helper. Wired into log_task_action_denial,
log_state_transition_denial, and log_notification_denial. The
caller-supplied role param is kept as a best-effort fallback for the
case where the DB lookup fails (singleton-without-DB paths,
permission errors, etc.) so audit writes never block the operation
being audited.
Coverage:
- 3 unit tests (test_audit.py) for the no-DB / invalid-id paths
- 1 unit-with-real-DB test (test_audit_real_query.py) verifying
the persisted row's agent_role is read from DB even when the
caller passes a deliberately-wrong role
- 1 unit-with-real-DB test for the no-row case
Tests: 3135 passing, 100% coverage, ruff clean.
|
||
|
|
19f27b4f88 |
fix(gateway): improve unclaim/resume rejection when task was reassigned
Investigation finding for Task 6 of the gateway introspection plan:
the 2026-05-08 trace's "not your claim" rejection at 02:51:22 / 02:52:48
was NOT caused by a UUID-comparator bug. AGENT_UUIDS in
roboco/seeds/initial_data.py are static so identity is stable across
restarts, and SQLAlchemy + Pydantic both round-trip UUIDs cleanly
(pinned by two new regression tests in test_task_service_misc.py).
The actual cause: the task was REASSIGNED out from under main-pm by
an upstream verb between when main-pm last touched it and when it
tried to unclaim/resume. Common triggers:
- cell_pm_complete propagates up via _maybe_advance_parent_to_pm_review,
which reassigns the parent to the cell PM for the team
- main_pm_complete clears assigned_to to None (CEO acts via UI)
- unblock with restore=True flips assigned_to back to pre_block_state
Pre-fix the rejection said only "not your claim" — agents can't tell
whether they hit a transient race or whether the task was legitimately
moved on. Fix: surface the current_owner UUID and hint that an
upstream verb did this, telling the agent to call give_me_work() to
find its current work.
|
||
|
|
6806516015 |
refactor(gateway): rename submit_for_qa to open_pr; pin atomic preconditions
Pre-fix, submit_for_qa opened a PR (side effect) and returned OK with next='call i_am_done' — agents read the verb name, assumed they were done with QA handoff, never called i_am_done, and PRs ended up orphaned (PR #12 in the 2026-05-08 trace). Two changes: 1. Rename submit_for_qa -> open_pr so the verb name matches the semantic. The PR opens here; the actual QA handoff happens at i_am_done. Renamed across: - choreographer/_impl.py (method) - mcp/flow_server.py (tool registration + _TOOLS dict) - api/routes/v2/flow_dev.py (route + handler) - api/schemas/v2/flow.py (OpenPrRequest) - services/gateway/verb_gates.py (_STATE_VERBS) - services/gateway/role_config.py (developer flow manifest) - services/gateway/content_actions.py (commit-success next= hint) - agent_sdk/server.py (post-tool guidance map) - runtime/orchestrator.py (developer prompt) - agents/prompts/{base,roles/developer,_generated/*}.md - tests/unit/gateway/test_submit_for_qa.py -> test_open_pr.py - tests/unit/api/routes/v2/test_flow_dev.py - tests/unit/gateway/test_verb_gates.py - tests/unit/api/test_correlation_id.py - tests/unit/mcp_servers/test_flow_server.py - tests/integration/test_full_lifecycle_real_db.py 2. New regression test (test_open_pr_does_not_create_pr_if_no_commits) pins the atomic invariant: preconditions (assignee, commits, no-prior-PR) must be checked BEFORE git.create_pr/push_branch run. Any future re-ordering breaks the test. Tests: 3128 passing (3127 + 1 new), 100% coverage, ruff clean. Note: TaskService.submit_for_qa() (the v1-layer service method) is INTENTIONALLY not renamed — it's a different layer used by the v1 routes. The rename here is only the gateway verb surface. |
||
|
|
2eeefb2ee1 |
refactor(gateway): consolidate commit + notify role gates into verb_gates
Replaces hardcoded role-string-constants in content_actions.py (_COMMIT_ALLOWED_ROLES, _NOTIFY_ALLOWED_ROLES) with calls into verb_gates.is_verb_allowed against a synthetic in-progress task probe. Pre-fix the same role lists lived in both content_actions and verb_gates; if one drifted the other would mask it. Now there's one table. Adds `notify` to verb_gates._ALWAYS_AVAILABLE for cell_pm, main_pm, product_owner, head_marketing. Note: i_will_plan / delegate role checks INTENTIONALLY stay as explicit `role not in (cell_pm, main_pm)` checks, not is_verb_allowed. Their state checks must surface as `invalid_state` (a different agent-side error code) — conflating them with role-state combo checks breaks the rejection-code semantics agents rely on. Tests: 3127 passing, 100% coverage, ruff clean. |
||
|
|
ebdbd7fc47 |
feat(gateway): wire envelope introspection into qa.py + doc.py role mixins
Closes the Task 3 wiring loop: every Envelope construction site in the QA and Documenter mixins now stamps current_state + valid_next_verbs. Refactored doc._check_i_documented_inputs to take the loaded task as a parameter so it can pass through to .with_introspection() without re-fetching. Task 3 of the 2026-05-08 gateway introspection plan is now complete across _impl.py, qa.py, and doc.py. |
||
|
|
17630bf3fc |
feat(gateway): wire envelope introspection into i_am_blocked, unclaim, resume,
escalate_up, escalate_to_ceo, submit_up, unblock Continues Task 3 of the gateway introspection plan. Every developer- and PM-facing lifecycle verb in _impl.py now stamps current_state + valid_next_verbs on both the success path and (where the task was loaded successfully) on rejection paths. The qa.py / doc.py role mixins still need wiring; that's a follow-up since their structure mirrors what's already been done here. |
||
|
|
6092df59b2 |
feat(gateway): wire envelope introspection into complete + main_pm/cell_pm complete
Trace-driven priority: the 2026-05-08 audit log showed PMs spamming `complete` against tasks in the wrong status (claimed/in_progress rather than awaiting_pm_review). Introspection on the rejection path now tells the PM the actual current_state and which verbs are valid right now (typically `delegate` / `escalate_up`), shrinking the trial-and-error loop. Continues Task 3 of the gateway introspection plan. |
||
|
|
56fbb97a80 |
feat(gateway): wire envelope introspection into submit_for_qa, i_am_done
submit_for_qa: ok envelopes (PR-already-open + new-PR) and not-assigned / no-commits rejections now stamp current_state + valid_next_verbs. i_am_done: not-assigned rejection plus tracing-gap and field-gate rejections all stamp introspection. Success path inherits via the shared _build_i_am_done_ok helper. Continues Task 3 of the gateway introspection plan. |
||
|
|
93c66fa07a |
feat(gateway): wire envelope introspection into give_me_work, i_will_plan, delegate
Continues Task 3 of the gateway introspection plan: every successful or rejected envelope from these three verbs now carries current_state + valid_next_verbs sourced from verb_gates.valid_next_verbs(role, task). Remaining verbs (submit_for_qa, i_am_done, i_am_blocked, unclaim, resume, unblock, complete, main_pm_complete, escalate_up, escalate_to_ceo, submit_up, plus the qa.py / doc.py role-mixins) to be wired in subsequent commits — the choke-point pattern at each call site is `.with_introspection(task=t, role=role)` on the constructed Envelope, so it stays mechanical from here. |
||
|
|
fd344511a5 |
feat(gateway): add Envelope introspection (current_state + valid_next_verbs)
Pre-fix, agents had no way to introspect what verbs were valid from a
task's current state — the 2026-05-08 trace showed them spamming
escalate_to_ceo/complete/unblock/resume against a `claimed` task and
racking up rejections. Pre-gateway agents could ground reasoning in
VALID_TRANSITIONS[status] from a doc; the gateway hid that.
Now every Envelope carries:
- current_state: the task's status string (or None for tool-discovery
envelopes that aren't task-bound)
- valid_next_verbs: the verbs the caller can usefully call right now,
sourced from verb_gates.valid_next_verbs(role, task)
Wired into i_will_work_on (highest-traffic verb) for both the OK path
and the wrong-state rejection path. Remaining lifecycle verbs to be
wired in subsequent commits (Task 3.9).
|
||
|
|
b4ec19ca9c |
feat(gateway): add verb_gates single source of truth for role x state
Pre-2026-05-08, role checks lived in three places that could disagree
silently:
1. roboco/services/gateway/role_config.py — verb allow-list per
role (used by spawn manifest)
2. roboco/services/gateway/claim_guards.py — pm_cannot_execute_code,
role_typed_claim
3. Choreographer string constants in _impl.py / qa.py / doc.py /
content_actions.py
verb_gates.valid_next_verbs(role, task) collapses them into one
declarative table mapping (role, task_status) -> tuple of valid verbs,
plus a per-role set of always-available verbs. Will be wired into
Envelope.valid_next_verbs in the next task so agents stop
trial-and-erroring against the gateway, and into the choreographer
guards in Task 4.
|
||
|
|
01ff44b83f |
fix(gateway): unblock PM planning + drop magic delegate task_type
Two coupled fixes from the 2026-05-08 smoke-test trace: 1. pm_cannot_execute_code is now scoped to i_will_work_on (the EXECUTION verb) only. Pre-fix it also fired on i_will_plan, which deadlocked any code-typed parent: cell_pm couldn't plan, so couldn't transition parent to in_progress, so couldn't delegate. PMs PLAN code-typed parents and DELEGATE the work — that's exactly the verb we were blocking. 2. delegate.task_type is now REQUIRED at both the HTTP boundary (DelegateRequest) and the choreographer dataclass (DelegateInputs). The pre-fix default of 'code' silently changed semantics whenever a caller forgot the field — main-pm's call in the smoke trace omitted it, schema defaulted to 'code', and the cell PM downstream was wedged. Also drops the choreographer's task.task_type fallback (the DB column is NOT NULL anyway). Plus middleware coverage tests for the parallel ServiceError → 4xx handler hierarchy added in the prior session, restoring 100% coverage across the touched files. Tests: 3101 passing, 100% coverage, ruff clean. |
||
|
|
f0eec854d1 | Code quality | ||
|
|
9aa30fb945 | 100% Coverage | ||
|
|
64c48356d0 |
test: lift coverage 41% → 76% (+1068 tests across 36 files)
Service-level tests now exercise provider, permissions, project, journal, messaging, work_session, metrics, kanban, extraction, learning, notification, dashboard, llm_routing, a2a, task, repository_base, audit, db_seed, branch_name, indexed_document, query_helpers, agent. API route tests cover provider, journal, project, sessions, dashboard, work_session, tasks, a2a, groups, notifications, agents, channels, messages, kanban, api_resources. Pure-function helpers covered: handlers, deps_helpers, middleware, middleware_docs, transcription, pr templates, agents_config, errors, logging, journal/notification/channel/a2a access, task_lifecycle, streaming, converters, crypto, schemas (common + websocket), events, permissions extras. pyproject ruff per-file-ignores extended for tests so PLR2004 (status code magic values), PLC0415 (lazy imports), PLR0913 (fixture params), ARG001 (unused fixture deps), SIM105, and E501 don't fight test idioms. |
||
|
|
b6903490f1 | + tests | ||
|
|
85ef124c8f | Quality Gates | ||
|
|
4829f93a68 |
fix(gateway): unblock task claim; full Phase 0/1/2 remediation
Resolves the 100% claim-failure rate introduced by the gateway rewrite
(commit
|
||
|
|
0a3c963923 |
fix(gateway): make i_will_plan / i_will_work_on idempotent on re-entry
Smoke 2026-05-04 captured the cycle the prior
|
||
|
|
cfdd389c43 |
test(gateway): regression for ContentActions.commit role gate
Pin: PM, QA, Board cannot call commit; developer + documenter can. Smoke 2026-05-03 saw main-pm reach the git layer with a 'commit' call attempting to author a fix to the very gateway bug we were hitting. |
||
|
|
c61a3bf81e |
fix(gateway): role-gate ContentActions.commit (developers + documenters only)
Smoke 2026-05-03 saw main-pm reach the git layer with a 'commit' call, trying to author 'fix(gateway): allow claimed status in i_will_plan preflight'. That should never have been possible — main_pm/cell_pm/ board/auditor/qa manifests all exclude commit. Reaching the verb body means either the MCP manifest filter mis-routed, or the agent hit the v2 do.py route directly. Mirror Task 16 notify pattern: server-side role check in the verb body rejects with not_authorized + 'PMs delegate, do not commit' remediate. Defense-in-depth — manifest is still the primary gate. |
||
|
|
ee743ffc7a |
fix(git): pass actor_agent_id to pr_merge for workspace resolution
pr_merge falls back to task.assigned_to for workspace resolution, but that field is None at merge time (submit_qa/pass_qa cleared it during prior transitions). When project.workspace_path is unset, the resolver raises ValidationError 'no workspace configured and no agent_id provided' — surfaces as 500 from cell_pm_complete. Add actor_agent_id parameter (the PM doing the merge) and use it as the primary workspace owner. Falls back to task.assigned_to, then created_by, before raising. cell_pm_complete now threads pm_agent_id through. |
||
|
|
24279fbee4 |
test(task): pin claim() seeds last_heartbeat_at (regression for 931eb0a)
Real-DB integration test: pending pre-assigned task → claim() → status=CLAIMED AND last_heartbeat_at populated. Without the seed (reverted in unit testing), the reaper would interpret NULL as stale and reap the freshly-claimed task on the next dispatch tick. |
||
|
|
638e06910f |
fix(orchestrator/prompts): enumerate task_type/complexity values explicitly
Smoke run agents picked invalid enums ('development', 'small') because
spawn prompts used 'code|documentation|...' format with literal '...'
that LLMs interpret as 'fill in something creative'. Replace with
explicit '<one of "a" / "b" / ...>' enumerations matching the
TaskType + Complexity enum members, plus an explicit reminder that
the gateway rejects invented values.
|
||
|
|
931eb0a40f |
fix(task): seed last_heartbeat_at on claim to stop reaper tight-loop
Smoke 2026-05-03: reaper fired ~12 times/sec against a freshly-claimed task because _finalize_claim never seeded last_heartbeat_at. Reaper's 'NULL means stale' rule then reaped the claim within ~250ms of the claim landing, agent re-claimed via i_will_plan, reaper reaped again. Set last_heartbeat_at = claimed_at in _finalize_claim so the freshly- claimed task carries an authentic heartbeat from second zero. Reaper logic stays untouched. |
||
|
|
809f169bb0 |
test(gateway): regression tests for i_will_plan claim/start bugs
Pin the smoke-2026-05-03 bugs that motivated
|
||
|
|
63d0adfaa5 |
fix(gateway): always claim() pending tasks in i_will_plan/i_will_work_on
Smoke run revealed CEO-pre-assigned root tasks stay stuck in pending because both verbs skipped claim() when task.assigned_to already matched the caller. Claim is what transitions pending → claimed; without it, start() refused the claimed → in_progress transition and silently returned None. i_will_plan then returned an OK envelope with a fabricated 'in_progress' status string while the DB stayed pending, causing the next delegate() call to correctly reject with PARENT_NOT_CLAIMED. Agent looped on delegate retries, never figuring out claim was the missing step. Fix: - Drive claim() on status == pending (idempotent for same assignee), not on assigned_to mismatch. - If start() returns None, surface invalid_state envelope instead of fabricating success. |
||
|
|
9310d66508 |
chore(post-audit): document notify verb + remove # type: ignore
Audit followups before pushing the gateway-restoration batch:
- Add notify() row to cell_pm.md, main_pm.md, board.md verb tables.
Manifests + routes + MCP + tests all wired in
|
||
|
|
9bae446cbe | Linting/Formatting | ||
|
|
95c96d4ce3 |
chore(work_session): delete dead generate_branch_name helper
Used '/' separator while live build_branch_name uses '--'. Footgun for future contributors. No live callers; safe to remove. |
||
|
|
f8e07d47e3 |
fix(optimal): make IndexJournalEntryParams.entry_id required
Task 22 added a runtime ValueError when entry_id is None, but the dataclass still typed it Optional. Contract-vs-runtime split — callers get no IDE/mypy hint about the required field. Tighten the type to UUID (required) and remove the misleading "can be None for system events" docstring note. Lifecycle events already use their synthetic uuid5 path, so no real caller is broken. |
||
|
|
b0107f8c12 |
fix(optimal): raise instead of falling back to 'unknown' doc source
Silent fallback hid an upstream bug where journal_entry.id wasn't flushed before indexing. Raise so we see the regression. |
||
|
|
279aceab3d |
chore(docker): set ROBOCO_PUBLIC_BASE_URL for commit-link rendering
Default 127.0.0.1 produced unusable links in commit message bodies. Set to the NAS LAN IP. |
||
|
|
d6f64f4b9d |
fix(audit): populate agent_id on task.* and agent.* events
task.awaiting_qa fired after submit_qa cleared claimed_by; agent.* events stored slug-only. Capture claimed_by before mutation and add slug→UUID resolver in orchestrator audit path. |
||
|
|
bf44d5aade |
fix(orchestrator): skip closure spawn if PM just paused via i_am_idle
Tiny race: dispatcher decided to spawn for closure between agent's heartbeat and idle-pause. Spawn would land against an already-paused parent. Gate spawn on (status != PAUSED OR last_heartbeat older than cutoff). |
||
|
|
3cabee155e |
chore(lifecycle): remove quarantined state (phantom)
State existed in the lifecycle table and the enum but no verb, route, or service path ever set it. Removing dead state. If we need problem-task isolation later we'll add it explicitly with a verb. |
||
|
|
5c742e6aec |
fix(git): serialize concurrent merges to the same parent branch
Two PMs completing different subtasks of the same parent could race on gh API merge calls. GitHub returns 409 to one but local DB write ordering wasn't guaranteed. Take row-level lock on parent task before merge; retry once on 409. |
||
|
|
3a2498a609 |
feat(gateway): notify(target, text, scope?) for PMs and Board
Pre-gateway PMs/Board sent formal notifications (require ack); gateway had say/dm only. Now PMs and Board can issue ack-required notifications via NotificationService through the standard envelope path. |
||
|
|
1ed1317a35 |
fix(workspace): re-apply agent ownership after refresh fetch
I1: _fetch_origin_best_effort runs as root and writes new pack files + ref updates that land root-owned, undoing _ensure_agent_owned that ran before. Subsequent spawns hit Permission denied. Mirror the fetch_branch_for_inspection pattern: re-run _ensure_agent_owned AFTER the fetch. I2: separate workspace_refresh_fetch_timeout_seconds (default 60s) from workspace_clone_timeout (300s). Refresh transfers small deltas; 300s of blocking on every spawn against a hung remote is operationally bad. 60s is enough for any sane refresh. |
||
|
|
e21ecd000b |
feat(workspace): fetch refs on healthy-clone re-entry
ensure_workspace short-circuited when the clone existed, so a respawned PM/Doc could be reviewing arbitrarily stale diffs. Add a best-effort 'git fetch origin' on every entry; checkout unchanged. |
||
|
|
074f47a2f9 |
feat(observability): propagate correlation_id end-to-end
X-Correlation-ID was bound to structlog but lost on the MCP→API hop. MCP shims now forward it; Envelope carries it back to the agent; audit_log row records it for forensic joins. |
||
|
|
99eac69aab |
fix(audit): migrate audit_log.details to JSONB so .astext works
Task 13's has_recent_tracing_gap query used .astext on a generic JSON column, which raises AttributeError at runtime. The choreographer's exception handler swallowed it, leaving the strike-count reset permanently inert in production. Migrate the column to JSONB (which supports .astext + GIN indexing for future audit queries), update the ORM, and add a real-DB integration test that would have caught this. |
||
|
|
44784293c7 |
fix(orchestrator): respect tracing-gap as forward progress
_pm_respawn_should_gate counted PARENT_NOT_CLAIMED rejections as no-progress and killed PMs after 3 strikes — even when the new prompts told them to call i_will_plan first. Reset counter when last response was a tracing_gap (rule-following retry, not stuck). |
||
|
|
87ef42bf09 |
chore(orchestrator): enable gateway cooldown logic in production
ROBOCO_GATEWAY_ENABLED defaulted to False, leaving trigger_filter's spawn cooldown / role-rate logic dormant. Flip to true and add the gateway_triggers table migration if missing. Without this, respawn rate has no server-side limit besides _pm_respawn_should_gate. |
||
|
|
00b385c019 |
fix(orchestrator): retry parent branch lookup to close PM race
When a PM's i_will_plan and a child dev's spawn fire in the same tick, the dev sometimes saw branch_name=None because the PM's transaction hadn't committed yet. Auto-block fired and the dev sat blocked until the next 30s tick. Add 3x250ms retries when parent is mid-claim. |
||
|
|
41d7e8295e |
fix(task): resume_for_agent delegates to resume() for observability
I2: resume_for_agent did its own _validate_and_set_status call, skipping resume()'s structlog 'Task resumed' event and the fire-and- forget RAG lifecycle-event indexing. Gateway-driven resumes were invisible to logs and the RAG corpus. Delegate to resume() after the gateway-specific ownership pre-checks (mirrors pause_for_agent's pattern). I1: documented why Choreographer.unclaim does NOT call _touch on its OK path - assigned_to is cleared, no claimant heartbeat to refresh. Prevents future readers from 'fixing' the asymmetry. |
||
|
|
cd9f999533 |
feat(gateway): add resume verb for paused -> in_progress
i_am_idle auto-pauses owned in_progress tasks; lifecycle table allows paused -> in_progress; no verb implemented it. Adds resume so an agent respawned for a paused task can continue. Routes through _validate_and_set_status mirroring the unclaim pattern (Task 9 fix). |
||
|
|
bf3de1ab2e |
fix(task): route unclaim_for_agent through _validate_and_set_status
unclaim_for_agent did direct attribute assignment, bypassing the single point of truth for status transitions. Choreographer's pre-check made the runtime correct today, but a future change to VALID_TRANSITIONS or ROLE_RESTRICTED_TRANSITIONS would silently miss this path. Route through _validate_and_set_status; assigned_to is still cleared explicitly as the unclaim's specific side effect. |
||
|
|
1f7c9adaba |
feat(gateway): add unclaim verb
Choreographer remediate strings already pointed agents at unclaim; the verb didn't exist. Now it does — claimed/in_progress -> pending, clears assigned_to, available to dev/qa/doc/cell_pm/main_pm. |
||
|
|
68466e9bf0 |
fix(gateway): enforce [task-id] prefix on every commit
ContentActions.commit was stripping user-supplied prefixes but never
re-adding the canonical one. Dev prompt promised auto-prefix; code
delivered nothing. Now every gateway commit lands with [task-id-short].
Format choice: simple [task_id[:8]] (8-char), matching the dev prompt
("Auto-prefixes [task-id]") and CLAUDE.md's documented commit format.
The legacy templates/git/commit.py uses the richer
[root_short:task_short] for the commit_for_task API path with full
traceability metadata; the gateway commit path is intentionally simpler
and stays aligned with the prompt-level promise.
|
||
|
|
c0c5838baa |
fix(bash-guard): close scheme-less curl bypass
Previous regex used a greedy [^|]* and required at least one / before the host, so 'curl roboco-orchestrator:8000/api' (no scheme, no slash) slipped through. Split into two simpler checks: (a) line starts with curl/wget/http/https/httpie, AND (b) line contains a forbidden host. Probe-verified: scheme-ful, scheme-less, and protocol-relative forms all denied; external URLs still allowed; GitHub-specific deny still fires first. |
||
|
|
8381ade3ce |
fix(bash-guard): deny internal curl to orchestrator/localhost
Prompts told agents internal API calls were denied; the guard only denied GitHub. Combined with task 4 (X-Agent-Role enforcement) this closes the manifest-bypass loophole. |
||
|
|
38246050d6 |
feat(gateway): audit-log every gate rejection
Choreographer takes an audit dep but never used it. Now every rejection Envelope (invalid_state, not_authorized, tracing_gap, not_found) writes a gateway.rejected row with verb + reason + missing fields. Forensic signal for stuck flows; no behaviour change for happy path. Adds AuditService.log_event() — generic free-form event_type write so the gateway doesn't have to extend AuditEventType for every new surface. Audit writes are best-effort: a failure logs a warning but never propagates, since the agent's response Envelope is the contract. |
||
|
|
8b43ed98be |
fix(messaging): close fail-open when agent_slug lookup returns None
I1: post_to_channel raised no error if get_agent_slug returned None (unknown/deleted agent), and send_message's 'if agent_slug' skipped the validate_channel_access call. Same fail-open class the prior commit was fixing, just narrower window. Now post_to_channel raises ChannelAccessDeniedError directly when the slug lookup fails — say() already converts that to a clean not_authorized Envelope. I2: 'writable channels for your role' wording was misleading because get_agent_channels resolves by slug, not role. Replaced with 'channels you may write to' for clarity. |
||
|
|
7907988e52 |
fix(messaging): restore channel-access RBAC in gateway say()
post_to_channel was calling send_message without agent_slug, which disabled the validate_channel_access check. Forward the slug; convert ChannelAccessDeniedError to a friendly not_authorized Envelope with the agent's writable-channel list (pre-gateway behaviour). |
||
|
|
92294f90fb |
feat(api/v2): enforce X-Agent-Role on every flow router
Route layer now rejects 403 if the role doesn't match the router's allowed set. Choreographer still re-checks role per verb where needed, but defense in depth means a future verb that forgets the role check doesn't leak. Auditor router also gated. |
||
|
|
3d5f14815d |
feat(gateway): add submit_for_qa verb so devs can open PRs
Gate E made i_am_done strict (requires pr_number set), but the only verb that opened PRs was i_am_done_with_catchup which lives off the dev manifest. Devs hit NO_PR with no escape. Adds submit_for_qa as the explicit push+PR step, leaving i_am_done to do the strict submit. |
||
|
|
6643b4c375 |
feat(gateway): heartbeat on every hot verb
i_will_work_on, i_have_committed, i_am_done, i_am_blocked, i_will_plan, and pm_give_me_work now call task.heartbeat() so the reaper has fresh data. Closes the loop opened by tasks 2a + 2b. |
||
|
|
c12aad3005 |
fix(orchestrator): consolidate stale-heartbeat config + drop dead _task_svc slot
I1: claim_heartbeat_ttl_seconds (300s) overlapped semantically with the pre-existing claim_stale_seconds (180s). Between 180-300s of silence, trigger_filter queued duplicate spawns while the reaper hadn't yet released the claim — exactly the dispatcher churn the reaper was supposed to close. Collapse to one field (claim_stale_seconds, 180s); reaper now consumes the same setting trigger_filter uses, so both agree on 'stale' on the same tick and the reaper runs first. I2: _task_svc injection slot on AgentOrchestrator.__init__ was production-dead (always None) and only used by __new__-based test instances. Drop the __init__ slot + the production branch in _reap_stale_claims that read it. Tests still pre-bind on __new__ instances; the attribute exists per-instance, not per-class. |
||
|
|
b301020398 |
feat(orchestrator): reap stale claims via last_heartbeat_at
Dispatch loop now releases tasks whose holder has gone silent past ROBOCO_CLAIM_HEARTBEAT_TTL_SECONDS (default 300s). Closes the 'dead container squats task forever' failure mode that the schema hinted at but no code enforced. |
||
|
|
37bc4e58ed |
feat(task): add heartbeat() to touch last_heartbeat_at
Foundation for stale-claim recovery. Schema column existed since migration 006; no writer until now. Idempotent UPDATE — no select roundtrip — so callable from any hot verb without extra DB cost. |
||
|
|
c33340bda1 |
fix(gateway): correct claim/start argument order in choreographer
Service signatures are (task_id, agent_id, ...) but choreographer was calling (agent_id, task_id). Production claim path silently returned None; unit tests pinned the buggy order so the bug was invisible. Swap all 7 call sites and update test assertions. Add a regression pin that locks in the correct order. |
||
|
|
4c9b7c4210 |
feat(gateway): restore Gate Set F completion-time guards
cell_pm_complete, main_pm_complete, and submit_up already had subtask- terminality gates inherited from the pre-gateway closure check at roboco/services/task.py. This commit verifies the gate is preserved and improves the remediation hint to actually NAME the non-terminal subtasks instead of telling the PM to "call triage()". The improvement uses a new private helper ``Choreographer._non_terminal_subtask_ids`` that queries get_subtasks and filters to non-terminal statuses, returning a comma-separated list of "<id> (<status>)" pairs. The PM now sees exactly which subtasks are blocking the parent's completion. Pre-gateway reference: roboco/services/task.py closure check (documented in PRE_GATEWAY_LIFECYCLE.md §4.3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4cb47afdb2 |
feat(gateway): restore Gate Set E submit-qa field-level gates
i_am_done now strictly enforces the four pre-gateway field-level gates
restored from roboco/api/routes/tasks.py:903-940 at commit
|
||
|
|
855cd24477 |
feat(gateway): restore Gate Set D content-tool ownership guards
When a caller passes an explicit task_id to commit / note / say / dm / evidence, ContentActions now verifies task.assigned_to == caller_agent_id before allowing the side effect. Auto-fill from get_active_task_for_agent is implicitly self-owned and does not need a re-check. evidence() additionally allows assigned_to=None (post-handoff transient state) so QA / documenter can inspect tasks between reassignments. Pre-gateway, agents could not even see tasks they didn't own because the MCP handlers resolved task from session context. The gateway exposes task_id parameters across multiple verbs, so the explicit ownership gate is required. Exception: say() and dm() with NO task_id are exempt — used for channel announcements and off-task A2A. The strict guard only applies when the agent supplies a task_id parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
197b0f22dc |
feat(gateway): restore Gate Set C exit-time guards
Choreographer.i_am_idle now refuses with INVALID_STATE when the caller has any pending (assigned but never claimed) task. Pre-gateway this was implicit because the orchestrator's auto-respawn would re-spawn the agent for the assignment, leading to a tight respawn loop. The explicit refusal lets the agent fix the state via i_will_work_on (dev/qa/doc) or i_will_plan (pm) first. Existing auto-pause for in_progress tasks is preserved (Gate Set C spec calls this out as still required) — it runs AFTER the pending guard, so an agent with a mix of pending+in_progress is told about the pending task first instead of silently pausing in_progress and then looping on the pending one. Pre-gateway reference: roboco/runtime/orchestrator.py auto-respawn loop guards (already preserved at HEAD); the explicit agent-facing gate is new. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
466cc8d8f7 |
feat(gateway): restore Gate Set B delegation-time guards
PARENT_NOT_CLAIMED: Choreographer.delegate now enforces that the parent task is in_progress AND assigned to the calling PM before allowing subtask creation. Pre-gateway this was implicit (orchestrator only spawned PMs after they claimed their parent); the gateway exposes delegate as a first-class verb so the gate must be explicit. SUBTASK_CAP: hard-blocks delegation when the parent already has 12 subtasks. Pre-gateway never had this cap because PMs naturally never created more than a handful per spawn cycle; with delegate as a verb agents can loop, so a cap is needed. The _delegate_guard helper was split into _delegate_role_guards, _delegate_static_guards, and _delegate_lifecycle_guards to keep each piece below the PLR0911 return-count threshold and make the layered gating explicit. Pre-gateway reference: implicit in roboco/runtime/orchestrator.py spawn flow; restored here as explicit server-side enforcement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5c0011c90b |
feat(gateway): restore Gate Set A claim-time guards
Ports five pre-gateway predicates that were dropped when the gateway
displaced the MCP claim handler. Predicates restored from
roboco/mcp/tasks/handlers/_helpers.py:124-204 and
roboco/mcp/tasks/handlers/claim.py:121-180 at commit
|
||
|
|
1da4ac4b2e |
chore(prompts): restore role identity framing + anti-patterns
Each role file now has a six-section structure (Identity / Inputs / Your
verbs / Workflow / Anti-patterns / When the gateway returns an error).
The Identity section gives a hard role prior with explicit negative space
("you do NOT write code", "you do NOT merge"), recovering the framing
that was lost in the gateway slim-down. The Anti-patterns section names
the gateway error codes the role will hit if they step out
(PARENT_NOT_CLAIMED, SUBTASK_CAP, PM_CANNOT_EXECUTE_CODE, NO_COMMITS,
NO_PR, NOT_SELF_VERIFIED, etc.), so agents expect the system to catch
them. base.md now centralizes the envelope contract, ground rules, and
channel-slug convention so per-role files stay focused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
27dccc7215 |
fix(gateway): four PM-lifecycle smoke regressions
* choreographer.escalate_up: AttributeError when target lookup returned
None. Switched from role-based escalate_up_to_role (which mishandled
slug-shaped escalation_target like "main-pm" because AgentRole only
accepts underscore form) to slug-based task.escalate, with explicit
None-handling that returns invalid_state instead of 500.
* prompts/roles/cell_pm.md + main_pm.md: enumerate the new lifecycle
verbs (i_will_plan, delegate, submit_up, give_me_work, i_am_idle).
Without this, PM agents fell back to calling i_will_work_on (the dev
verb) and 404'd at /api/v2/flow/cell_pm/i_will_work_on. Workflow
walkthroughs included.
* messaging.get_channel_by_slug + get_or_create_channel_by_slug: strip
leading "#" so "#main-pm-board" resolves to the row stored as
"main-pm-board". Agents follow Slack convention; gateway must accept
it.
* agent_sdk session-end post-mortem hook: corrected payload shape from
{content, kind:"reflect"} to {type:"task_reflection", title, content}
so /api/journals/me/entries validates. Added pad-to-min-length so the
50-char content gate doesn't reject thin post-mortems.
* test_choreographer_pm: updated escalate_up test to assert task.escalate
is awaited, plus regression test for the None-target invalid_state path.
380 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4520293def |
feat(gateway): wire PM lifecycle verbs through API + MCP + tests
* api/schemas/v2/flow.py: IWillPlanRequest, DelegateRequest, SubmitUpRequest with min_length=1 validators where appropriate. * api/routes/v2/flow_cell_pm.py: give_me_work routes to pm_give_me_work; new endpoints i_will_plan, delegate, submit_up. * api/routes/v2/flow_main_pm.py: new endpoints give_me_work, i_will_plan, delegate. * mcp/flow_server.py: Python wrappers for i_will_plan, delegate, submit_up registered in _TOOLS so manifest-scoped agents can call them. * tests/unit/gateway/test_choreographer_pm_extras.py: 22 tests covering happy + reject paths for each new verb plus i_am_idle's auto-pause behavior. * tests/unit/api/routes/v2/test_flow_cell_pm.py + test_flow_main_pm.py: route-level tests for the new endpoints. Test count: 352 → 381 (+29). make quality-fast green. |
||
|
|
4e5cdd0891 |
feat(gateway): restore PM lifecycle verbs (i_will_plan/delegate/submit_up)
Adds the missing gateway verbs that PMs need to drive their parent task
through the lifecycle. Pre-Phase-4, PMs could only triage/unblock/
complete/escalate/idle — they had no way to claim+start their parent
task, no way to create subtasks (told to curl raw), no way to bubble
cell-level PRs up to the Main PM. The lifecycle stalled at pending.
This commit lands the choreographer + service layer:
* TaskService gains: list_in_progress_for_agent, pause_for_agent,
submit_pm_review (gateway alias of submit_for_pm_review),
create_subtask (TaskCreateRequest-based, infers status from
assigned_to), and main_pm_agent.
* Choreographer gains:
- i_will_plan(task_id, plan): claim + set_plan + start for PMs.
Mirrors i_will_work_on, scoped to cell_pm/main_pm.
- delegate(parent_task_id, DelegateInputs): create a subtask with
delegation-chain validation (Main PM → cell PMs; Cell PM → its
own team's devs). Resolves slug → UUID via AGENT_UUIDS.
- submit_up(task_id, notes): cell PM bubbles a finished cell-scope
task up to Main PM. Opens a cell-level PR into the parent (Main
PM) branch, transitions to awaiting_pm_review, reassigns to
Main PM. Tracing gates: notes>=20, journal:decision, all
subtasks terminal, branch present.
- pm_give_me_work: returns the PM's first assigned task in any
active status (not just dev-active states).
- i_am_idle: now auto-pauses every in_progress task this agent
owns before marking idle. Restores the pre-Phase-4 behavior the
closure dispatcher relies on.
* role_config: cell_pm gains give_me_work + i_will_plan + delegate
+ submit_up; main_pm gains give_me_work + i_will_plan + delegate.
|
||
|
|
736bd57f30 |
fix(orchestrator/prompts): rewrite stale spawn prompts to use gateway verbs
5 spawn prompt builders (_build_dev_prompt + _get_workflow_instructions, _build_qa_prompt, _build_doc_prompt, _build_pm_review_prompt, _build_pm_closure_prompt) plus the recently-rewritten triage prompts referenced MCP tools deleted in Phase 4 T9. Replace with gateway verbs (give_me_work, i_will_work_on, claim_review, claim_doc_task, i_will_plan, delegate, complete, submit_up, etc). Drops the curl-POST recipe in favor of the new delegate verb. Surfaced live during NAS smoke. |
||
|
|
249e9c2c59 |
fix(gateway): align content_actions with actual service method names
content_actions.note/dm/say/commit/evidence called write_entry/send/ post_to_channel/git.commit/git.diff(base=)/fetch_branch_for_inspection, none of which existed on JournalService/A2AService/MessagingService/ GitService/WorkspaceService. Live smoke threw AttributeError on every content tool. Add the matching gateway-shaped adapters on each service (scope-string -> JournalEntryType for note; channel-by-slug -> default group -> active session for say; UUID-or-slug recipient resolution for dm; branch-name commit + diff(base=) for commit/evidence; project-aware fetch_branch_for_inspection on workspace). Surfaced live. |
||
|
|
54e19f88e6 |
fix(orchestrator): rewrite PM spawn prompts with gateway verbs + curl for delegation
Both _build_main_pm_triage_prompt and _build_pm_triage_prompt referenced MCP tools deleted in Phase 4 T9 (roboco_task_get/plan/start/create/activate/journal_decision/agent_idle). Main_pm + cell_pm spawned with these and tried the dead tools — minimax-m2.7 then improvised, ending up implementing the task itself instead of breaking it down. New prompts: gateway verbs (evidence, note, say, dm, complete, escalate_*, i_am_idle) for transitions/journal/comms; explicit POST /api/tasks curl recipe for breakdown + delegation (gateway has no create_subtask verb yet). Both prompts now reinforce: PMs do not implement, do not run git, must delegate. Main PM hands to Cell PMs; Cell PMs hand to devs. Surfaced live during NAS smoke. |
||
|
|
7089d78428 |
Revert "fix(orchestrator): default code tasks route to dev, not cell_pm"
This reverts commit
|
||
|
|
8d689e3eec |
fix(orchestrator): default code tasks route to dev, not cell_pm
_classify_code_task routed every default-complexity (medium) code task to cell_pm even when the description named no coordination work. The PM then re-delegated back to a developer, adding a useless hop and producing the smoke-test pattern where main_pm/cell_pm tried to do every lifecycle stage themselves. Drop the complexity==medium → cell_pm branch. Cell PM now lights up only when the description carries an actual coordination keyword (coordinate, integration, cross-team, sync, planning, milestone, dependencies, review). High/critical complexity, cross-cell keywords, missing team, and team=all still route to main_pm. Adds 11 unit tests; full suite 363 passing. Surfaced live during NAS smoke. |
||
|
|
4a3da479d1 |
test(gateway): cover per-transition reassignment
Adds:
- TaskService.reassign unit tests (set, clear, missing-task)
- New test_choreographer_reassignment.py covering:
i_am_done -> qa
pass_review -> documenter
i_documented -> cell_pm
main_pm_complete -> None (CEO via UI)
escalate_to_ceo (board) -> None
cell_pm_complete -> walks up to parent and reassigns when all
siblings terminal, skips otherwise
fail_review -> does NOT issue an explicit reassign (qa_fail
already restores the original developer via
quick_context)
|
||
|
|
3b02a72e05 |
feat(gateway): reassign task.assigned_to on every lifecycle transition
The orchestrator polls per-agent for actionable tasks. Without updating assigned_to as the lifecycle hands a task to the next role, the orchestrator kept respawning the previous-stage agent (often main-pm) for every stage, hitting role-permission rejections and looping. Each Choreographer transition now writes the new assignee alongside the existing A2A notification. - Adds TaskService.reassign(task_id, new_assignee | None) - _notify_qa: dev → qa - pass_review: qa → documenter - i_documented: doc → cell_pm - main_pm_complete + escalate_to_ceo: clear assignment (CEO acts via UI) - cell_pm_complete: walks up to parent; if all subtasks terminal, hands the parent off to the cell PM for that team via a new _maybe_advance_parent_to_pm_review helper fail_review still leans on qa_fail's existing original_developer recovery via quick_context, and unblock_with_restore restores pre_block_assignee — both already correct, no changes needed. |
||
|
|
55d2fae564 |
fix(errors): replace stale roboco_task_* MCP refs in error messages with gateway verbs
10 stale remediation hints across api/routes/tasks.py + exceptions.py pointed at MCP tools deleted in Phase 4 T9 (roboco_task_start, roboco_task_qa_pass, roboco_task_qa_fail, roboco_task_progress, roboco_task_unblock, roboco_task_submit_verification, roboco_task_submit_qa, roboco_task_complete, roboco_task_claim, roboco_task_activate). Each now mentions both the gateway verb (i_will_work_on, pass, fail, complete, unblock, etc.) and the panel REST equivalent. Surfaced live during NAS smoke. |
||
|
|
453a7ae22a |
fix(orchestrator): forward agent UUID (not slug) as ROBOCO_AGENT_ID to MCP env
Gateway v2 endpoints declare X-Agent-ID as Annotated[UUID, Header(...)]. The MCP servers (flow_server, do_server) read os.environ['ROBOCO_AGENT_ID'] verbatim and put it in the header. We were exporting the slug ('main-pm', 'be-dev-1', ...) so every gateway call from a containerized agent 422'd on header UUID parse. Resolve to UUID via AGENT_UUIDS lookup in seeds/initial_data.py. Surfaced by the new 422 logger (
|
||
|
|
de54c3b52d |
feat(api/middleware): log request body + per-field errors on 422 validation failures
FastAPI's default RequestValidationError returns details to the client but nothing to server logs. Smoke test hit a 422 on /api/v2/flow/main_pm/complete with no way to tell which field failed. Add a handler that logs path/method/body/errors on every 422 so the next failure is debuggable in one log scan. |
||
|
|
33464a207a |
docs(prompts): forbid Bash curl/git for gateway-covered ops; PMs don't implement
Live smoke runs showed agents reaching for `Bash curl /api/...` and `Bash git ...` even though the slim role prompts named the gateway verbs, and main_pm took ownership of an implementation task and tried to commit code from the PM seat. - base.md and every role prompt: explicit ground rule that direct curl-to-orchestrator and raw-git invocations are forbidden — every commit/push/PR/transition/journal/comms call goes through the gateway verbs. The deny-list line in base.md now also covers curl/wget to the orchestrator's /api/... (was GitHub-only before). - main_pm.md and cell_pm.md: explicit "you do not implement tasks yourself" rule. Implementation belongs to developers; PMs delegate. |
||
|
|
f2211a15a9 |
refactor(mcp): scope do_server tool registration to per-agent manifest
Same change as flow_server: read the spawn manifest's do_tools list and register only those names on this MCP server, so e.g. a Cell PM agent no longer sees commit() in its tool palette (Cell PMs don't write code, they only coordinate). Falls back to registering all do tools when the manifest is missing, with a warning, so test runs without the bind mount keep working. |
||
|
|
3d6028b036 |
refactor(mcp): scope flow_server tool registration to per-agent manifest
The MCP server previously registered every flow verb unconditionally, so Claude Code surfaced off-role verbs (e.g. main_pm seeing i_am_blocked) in the model's tool palette and agents called them, only to get 404s back from the role-scoped orchestrator API. Refactor each verb into a plain function and register them through a manifest-driven loop: read /app/tool-manifest.json, register only the verbs in flow_tools. When the manifest is absent or malformed, register the full set as a failsafe (covers local test runs without the bind mount) and log a warning. Drop _validate_role_compatibility — it's superseded by the new registration loop. |
||
|
|
6bae4ad69f |
fix(gateway): add escalate_to_ceo endpoint + unblock to main_pm role
Smoke surfaced: main-pm hit 404 on /api/v2/flow/main_pm/escalate_to_ceo and /i_am_blocked. role_config promised neither escalate_to_ceo nor unblock for main_pm; the router exposed unblock but not escalate_to_ceo. Aligned both: role_config now lists triage_all/unblock/complete/escalate_up/escalate_to_ceo/i_am_idle, and the router has the matching escalate_to_ceo endpoint. Note: i_am_blocked stays absent (PMs don't get blocked, they unblock others) — agent hallucinated it from the unscoped MCP tool list. Tightening MCP visibility to the manifest is a separate followup. |
||
|
|
25c51941f5 |
fix(api/tasks): coerce malformed sub_task ids in convert_plan response
Agents (esp. minimax-m2.7) PUT plans with sub_tasks like {id: '1', ...}. The write succeeds (DB stores raw JSON), but the response model SubTaskResponse.id requires a UUID — first read crashes the endpoint. Coerce non-UUID ids to a fresh UUID at serialization time so a single bad write doesn't brick the read path. Surfaced during NAS smoke.
|
||
|
|
dc147f41db |
fix(api/tasks): NO_PLAN remediation hint references gateway, not deleted MCP
Old hint pointed agents at roboco_task_plan() / roboco_task_start() — both deleted in Phase 4 T9. New hint covers both callers: panel via PATCH, agents via gateway i_will_work_on(plan=...). |
||
|
|
0ecf47ad77 | style(notification_delivery): re-format after subagent dead-code cleanup | ||
|
|
2c7b1aed27 |
refactor(tasks): remove dead /tasks routes + service code
Phase 4 left these task-route endpoints with no callers:
- POST /tasks/{id}/unclaim — no panel button, no orchestrator call,
no agent_sdk path. CLAIMED→PENDING transition stays defined in
enforcement/task_lifecycle.py for any future re-introduction.
- POST /tasks/{id}/pm-reject — orchestrator's PM-closure briefing
prompt references roboco_task_pm_reject (an MCP tool that does not
exist). Real PM rework path is escalate or cancel-and-recreate.
Cascading service cleanup (now-unreferenced):
- TaskService.unclaim, TaskService.pm_reject
- NotificationDeliveryService.notify_developer_of_pm_reject
- PMRejectDetails dataclass
- EventType.TASK_PM_REJECTED enum value (never emitted)
Panel and orchestrator paths preserved:
- claim, start, block/unblock, pause/resume, verify, submit-qa,
pass-qa, fail-qa, complete, cancel, activate, docs-complete,
ceo-approve/reject, escalate, escalate-to-ceo, substitute,
progress, checkpoint, commit, soft-block, submit-pm-review.
Tests: 336 unit tests still passing.
|
||
|
|
27e0b2c689 |
refactor(notifications): remove dead /notifications routes + service code
Phase 4 left these endpoints with no callers:
- POST /notifications (send_notification)
- GET /notifications/pending-a2a (check_pending_a2a)
- POST /notifications/ack-a2a (ack_a2a_notifications)
Panel only uses GET (list), GET/{id}, POST/{id}/read, POST/{id}/ack.
Orchestrator only calls GET /notifications. Agent_sdk does not call
notifications at all (uses A2A through different paths).
Cascading service cleanup (now-unreferenced):
- ApiNotificationCreate dataclass
- send_from_api, _assert_content
- has_pending_a2a, auto_ack_a2a
- NotificationCreateRequest schema + __init__ re-export
Also resolves smoketest issue #9 (Missing X-Agent-ID on
/notifications/pending-a2a) — endpoint no longer exists.
|