From 9bbe48ed1241bc3e6fbda1be8494fea9e7bb10b9 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 25 Jun 2026 12:21:40 -0400 Subject: [PATCH] feat(agent-team): surface the Confluence-writer lane on the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard topology is introspected from the compiled graph, but _maximal_compiled() built it without confluence=True, so conf_draft/conf_gate/ conf_write never appeared on the pipeline map. Wire them in: - topology.py: _maximal_compiled(confluence=True); NODE_META entries for the three conf nodes (own 'docs'/Documentation tree, conf_gate kind=gate, gated=True so the UI dims the opt-in lane); add the 'docs' tree; map the CONF_* phases in PHASE_TO_NODE so a live task buckets onto its node. - layout.ts: the conf_gate->conf_draft request-changes loop reads '↺ revised'. Frontend renders the new lane automatically (generic dagre layout + dynamic treeByNode). +2 topology tests; edge-classification test updated (intake is now a conditional branch point: clarify vs conf_draft). Full suite 1616 passed; frontend typecheck + 44 tests green. --- agent-team/agent_team/topology.py | 30 ++++++++++++++++++++++++++++++ agent-team/tests/test_topology.py | 31 +++++++++++++++++++++++++++++-- agent-team/web/src/layout.ts | 2 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/agent-team/agent_team/topology.py b/agent-team/agent_team/topology.py index 58d6fb7..43c39a9 100644 --- a/agent-team/agent_team/topology.py +++ b/agent-team/agent_team/topology.py @@ -99,6 +99,31 @@ NODE_META: dict[str, dict[str, Any]] = { "kind": "phase", "gated": True, }, + # Confluence-writer lane (opt-in, gated behind AGENT_TEAM_CONFLUENCE_ENABLED): + # draft -> human approval gate -> (dry-run) write. Its own "docs" process tree; + # reached from intake (Flow A: task_kind=confluence) and from verify (Flow B: + # document a shipped change). gated=True so the UI dims it in the default deploy. + graph_mod.CONF_DRAFT_NODE: { + "label": "Conf Draft", + "agent": "Claude (sub)", + "tree": "docs", + "kind": "phase", + "gated": True, + }, + graph_mod.CONF_GATE: { + "label": "Conf Gate", + "agent": "Owner", + "tree": "docs", + "kind": "gate", # human approval interrupt before any write + "gated": True, + }, + graph_mod.CONF_WRITE_NODE: { + "label": "Conf Write", + "agent": "Confluence", + "tree": "docs", + "kind": "phase", + "gated": True, + }, } # Process trees, in display order. ``root`` flags the tree that owns intake (the @@ -106,6 +131,7 @@ NODE_META: dict[str, dict[str, Any]] = { TREES: tuple[dict[str, Any], ...] = ( {"id": "core", "label": "Core", "root": True}, {"id": "sdlc", "label": "SDLC Pipeline", "root": False}, + {"id": "docs", "label": "Documentation", "root": False}, ) # Default tree/meta for a graph node with no NODE_META entry (a newly added @@ -129,6 +155,9 @@ PHASE_TO_NODE: dict[str, str] = { Phase.REVIEW.value: graph_mod.REVIEW, Phase.BUILD.value: graph_mod.BUILD_NODE, Phase.VERIFY.value: graph_mod.VERIFY_NODE, + Phase.CONF_DRAFT.value: graph_mod.CONF_DRAFT_NODE, + Phase.CONF_GATE.value: graph_mod.CONF_GATE, + Phase.CONF_WRITE.value: graph_mod.CONF_WRITE_NODE, } @@ -159,6 +188,7 @@ def _maximal_compiled() -> Any: route_review=_stub_route, build_verify=(_stub_node, _stub_node, _stub_route), dispatch_node=_stub_node, + confluence=True, # include the Confluence-writer lane in the drawn shape ) diff --git a/agent-team/tests/test_topology.py b/agent-team/tests/test_topology.py index 623a090..28f5b0a 100644 --- a/agent-team/tests/test_topology.py +++ b/agent-team/tests/test_topology.py @@ -33,14 +33,41 @@ def test_topology_has_expected_nodes_and_trees() -> None: assert by_id["build_node"]["gated"] is True +def test_confluence_lane_in_topology() -> None: + """The Confluence-writer lane renders as its own 'docs' tree (flag-gated).""" + topo = build_topology() + ids = {n["id"] for n in topo["nodes"]} + assert {"conf_draft", "conf_gate", "conf_write"} <= ids + tree_ids = {t["id"] for t in topo["trees"]} + assert "docs" in tree_ids + by_id = {n["id"]: n for n in topo["nodes"]} + for nid in ("conf_draft", "conf_gate", "conf_write"): + assert by_id[nid]["tree"] == "docs" + assert by_id[nid]["gated"] is True # opt-in / flag-gated -> dimmed + assert by_id["conf_gate"]["kind"] == "gate" # human approval interrupt + assert by_id["conf_draft"]["kind"] == "phase" + assert by_id["conf_write"]["kind"] == "phase" + + +def test_confluence_phase_to_node_mapping() -> None: + assert node_for_phase("conf_draft") == "conf_draft" + assert node_for_phase("conf_gate") == "conf_gate" + assert node_for_phase("conf_write") == "conf_write" + + def test_edges_classified_spine_branch_loopback() -> None: topo = build_topology() kinds = {(e["from"], e["to"]): e["kind"] for e in topo["edges"]} - assert kinds[("intake", "clarify")] == "spine" + # intake is a conditional branch point once the docs lane exists: it routes a + # normal task to clarify and a task_kind=confluence task to conf_draft. + assert kinds[("intake", "clarify")] == "branch" + assert kinds[("intake", "conf_draft")] == "branch" assert kinds[("clarify", "plan")] == "spine" - # review -> plan is the revise loop; verify -> build_node is the build loop. + # review -> plan is the revise loop; verify -> build_node is the build loop; + # conf_gate -> conf_draft is the request-changes redraft loop. assert kinds[("review", "plan")] == "loopback" assert kinds[("verify_node", "build_node")] == "loopback" + assert kinds[("conf_gate", "conf_draft")] == "loopback" # No edge references the framework terminals. nodes = {n["id"] for n in topo["nodes"]} for e in topo["edges"]: diff --git a/agent-team/web/src/layout.ts b/agent-team/web/src/layout.ts index 2b039af..33ec83a 100644 --- a/agent-team/web/src/layout.ts +++ b/agent-team/web/src/layout.ts @@ -90,7 +90,7 @@ export function layout( target: e.to, animated: true, style: { stroke: "#7a5cff", strokeWidth: 2, strokeDasharray: "5 4" }, - label: e.to === "plan" ? "↺ revised" : "↺ retried", + label: e.to === "plan" || e.to === "conf_draft" ? "↺ revised" : "↺ retried", labelStyle: { fill: "#9b8bff", fontWeight: 700 }, type: "smoothstep", zIndex: 5,