feat(agent-team): surface the Confluence-writer lane on the dashboard
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.
This commit is contained in:
parent
f56ee1c58d
commit
9bbe48ed12
3 changed files with 60 additions and 3 deletions
|
|
@ -99,6 +99,31 @@ NODE_META: dict[str, dict[str, Any]] = {
|
||||||
"kind": "phase",
|
"kind": "phase",
|
||||||
"gated": True,
|
"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
|
# 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], ...] = (
|
TREES: tuple[dict[str, Any], ...] = (
|
||||||
{"id": "core", "label": "Core", "root": True},
|
{"id": "core", "label": "Core", "root": True},
|
||||||
{"id": "sdlc", "label": "SDLC Pipeline", "root": False},
|
{"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
|
# 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.REVIEW.value: graph_mod.REVIEW,
|
||||||
Phase.BUILD.value: graph_mod.BUILD_NODE,
|
Phase.BUILD.value: graph_mod.BUILD_NODE,
|
||||||
Phase.VERIFY.value: graph_mod.VERIFY_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,
|
route_review=_stub_route,
|
||||||
build_verify=(_stub_node, _stub_node, _stub_route),
|
build_verify=(_stub_node, _stub_node, _stub_route),
|
||||||
dispatch_node=_stub_node,
|
dispatch_node=_stub_node,
|
||||||
|
confluence=True, # include the Confluence-writer lane in the drawn shape
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,41 @@ def test_topology_has_expected_nodes_and_trees() -> None:
|
||||||
assert by_id["build_node"]["gated"] is True
|
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:
|
def test_edges_classified_spine_branch_loopback() -> None:
|
||||||
topo = build_topology()
|
topo = build_topology()
|
||||||
kinds = {(e["from"], e["to"]): e["kind"] for e in topo["edges"]}
|
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"
|
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[("review", "plan")] == "loopback"
|
||||||
assert kinds[("verify_node", "build_node")] == "loopback"
|
assert kinds[("verify_node", "build_node")] == "loopback"
|
||||||
|
assert kinds[("conf_gate", "conf_draft")] == "loopback"
|
||||||
# No edge references the framework terminals.
|
# No edge references the framework terminals.
|
||||||
nodes = {n["id"] for n in topo["nodes"]}
|
nodes = {n["id"] for n in topo["nodes"]}
|
||||||
for e in topo["edges"]:
|
for e in topo["edges"]:
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ export function layout(
|
||||||
target: e.to,
|
target: e.to,
|
||||||
animated: true,
|
animated: true,
|
||||||
style: { stroke: "#7a5cff", strokeWidth: 2, strokeDasharray: "5 4" },
|
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 },
|
labelStyle: { fill: "#9b8bff", fontWeight: 700 },
|
||||||
type: "smoothstep",
|
type: "smoothstep",
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
|
|
|
||||||
Reference in a new issue