54 KiB
lda.chat Report Workflow Example Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a deterministic examples/lda_report_workflow/ case study that exercises document reading, report synthesis, a typed human approval interrupt, issue creation, resume, trace, and final output.
Architecture: Implement three trusted Python sources in one example package: local.lda_docs, local.lda_report, and local.issue_board. Author the graph with wf_authoring.WorkflowBuilder, generate a committed raw workflow plan from the compiled Workflow, and test artifact, deployment, interrupt inspection, resume, issue creation, and final output through the product API.
Tech Stack: Python 3.14, Pydantic models, wf_authoring.WorkflowBuilder, static Python workflow sources, generated raw workflow plans, wf_config, wf_server, pytest.
Scope
This is the second implementation slice for the Workflow Console initiative. It creates a deterministic workflow the future web console can render and replay.
Do not build the web app, presentation routes, or a model-backed demo agent in this plan. Do not connect to Google Drive, email, GitHub, or any external issue tracker. The issue board is a local JSON-backed demo source only.
File Structure
Create:
examples/lda_report_workflow/
.gitignore
README.md
documents/
architecture-notes.md
evaluation-findings.md
project-brief.md
risk-register.md
roadmap.md
document_source.py
report_source.py
issue_board_source.py
build_workflow.py
run-input.json
wf.config.json
workflow.plan.json # generated by build_workflow.py and committed
Create tests:
tests/examples/test_lda_report_workflow_example.py
Modify docs:
docs/current_roadmap.md
docs/add/evidence-index.md
docs/add/system-design-implementation.md
If either docs/add/... file no longer contains live thesis evidence sections,
only update docs/current_roadmap.md and the new example README.
Workflow Shape
The first implementation should be serial around the interrupt. Do not implement
real foreach in the example plan yet. Use arrays inside node payloads for the
document set and issue set so the demo is stable and easy to audit.
Graph:
list_documents -> read_documents -> analyze_documents -> build_report
-> review_issues interrupt
submitted -> create_selected_issues -> finalise_report -> end
cancelled -> record_revision_request -> end
The workflow still demonstrates typed interrupts and resume. A later slice can
replace the serial document/issue aggregation with explicit foreach once the
web graph renderer needs that visual complexity.
Data Contract
Use these durable ids:
- artifact id:
lda_report_case_study - deployment id:
lda_report_case_study.default - source ids:
local.lda_docs,local.lda_report,local.issue_board
Expected final report title:
lda.chat Thesis And Project Readiness Report
The interrupt:
kind:issue_review- outcomes:
submitted,cancelled - request payload contains rendered report markdown and proposed issues
- resume payload contains
approved,selected_issue_ids, and optionalcomment - request/resume schemas are explicit JSON Schema objects authored in
build_workflow.pyand generated into the raw plan
Task 1: Create Deterministic Document Fixtures
Files:
-
Create:
examples/lda_report_workflow/.gitignore -
Create:
examples/lda_report_workflow/documents/project-brief.md -
Create:
examples/lda_report_workflow/documents/architecture-notes.md -
Create:
examples/lda_report_workflow/documents/evaluation-findings.md -
Create:
examples/lda_report_workflow/documents/risk-register.md -
Create:
examples/lda_report_workflow/documents/roadmap.md -
Step 1: Create
.gitignore
Create examples/lda_report_workflow/.gitignore:
.wf_lda_report_store/
issue-board.json
- Step 2: Create project brief
Create examples/lda_report_workflow/documents/project-brief.md:
# Project Brief
lda.chat is a workflow substrate for AI-agent-facing workspace automation. The
prototype separates external planning from deterministic workflow execution.
Key achievements:
- Typed Draft, Artifact, Deployment, Run, and Trace lifecycle records.
- Source-provider boundary for platform, MCP, Python, and experimental OpenAPI
sources.
- JSON-RPC and CLI surfaces usable by external agents and human operators.
- Deterministic report and browser-click examples with audited agent challenge
runs.
Current positioning:
- The system is not a bundled autonomous planner.
- External agents or humans operate the workflow lifecycle.
- The next product-facing step is a local Workflow Console and defense demo.
- Step 3: Create architecture notes
Create examples/lda_report_workflow/documents/architecture-notes.md:
# Architecture Notes
Lifecycle:
- Drafts are mutable authoring workspaces with revisions.
- Artifacts are immutable versioned workflow definitions.
- Deployments bind logical source ids to configured concrete sources.
- Runs persist stopped execution records and bounded traces.
Runtime:
- The core executes typed graph nodes and routes by declared outcomes.
- State writes go through reducer-aware merge semantics.
- Interrupt nodes pause at explicit human-in-the-loop boundaries.
- Resume payloads are validated before state mutation.
Source providers:
- Python sources support trusted local demo capabilities.
- MCP sources preserve upstream session state through a runtime pool.
- OpenAPI source support exists as an experimental provider.
- Step 4: Create evaluation findings
Create examples/lda_report_workflow/documents/evaluation-findings.md:
# Evaluation Findings
Evidence:
- Automated tests cover core runtime, artifacts, deployments, CLI, JSON-RPC,
source providers, and examples.
- A 36-trial audited agent challenge campaign evaluated the product-facing CLI
under bounded conditions.
- Manual audit flags separate product-surface success from source-code or prior
answer reads.
Limitations:
- Agent challenge runs are operational evidence, not a controlled model study.
- The campaign used small sample sizes and changing prototype snapshots.
- The prototype does not claim production security, scheduling, RBAC, or a
general autonomous planning algorithm.
- Step 5: Create risk register
Create examples/lda_report_workflow/documents/risk-register.md:
# Risk Register
Material risks:
- Title and product framing can overstate the implemented autonomous-agent
layer if not explained carefully.
- Evaluation evidence is stronger as systems evidence than as a controlled
empirical model comparison.
- File-backed stores are useful for auditability but not a production
transaction boundary.
- The Workflow Console needs a strict loopback-only first slice to avoid
becoming an arbitrary RPC proxy.
Mitigations:
- Keep the agent/substrate boundary explicit in the thesis and defense.
- Present challenge data as bounded operational evidence.
- Defer production storage, auth, and remote proxying to future work.
- Step 6: Create roadmap document
Create examples/lda_report_workflow/documents/roadmap.md:
# Roadmap
Near-term:
- Add self-describing interrupt request and resume contracts.
- Build a deterministic lda.chat report workflow with typed issue approval.
- Build a local Workflow Console over JSON-RPC.
- Add live-demo replay support for the defense.
Later:
- Stabilize the experimental OpenAPI provider.
- Add production secret stores and transactional persistence.
- Add a surrounding agent interface and planner loop.
- Explore scheduling, richer debugging, and visual workflow editing.
- Step 7: Commit
git add examples/lda_report_workflow/.gitignore examples/lda_report_workflow/documents
git commit -m "test: add lda report workflow fixtures"
Task 2: Implement Document Source
Files:
-
Create:
examples/lda_report_workflow/document_source.py -
Test:
tests/examples/test_lda_report_workflow_example.py -
Step 1: Write failing tests for document source
Create tests/examples/test_lda_report_workflow_example.py:
from __future__ import annotations
from pathlib import Path
import pytest
from examples.lda_report_workflow.document_source import (
ListDocumentsInput,
ReadDocumentsInput,
_list_documents,
_read_documents,
)
EXAMPLE_DIR = Path(__file__).resolve().parents[2] / "examples" / "lda_report_workflow"
def test_lda_docs_lists_known_documents() -> None:
result = _list_documents(ListDocumentsInput())
names = {document.name for document in result.documents}
assert "project-brief.md" in names
assert "architecture-notes.md" in names
assert len(result.documents) == 5
def test_lda_docs_reads_selected_documents() -> None:
result = _read_documents(
ReadDocumentsInput(names=["project-brief.md", "roadmap.md"])
)
assert [document.name for document in result.documents] == [
"project-brief.md",
"roadmap.md",
]
assert "workflow substrate" in result.documents[0].text
def test_lda_docs_rejects_path_traversal() -> None:
with pytest.raises(ValueError, match="known document"):
_read_documents(ReadDocumentsInput(names=["../README.md"]))
- Step 2: Run tests and verify red
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_docs_lists_known_documents -q -n0
Expected: import failure because document_source.py does not exist.
- Step 3: Implement document source
Create examples/lda_report_workflow/document_source.py:
from __future__ import annotations
from pathlib import Path
from pydantic import BaseModel, Field
from wf_authoring import node
_EXAMPLE_DIR = Path(__file__).resolve().parent
_DOCUMENT_DIR = _EXAMPLE_DIR / "documents"
class ListDocumentsInput(BaseModel):
include_archived: bool = Field(
default=False,
description="Reserved for future filtering; current fixture has no archived docs.",
)
class DocumentRef(BaseModel):
name: str
title: str
class ListDocumentsOutput(BaseModel):
documents: list[DocumentRef]
class ReadDocumentsInput(BaseModel):
names: list[str] = Field(description="Document names returned by list_documents.")
class DocumentText(BaseModel):
name: str
title: str
text: str
class ReadDocumentsOutput(BaseModel):
documents: list[DocumentText]
@node(
name="list_documents",
description="List deterministic lda.chat project documents available to the demo.",
)
def list_documents(payload: ListDocumentsInput) -> ListDocumentsOutput:
return _list_documents(payload)
@node(
name="read_documents",
description="Read selected lda.chat project documents by fixture name.",
)
def read_documents(payload: ReadDocumentsInput) -> ReadDocumentsOutput:
return _read_documents(payload)
def _list_documents(_payload: ListDocumentsInput) -> ListDocumentsOutput:
documents = [
DocumentRef(name=path.name, title=_title_for(path))
for path in sorted(_DOCUMENT_DIR.glob("*.md"))
]
return ListDocumentsOutput(documents=documents)
def _read_documents(payload: ReadDocumentsInput) -> ReadDocumentsOutput:
allowed = {document.name for document in _list_documents(ListDocumentsInput()).documents}
selected: list[DocumentText] = []
for name in payload.names:
if name not in allowed:
raise ValueError(f"unknown or unsafe document name: {name!r}; use a known document")
path = _DOCUMENT_DIR / name
selected.append(
DocumentText(name=name, title=_title_for(path), text=path.read_text(encoding="utf-8"))
)
return ReadDocumentsOutput(documents=selected)
def _title_for(path: Path) -> str:
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith("# "):
return line.removeprefix("# ").strip()
return path.stem.replace("-", " ").title()
registry = [list_documents, read_documents]
- Step 4: Run document source tests
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_docs_lists_known_documents tests/examples/test_lda_report_workflow_example.py::test_lda_docs_reads_selected_documents tests/examples/test_lda_report_workflow_example.py::test_lda_docs_rejects_path_traversal -q -n0
Expected: pass.
- Step 5: Commit
git add examples/lda_report_workflow/document_source.py tests/examples/test_lda_report_workflow_example.py
git commit -m "feat: add lda document source example"
Task 3: Implement Report Source
Files:
-
Create:
examples/lda_report_workflow/report_source.py -
Modify:
tests/examples/test_lda_report_workflow_example.py -
Step 1: Add failing report source tests
Append to tests/examples/test_lda_report_workflow_example.py:
from examples.lda_report_workflow.report_source import (
AnalyzeDocumentsInput,
BuildReportInput,
CreateIssueDraftsInput,
FinaliseReportInput,
_analyze_documents,
_build_report,
_create_issue_drafts,
_finalise_report,
_record_revision_request,
)
def test_lda_report_source_builds_report_and_issue_drafts() -> None:
docs = _read_documents(
ReadDocumentsInput(names=["project-brief.md", "risk-register.md", "roadmap.md"])
)
analysis = _analyze_documents(AnalyzeDocumentsInput(documents=docs.documents))
report = _build_report(BuildReportInput(analysis=analysis.analysis))
issue_drafts = _create_issue_drafts(CreateIssueDraftsInput(report=report.report))
assert report.report.title == "lda.chat Thesis And Project Readiness Report"
assert "workflow substrate" in report.report.summary
assert issue_drafts.issues
assert issue_drafts.issues[0].id
assert issue_drafts.issues[0].title
def test_lda_report_source_finalises_approved_report() -> None:
docs = _read_documents(ReadDocumentsInput(names=["project-brief.md", "roadmap.md"]))
analysis = _analyze_documents(AnalyzeDocumentsInput(documents=docs.documents))
report = _build_report(BuildReportInput(analysis=analysis.analysis))
issue_drafts = _create_issue_drafts(CreateIssueDraftsInput(report=report.report))
final = _finalise_report(
FinaliseReportInput(
report=report.report,
created_issues=[],
approved=True,
selected_issue_ids=[issue_drafts.issues[0].id],
comment="Looks good.",
)
)
assert final.approved is True
assert final.markdown.startswith("# lda.chat Thesis And Project Readiness Report")
assert "Looks good." in final.markdown
def test_lda_report_source_records_revision_request() -> None:
result = _record_revision_request({"comment": "Needs revision"})
assert result["approved"] is False
assert "Needs revision" in result["markdown"]
- Step 2: Run one test and verify red
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_source_builds_report_and_issue_drafts -q -n0
Expected: import failure because report_source.py does not exist.
- Step 3: Implement report source
Create examples/lda_report_workflow/report_source.py:
from __future__ import annotations
from pydantic import BaseModel, Field
from wf_authoring import node
from .document_source import DocumentText
class AnalyzeDocumentsInput(BaseModel):
documents: list[DocumentText]
class Finding(BaseModel):
source: str
summary: str
risks: list[str] = Field(default_factory=list)
actions: list[str] = Field(default_factory=list)
class AnalyzeDocumentsOutput(BaseModel):
analysis: list[Finding]
class BuildReportInput(BaseModel):
analysis: list[Finding]
class ReadinessReport(BaseModel):
title: str
summary: str
achievements: list[str]
risks: list[str]
next_actions: list[str]
class BuildReportOutput(BaseModel):
report: ReadinessReport
markdown: str
class CreateIssueDraftsInput(BaseModel):
report: ReadinessReport
class ProposedIssue(BaseModel):
id: str
title: str
body: str
severity: str = "medium"
class CreateIssueDraftsOutput(BaseModel):
issues: list[ProposedIssue]
class CreatedIssue(BaseModel):
id: str
title: str
url: str
class FinaliseReportInput(BaseModel):
report: ReadinessReport
created_issues: list[CreatedIssue] = Field(default_factory=list)
approved: bool
selected_issue_ids: list[str] = Field(default_factory=list)
comment: str | None = None
class FinalReportOutput(BaseModel):
approved: bool
markdown: str
created_issues: list[CreatedIssue]
selected_issue_ids: list[str]
comment: str | None = None
@node(name="analyze_documents", description="Extract deterministic findings from lda.chat project documents.")
def analyze_documents(payload: AnalyzeDocumentsInput) -> AnalyzeDocumentsOutput:
return _analyze_documents(payload)
@node(name="build_report", description="Build a typed lda.chat readiness report from document findings.")
def build_report(payload: BuildReportInput) -> BuildReportOutput:
return _build_report(payload)
@node(name="create_issue_drafts", description="Create proposed local issue drafts from report risks and next actions.")
def create_issue_drafts(payload: CreateIssueDraftsInput) -> CreateIssueDraftsOutput:
return _create_issue_drafts(payload)
@node(name="finalise_report", description="Render the approved report and include created issue references.")
def finalise_report(payload: FinaliseReportInput) -> FinalReportOutput:
return _finalise_report(payload)
@node(name="record_revision_request", description="Return a cancelled report result when the human asks for revision.")
def record_revision_request(payload: dict[str, str | None]) -> FinalReportOutput:
return _record_revision_request(payload)
def _analyze_documents(payload: AnalyzeDocumentsInput) -> AnalyzeDocumentsOutput:
findings: list[Finding] = []
for document in payload.documents:
text = document.text.lower()
risks = _lines_after(document.text, "Material risks:")
actions = _lines_after(document.text, "Near-term:") or _lines_after(document.text, "Mitigations:")
if "workflow substrate" in text:
summary = "lda.chat is positioned as a workflow substrate for external agents."
elif "evaluation" in text:
summary = "Evaluation evidence is bounded and should be presented as operational evidence."
elif "risk" in text:
summary = "The current risk register emphasizes framing, evaluation, and storage limits."
elif "roadmap" in text:
summary = "Near-term roadmap focuses on typed interrupts, deterministic demos, and a console."
else:
summary = document.text.splitlines()[0].lstrip("# ").strip()
findings.append(
Finding(
source=document.name,
summary=summary,
risks=risks[:3],
actions=actions[:3],
)
)
return AnalyzeDocumentsOutput(analysis=findings)
def _build_report(payload: BuildReportInput) -> BuildReportOutput:
achievements = [
finding.summary for finding in payload.analysis if "risk register" not in finding.summary
]
risks = _unique(item for finding in payload.analysis for item in finding.risks)
next_actions = _unique(item for finding in payload.analysis for item in finding.actions)
report = ReadinessReport(
title="lda.chat Thesis And Project Readiness Report",
summary=(
"lda.chat is a typed workflow substrate with lifecycle records, "
"source-provider boundaries, and agent-operable CLI/RPC surfaces."
),
achievements=achievements[:6],
risks=risks[:6],
next_actions=next_actions[:6],
)
return BuildReportOutput(report=report, markdown=_render_report(report))
def _create_issue_drafts(payload: CreateIssueDraftsInput) -> CreateIssueDraftsOutput:
issues: list[ProposedIssue] = []
for index, risk in enumerate(payload.report.risks[:4], start=1):
issue_id = f"risk-{index}"
issues.append(
ProposedIssue(
id=issue_id,
title=risk.rstrip("."),
body=f"Track mitigation for: {risk}",
severity="high" if "title" in risk.lower() else "medium",
)
)
if not issues:
issues.append(
ProposedIssue(
id="follow-up-1",
title="Review thesis demo readiness",
body="Confirm the prepared workflow and replay are ready for defense.",
severity="medium",
)
)
return CreateIssueDraftsOutput(issues=issues)
def _finalise_report(payload: FinaliseReportInput) -> FinalReportOutput:
markdown = _render_report(payload.report)
if payload.created_issues:
markdown += "\n\nCreated issues:\n"
markdown += "\n".join(
f"- {issue.id}: {issue.title} ({issue.url})" for issue in payload.created_issues
)
if payload.comment:
markdown += f"\n\nApproval comment: {payload.comment}"
return FinalReportOutput(
approved=payload.approved,
markdown=markdown,
created_issues=payload.created_issues,
selected_issue_ids=payload.selected_issue_ids,
comment=payload.comment,
)
def _record_revision_request(payload: dict[str, str | None]) -> FinalReportOutput:
comment = payload.get("comment") or "Revision requested."
return FinalReportOutput(
approved=False,
markdown=f"# Revision Requested\n\n{comment}",
created_issues=[],
selected_issue_ids=[],
comment=comment,
)
def _render_report(report: ReadinessReport) -> str:
lines = [f"# {report.title}", "", "Summary:", report.summary, ""]
lines.append("Achievements:")
lines.extend(f"- {item}" for item in report.achievements)
lines.extend(["", "Risks:"])
lines.extend(f"- {item}" for item in report.risks)
lines.extend(["", "Next actions:"])
lines.extend(f"- {item}" for item in report.next_actions)
return "\n".join(lines)
def _lines_after(text: str, heading: str) -> list[str]:
lines: list[str] = []
active = False
for raw in text.splitlines():
line = raw.strip()
if line == heading:
active = True
continue
if active and line.endswith(":"):
break
if active and line.startswith("- "):
lines.append(line.removeprefix("- ").strip())
return lines
def _unique(items: object) -> list[str]:
seen: set[str] = set()
values: list[str] = []
for item in items:
if isinstance(item, str) and item and item not in seen:
seen.add(item)
values.append(item)
return values
registry = [
analyze_documents,
build_report,
create_issue_drafts,
finalise_report,
record_revision_request,
]
- Step 4: Run report source tests
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_source_builds_report_and_issue_drafts tests/examples/test_lda_report_workflow_example.py::test_lda_report_source_finalises_approved_report tests/examples/test_lda_report_workflow_example.py::test_lda_report_source_records_revision_request -q -n0
Expected: pass.
- Step 5: Commit
git add examples/lda_report_workflow/report_source.py tests/examples/test_lda_report_workflow_example.py
git commit -m "feat: add lda report source example"
Task 4: Implement Local Issue Board Source
Files:
-
Create:
examples/lda_report_workflow/issue_board_source.py -
Modify:
tests/examples/test_lda_report_workflow_example.py -
Step 1: Add failing issue board tests
Append to tests/examples/test_lda_report_workflow_example.py:
from examples.lda_report_workflow.issue_board_source import (
CreateIssuesInput,
ResetIssueBoardInput,
_create_issues,
_reset_issue_board,
)
def test_issue_board_creates_selected_issues(tmp_path: Path) -> None:
board_path = tmp_path / "issue-board.json"
drafts = _create_issue_drafts(
CreateIssueDraftsInput(
report=ReadinessReport(
title="Test",
summary="Summary",
achievements=[],
risks=["Risk one", "Risk two"],
next_actions=[],
)
)
)
result = _create_issues(
CreateIssuesInput(
board_path=str(board_path),
issues=drafts.issues,
selected_issue_ids=[drafts.issues[0].id],
)
)
assert len(result.created_issues) == 1
assert result.created_issues[0].title == drafts.issues[0].title
assert board_path.exists()
def test_issue_board_reset_removes_existing_file(tmp_path: Path) -> None:
board_path = tmp_path / "issue-board.json"
board_path.write_text("[]", encoding="utf-8")
result = _reset_issue_board(ResetIssueBoardInput(board_path=str(board_path)))
assert result.reset is True
assert not board_path.exists()
- Step 2: Run test and verify red
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_issue_board_creates_selected_issues -q -n0
Expected: import failure because issue_board_source.py does not exist.
- Step 3: Implement issue board source
Create examples/lda_report_workflow/issue_board_source.py:
from __future__ import annotations
import json
from pathlib import Path
from pydantic import BaseModel, Field
from wf_authoring import node
from .report_source import CreatedIssue, ProposedIssue
_EXAMPLE_DIR = Path(__file__).resolve().parent
class ResetIssueBoardInput(BaseModel):
board_path: str = Field(default="issue-board.json")
class ResetIssueBoardOutput(BaseModel):
reset: bool
board_path: str
class CreateIssuesInput(BaseModel):
issues: list[ProposedIssue]
selected_issue_ids: list[str]
board_path: str = Field(default="issue-board.json")
class CreateIssuesOutput(BaseModel):
created_issues: list[CreatedIssue]
board_path: str
@node(name="reset_issue_board", description="Reset the local demo issue board file.")
def reset_issue_board(payload: ResetIssueBoardInput) -> ResetIssueBoardOutput:
return _reset_issue_board(payload)
@node(name="create_issues", description="Create selected issues in the local demo issue board.")
def create_issues(payload: CreateIssuesInput) -> CreateIssuesOutput:
return _create_issues(payload)
def _reset_issue_board(payload: ResetIssueBoardInput) -> ResetIssueBoardOutput:
path = _resolve_board_path(payload.board_path)
path.unlink(missing_ok=True)
return ResetIssueBoardOutput(reset=True, board_path=str(path))
def _create_issues(payload: CreateIssuesInput) -> CreateIssuesOutput:
path = _resolve_board_path(payload.board_path)
selected = set(payload.selected_issue_ids)
existing = _read_board(path)
created: list[CreatedIssue] = []
next_number = len(existing) + 1
for issue in payload.issues:
if issue.id not in selected:
continue
issue_id = f"ISSUE-{next_number:03d}"
created_issue = CreatedIssue(
id=issue_id,
title=issue.title,
url=f"local://issue-board/{issue_id}",
)
existing.append(
{
"id": created_issue.id,
"title": created_issue.title,
"url": created_issue.url,
"body": issue.body,
"severity": issue.severity,
}
)
created.append(created_issue)
next_number += 1
_write_board(path, existing)
return CreateIssuesOutput(created_issues=created, board_path=str(path))
def _resolve_board_path(path: str) -> Path:
candidate = Path(path)
if candidate.is_absolute():
resolved = candidate.resolve()
else:
resolved = (_EXAMPLE_DIR / candidate).resolve()
if not resolved.is_relative_to(_EXAMPLE_DIR) and not resolved.is_relative_to(Path.cwd()):
raise ValueError("issue board path must stay inside the example or current workspace")
return resolved
def _read_board(path: Path) -> list[dict[str, str]]:
if not path.exists():
return []
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, list):
raise ValueError("issue board file must contain a JSON list")
return [item for item in value if isinstance(item, dict)]
def _write_board(path: Path, value: list[dict[str, str]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(value, indent=2, sort_keys=True), encoding="utf-8")
tmp.replace(path)
registry = [reset_issue_board, create_issues]
- Step 4: Run issue board tests
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_issue_board_creates_selected_issues tests/examples/test_lda_report_workflow_example.py::test_issue_board_reset_removes_existing_file -q -n0
Expected: pass.
- Step 5: Commit
git add examples/lda_report_workflow/issue_board_source.py tests/examples/test_lda_report_workflow_example.py
git commit -m "feat: add local issue board source example"
Task 5: Add Config, Run Input, And Workflow Builder
Files:
-
Create:
examples/lda_report_workflow/wf.config.json -
Create:
examples/lda_report_workflow/run-input.json -
Create:
examples/lda_report_workflow/build_workflow.py -
Create:
examples/lda_report_workflow/workflow.plan.json -
Modify:
tests/examples/test_lda_report_workflow_example.py -
Step 1: Create workflow config
Create examples/lda_report_workflow/wf.config.json:
{
"server": {
"store": {
"kind": "filesystem",
"root": ".wf_lda_report_store"
},
"transport": {
"kind": "rpc_http",
"host": "127.0.0.1",
"port": 8765,
"path": "/rpc"
},
"sources": [
{
"id": "local.lda_docs",
"kind": "python",
"path": ".",
"module": "document_source",
"registry": "registry"
},
{
"id": "local.lda_report",
"kind": "python",
"path": ".",
"module": "report_source",
"registry": "registry"
},
{
"id": "local.issue_board",
"kind": "python",
"path": ".",
"module": "issue_board_source",
"registry": "registry"
}
]
}
}
- Step 2: Create run input
Create examples/lda_report_workflow/run-input.json:
{
"selected_documents": [
"project-brief.md",
"architecture-notes.md",
"evaluation-findings.md",
"risk-register.md",
"roadmap.md"
],
"board_path": "issue-board.json"
}
- Step 3: Add config loading test
Add these imports to the top import block in
tests/examples/test_lda_report_workflow_example.py:
from wf_config import load_workflow_config
from wf_server.config import build_workflow_server_from_workflow_config
Append this test:
@pytest.mark.asyncio
async def test_lda_report_workflow_config_loads_sources(tmp_path: Path) -> None:
config = load_workflow_config(EXAMPLE_DIR / "wf.config.json")
config.server.store.root = tmp_path / "store"
server = build_workflow_server_from_workflow_config(config)
listed = await server.api.list_capabilities(source_id="local.lda_report")
names = {capability["name"] for capability in listed["capabilities"]}
assert "local.lda_report.build_report" in names
assert "local.lda_report.finalise_report" in names
- Step 4: Run config test and verify green
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_config_loads_sources -q -n0
Expected: pass.
- Step 5: Create workflow builder
Create examples/lda_report_workflow/build_workflow.py:
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from wf_api.models import RawWorkflowPlan
from wf_authoring import WorkflowBuilder
from wf_core import Workflow
HERE = Path(__file__).resolve().parent
WORKFLOW_OUTPUT = [
{"path": "state.approved", "target": "approved"},
{"path": "state.final_markdown", "target": "markdown"},
{"path": "state.created_issues", "target": "created_issues"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
]
def build_workflow() -> Workflow:
"""Build the demo workflow with the public authoring API.
`WorkflowBuilder` does not yet expose a workflow-output setter, so this
module adds the final output projection in `_with_workflow_output()` after
compiling the graph. Keep that seam small and validated.
"""
builder = WorkflowBuilder(
name="lda_report_case_study",
input_schema={
"type": "object",
"properties": {
"selected_documents": {
"type": "array",
"items": {"type": "string"},
},
"board_path": {"type": "string"},
},
"required": ["selected_documents", "board_path"],
},
state_schema={
"type": "object",
"properties": {
"documents": {"type": "array"},
"analysis": {"type": "array"},
"report": {"type": "object"},
"report_markdown": {"type": "string"},
"proposed_issues": {"type": "array"},
"selected_issue_ids": {"type": "array"},
"approval_comment": {"type": "string"},
"approved": {"type": "boolean"},
"created_issues": {"type": "array"},
"final_markdown": {"type": "string"},
},
},
output_schema={
"type": "object",
"properties": {
"approved": {"type": "boolean"},
"markdown": {"type": "string"},
"created_issues": {"type": "array"},
"selected_issue_ids": {"type": "array"},
},
},
outcomes=["completed", "cancelled"],
)
read_docs = builder.use_ref(
"local.lda_docs.read_documents",
id="read_docs",
input=[{"path": "input.selected_documents", "target": "names"}],
output=[{"source": "documents", "target": "state.documents"}],
)
analyze = builder.use_ref(
"local.lda_report.analyze_documents",
id="analyze",
input=[{"path": "state.documents", "target": "documents"}],
output=[{"source": "analysis", "target": "state.analysis"}],
)
build_report = builder.use_ref(
"local.lda_report.build_report",
id="build_report",
input=[{"path": "state.analysis", "target": "analysis"}],
output=[
{"source": "report", "target": "state.report"},
{"source": "markdown", "target": "state.report_markdown"},
],
)
draft_issues = builder.use_ref(
"local.lda_report.create_issue_drafts",
id="draft_issues",
input=[{"path": "state.report", "target": "report"}],
output=[{"source": "issues", "target": "state.proposed_issues"}],
)
review_issues = builder.interrupt(
id="review_issues",
kind="issue_review",
request=[
{"path": "state.report_markdown", "target": "report_markdown"},
{"path": "state.proposed_issues", "target": "proposed_issues"},
],
resume=[
{"source": "approved", "target": "state.approved"},
{"source": "selected_issue_ids", "target": "state.selected_issue_ids"},
{"source": "comment", "target": "state.approval_comment"},
],
outcomes=["submitted", "cancelled"],
request_schema={
"type": "object",
"properties": {
"report_markdown": {"type": "string"},
"proposed_issues": {"type": "array"},
},
"required": ["report_markdown", "proposed_issues"],
"additionalProperties": False,
},
resume_schema={
"type": "object",
"properties": {
"approved": {"type": "boolean"},
"selected_issue_ids": {
"type": "array",
"items": {"type": "string"},
},
"comment": {"type": "string"},
},
"required": ["approved", "selected_issue_ids"],
"additionalProperties": False,
},
)
create_issues = builder.use_ref(
"local.issue_board.create_issues",
id="create_issues",
input=[
{"path": "state.proposed_issues", "target": "issues"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
{"path": "input.board_path", "target": "board_path"},
],
output=[{"source": "created_issues", "target": "state.created_issues"}],
)
finalise = builder.use_ref(
"local.lda_report.finalise_report",
id="finalise",
input=[
{"path": "state.report", "target": "report"},
{"path": "state.created_issues", "target": "created_issues"},
{"path": "state.approved", "target": "approved"},
{"path": "state.selected_issue_ids", "target": "selected_issue_ids"},
{"path": "state.approval_comment", "target": "comment"},
],
output=[{"source": "markdown", "target": "state.final_markdown"}],
)
revision_requested = builder.use_ref(
"local.lda_report.record_revision_request",
id="revision_requested",
input=[{"path": "state.approval_comment", "target": "comment"}],
output=[
{"source": "approved", "target": "state.approved"},
{"source": "markdown", "target": "state.final_markdown"},
{"source": "created_issues", "target": "state.created_issues"},
{"source": "selected_issue_ids", "target": "state.selected_issue_ids"},
],
)
end_completed = builder.end("completed", id="end_completed")
end_cancelled = builder.end("cancelled", id="end_cancelled")
builder.set_entry_point(read_docs)
builder.connect(read_docs, "ok", analyze)
builder.connect(analyze, "ok", build_report)
builder.connect(build_report, "ok", draft_issues)
builder.branch(
review_issues,
{
"submitted": create_issues,
"cancelled": revision_requested,
},
)
builder.connect(create_issues, "ok", finalise)
builder.connect(finalise, "ok", end_completed)
builder.connect(revision_requested, "ok", end_cancelled)
return _with_workflow_output(builder.compile())
def _with_workflow_output(workflow: Workflow) -> Workflow:
payload = workflow.model_dump(mode="json", by_alias=True)
payload["output"] = WORKFLOW_OUTPUT
return Workflow.model_validate(payload)
def workflow_plan_payload() -> dict[str, Any]:
payload = build_workflow().model_dump(mode="json", by_alias=True)
payload.pop("node_defs", None)
RawWorkflowPlan.model_validate(payload)
return payload
def write_plan(path: Path = HERE / "workflow.plan.json") -> None:
payload = workflow_plan_payload()
path.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
write_plan()
- Step 6: Generate raw workflow plan
Run:
uv run python examples/lda_report_workflow/build_workflow.py
Expected: examples/lda_report_workflow/workflow.plan.json is written.
- Step 7: Add builder/generator regression test
Add these imports to the top import block:
import json
from examples.lda_report_workflow.build_workflow import (
build_workflow,
workflow_plan_payload,
)
from wf_api.models import RawWorkflowPlan
Append this test:
def test_lda_report_workflow_builder_generates_committed_raw_plan() -> None:
workflow = build_workflow()
payload = workflow_plan_payload()
committed = json.loads(
(EXAMPLE_DIR / "workflow.plan.json").read_text(encoding="utf-8")
)
validated = RawWorkflowPlan.model_validate(payload)
assert workflow.name == "lda_report_case_study"
assert validated.name == "lda_report_case_study"
assert any(node.id == "review_issues" for node in validated.nodes)
assert payload == committed
- Step 8: Run builder and config tests
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_config_loads_sources tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_builder_generates_committed_raw_plan -q -n0
Expected: pass.
- Step 9: Commit config, builder, and plan
git add examples/lda_report_workflow/wf.config.json examples/lda_report_workflow/run-input.json examples/lda_report_workflow/build_workflow.py examples/lda_report_workflow/workflow.plan.json tests/examples/test_lda_report_workflow_example.py
git commit -m "feat: add lda report workflow builder"
Task 6: Test Full Artifact, Interrupt, Resume Path
Files:
-
Modify:
tests/examples/test_lda_report_workflow_example.py -
Step 1: Add full lifecycle test
Append to tests/examples/test_lda_report_workflow_example.py. Do not add a
second import json; Task 5 already added it to the top import block.
@pytest.mark.asyncio
async def test_lda_report_workflow_artifact_interrupt_resume_path(tmp_path: Path) -> None:
config = load_workflow_config(EXAMPLE_DIR / "wf.config.json")
config.server.store.root = tmp_path / "store"
server = build_workflow_server_from_workflow_config(config)
plan = json.loads((EXAMPLE_DIR / "workflow.plan.json").read_text(encoding="utf-8"))
await server.api.create_artifact_from_plan(
artifact_id="lda_report_case_study",
version=1,
title="lda.chat Report Case Study",
plan=plan,
outcomes=["completed", "cancelled"],
source_bindings={
"local.lda_docs": "local.lda_docs",
"local.lda_report": "local.lda_report",
"local.issue_board": "local.issue_board",
},
)
await server.api.save_deployment(
{
"id": "lda_report_case_study.default",
"artifact_id": "lda_report_case_study",
"artifact_version": 1,
"bindings": {
"local.lda_docs": "local.lda_docs",
"local.lda_report": "local.lda_report",
"local.issue_board": "local.issue_board",
},
}
)
run_input = json.loads((EXAMPLE_DIR / "run-input.json").read_text(encoding="utf-8"))
run_input["board_path"] = str(tmp_path / "issue-board.json")
started = await server.api.run_deployment(
deployment_id="lda_report_case_study.default",
workflow_input=run_input,
)
assert started["status"] == "interrupted"
assert started["interrupt"]["kind"] == "issue_review"
assert started["interrupt"]["typed"] is True
assert started["interrupt"]["request_schema"]["required"] == [
"report_markdown",
"proposed_issues",
]
assert started["interrupt"]["resume_schema"]["required"] == [
"approved",
"selected_issue_ids",
]
proposed_ids = [
issue["id"] for issue in started["interrupt"]["payload"]["proposed_issues"]
]
assert proposed_ids
resumed = await server.api.resume_run(
run_id=started["run_id"],
resume_payload={
"approved": True,
"selected_issue_ids": proposed_ids[:2],
"comment": "Create selected issues before the defense.",
},
resume_outcome="submitted",
)
assert resumed["status"] == "completed"
assert resumed["outcome"] == "completed"
assert resumed["output"]["approved"] is True
assert resumed["output"]["created_issues"]
assert resumed["output"]["markdown"].startswith(
"# lda.chat Thesis And Project Readiness Report"
)
- Step 2: Run full lifecycle test
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py::test_lda_report_workflow_artifact_interrupt_resume_path -q -n0
Expected: pass. If validation fails because the raw plan schema is too loose, fix the plan rather than loosening the test.
- Step 3: Add cancellation path test
Append:
@pytest.mark.asyncio
async def test_lda_report_workflow_cancelled_resume_path(tmp_path: Path) -> None:
config = load_workflow_config(EXAMPLE_DIR / "wf.config.json")
config.server.store.root = tmp_path / "store"
server = build_workflow_server_from_workflow_config(config)
plan = json.loads((EXAMPLE_DIR / "workflow.plan.json").read_text(encoding="utf-8"))
await server.api.create_artifact_from_plan(
artifact_id="lda_report_cancel_case",
version=1,
title="lda.chat Report Cancel Case",
plan=plan,
outcomes=["completed", "cancelled"],
source_bindings={
"local.lda_docs": "local.lda_docs",
"local.lda_report": "local.lda_report",
"local.issue_board": "local.issue_board",
},
)
await server.api.save_deployment(
{
"id": "lda_report_cancel_case.default",
"artifact_id": "lda_report_cancel_case",
"artifact_version": 1,
"bindings": {
"local.lda_docs": "local.lda_docs",
"local.lda_report": "local.lda_report",
"local.issue_board": "local.issue_board",
},
}
)
run_input = json.loads((EXAMPLE_DIR / "run-input.json").read_text(encoding="utf-8"))
run_input["board_path"] = str(tmp_path / "issue-board.json")
started = await server.api.run_deployment(
deployment_id="lda_report_cancel_case.default",
workflow_input=run_input,
)
resumed = await server.api.resume_run(
run_id=started["run_id"],
resume_payload={
"approved": False,
"selected_issue_ids": [],
"comment": "Revise risk wording.",
},
resume_outcome="cancelled",
)
assert resumed["status"] == "completed"
assert resumed["outcome"] == "cancelled"
assert resumed["output"]["approved"] is False
assert "Revision Requested" in resumed["output"]["markdown"]
- Step 4: Run all example tests
Run:
uv run pytest tests/examples/test_lda_report_workflow_example.py -q -n0
Expected: pass.
- Step 5: Commit lifecycle tests
git add tests/examples/test_lda_report_workflow_example.py
git commit -m "test: prove lda report workflow lifecycle"
Task 7: Add README And Docs
Files:
-
Create:
examples/lda_report_workflow/README.md -
Modify:
docs/current_roadmap.md -
Modify:
docs/add/evidence-index.mdif present and live -
Modify:
docs/add/system-design-implementation.mdif present and live -
Step 1: Create README
Create examples/lda_report_workflow/README.md:
# lda.chat Report Workflow
This example is a deterministic case study for the Workflow Console and defense
demo. It uses local fixture documents, trusted Python sources, a typed
`issue_review` interrupt, and a local JSON-backed issue board.
It does not call Google Drive, email, GitHub, or an LLM.
## Sources
- `local.lda_docs`: lists and reads deterministic project documents.
- `local.lda_report`: analyses documents, builds the readiness report, creates
proposed issue drafts, and finalises the report.
- `local.issue_board`: writes selected issues to a local JSON file.
## Workflow Definition
`workflow.plan.json` is generated from `build_workflow.py`. After changing the
graph, regenerate and review the raw plan:
```powershell
uv run python examples/lda_report_workflow/build_workflow.py
Product Path
From the repository root:
uv run wf --config examples/lda_report_workflow/wf.config.json config validate
uv run wf --config examples/lda_report_workflow/wf.config.json --local cap list --source local.lda_report
uv run wf --config examples/lda_report_workflow/wf.config.json --local artifact create-from-plan examples/lda_report_workflow/workflow.plan.json --artifact lda_report_case_study --version 1 --title "lda.chat Report Case Study" --outcome completed --outcome cancelled --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board
uv run wf --config examples/lda_report_workflow/wf.config.json --local deploy save lda_report_case_study.default --artifact lda_report_case_study --version 1 --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board
uv run wf --config examples/lda_report_workflow/wf.config.json --local run start lda_report_case_study.default --input-file examples/lda_report_workflow/run-input.json
The run stops at an issue_review interrupt. Inspect it:
uv run wf --config examples/lda_report_workflow/wf.config.json --local run inspect <run_id>
Resume with selected issues:
uv run wf --config examples/lda_report_workflow/wf.config.json --local run resume <run_id> --payload '{"approved":true,"selected_issue_ids":["risk-1"],"comment":"Create selected issues."}'
Cleanup
The example writes .wf_lda_report_store/ and issue-board.json, both ignored
by git.
- [ ] **Step 2: Update roadmap**
In `docs/current_roadmap.md`, under the Workflow Console initiative,
change item 2 to completed after the example is tested:
```md
2. Completed: deterministic `examples/lda_report_workflow/` case study with
local document, report, issue-board sources, and typed issue-review
interrupt.
- Step 3: Update evidence docs if live
If docs/add/evidence-index.md exists and still tracks live thesis evidence,
add:
- `examples/lda_report_workflow/` and
`tests/examples/test_lda_report_workflow_example.py`: deterministic report
workflow with typed issue-review interrupt and local issue-board source.
If docs/add/system-design-implementation.md exists and is still used as a
source document, add one sentence to the examples/evidence section:
The `lda_report_workflow` example extends the deterministic report case study
with a typed human approval interrupt and local issue-board side effect for the
Workflow Console demo.
- Step 4: Run docs tests
Run:
uv run pytest tests/docs -q -n0
Expected: pass.
- Step 5: Commit docs
git add examples/lda_report_workflow/README.md docs/current_roadmap.md docs/add/evidence-index.md docs/add/system-design-implementation.md
git commit -m "docs: add lda report workflow runbook"
If either docs/add/... file was not changed, omit it from git add.
Task 8: Final Verification And Archive Plan
Files:
-
Move:
docs/superpowers/plans/2026-07-01-lda-report-workflow-example.mdtodocs/historical/superpowers/plans/2026-07-01-lda-report-workflow-example.md -
Step 1: Run focused verification
Run:
uv run python examples/lda_report_workflow/build_workflow.py
git diff --exit-code examples/lda_report_workflow/workflow.plan.json
uv run pytest tests/examples/test_lda_report_workflow_example.py tests/wf_sources_python/test_loader.py -q -n0
uv run ruff check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
uv run ruff format --check examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
uv run basedpyright --level error examples/lda_report_workflow tests/examples/test_lda_report_workflow_example.py
Expected: generation exits 0, git diff --exit-code reports no generated-plan
diff, and all checks pass. If basedpyright reports import-package issues for
the example package, add examples/lda_report_workflow/__init__.py and rerun.
- Step 2: Smoke the CLI path
Run:
uv run wf --config examples/lda_report_workflow/wf.config.json --local config validate
uv run wf --config examples/lda_report_workflow/wf.config.json --local cap list --source local.lda_report --format ids
Expected:
-
config validation exits 0;
-
capability list includes
local.lda_report.build_report. -
Step 3: Archive plan
git mv docs/superpowers/plans/2026-07-01-lda-report-workflow-example.md docs/historical/superpowers/plans/2026-07-01-lda-report-workflow-example.md
- Step 4: Commit final archive/polish
git add docs/historical/superpowers/plans/2026-07-01-lda-report-workflow-example.md docs/current_roadmap.md
git commit -m "docs: archive lda report workflow plan"
If no files changed after the archive move, commit only the move.
Final Report Requirements
The implementing agent must report:
- files changed;
- commands run and results;
- whether the workflow interrupts with
kind="issue_review"; - whether inspect exposes
request_schema,resume_schema,outcomes, andtyped; - whether submitted resume creates issues and completes;
- whether cancelled resume completes without creating issues;
- any deviations from this plan.
Plan Self-Review
Spec coverage:
- deterministic documents: Task 1;
- first-party document/report/issue-board sources: Tasks 2-4;
- authoring builder, generated raw plan, and config: Task 5;
- typed issue-review interrupt and resume: Task 6;
- product runbook and roadmap: Task 7;
- final verification/archive: Task 8.
Placeholder scan:
- No red-flag placeholder tokens or unspecified implementation work remain.
Type consistency:
- Source ids are consistently
local.lda_docs,local.lda_report, andlocal.issue_board. - Artifact/deployment ids are consistently
lda_report_case_studyandlda_report_case_study.default. - Interrupt kind is consistently
issue_review.