tenseleyflow/loader / 8893a2b

Browse files

Show workflow ledger in inspection surfaces

Authored by espadonne
SHA
8893a2babdc532089e833fd6a810aaa630d621db
Parents
383a8c1
Tree
13fbe06

3 changed files

StatusFile+-
M src/loader/cli/main.py 41 0
M src/loader/runtime/inspection.py 6 1
M tests/test_inspection.py 62 0
src/loader/cli/main.pymodified
@@ -1564,6 +1564,12 @@ def _workflow_show_main(
15641564
             snapshot.highlights,
15651565
             title="[bold blue]Workflow Answers[/bold blue]",
15661566
         )
1567
+    if snapshot.workflow_ledger.has_items():
1568
+        console.print()
1569
+        _print_workflow_ledger(
1570
+            snapshot.workflow_ledger,
1571
+            title="[bold blue]Workflow Ledger[/bold blue]",
1572
+        )
15671573
     console.print()
15681574
     _print_workflow_timeline_entries(
15691575
         snapshot.entries,
@@ -1845,6 +1851,41 @@ def _format_workflow_timeline_context(entry) -> str:
18451851
     return ", ".join(parts) or "-"
18461852
 
18471853
 
1854
+def _print_workflow_ledger(
1855
+    ledger,
1856
+    *,
1857
+    title: str,
1858
+) -> None:
1859
+    table = Table(show_header=False, box=None)
1860
+    table.add_column("Section", style="bold cyan")
1861
+    table.add_column("Details", style="white")
1862
+
1863
+    for label, items in (
1864
+        ("Assumptions", ledger.assumptions),
1865
+        ("Acceptance Anchors", ledger.acceptance_anchors),
1866
+        ("Decision Boundaries", ledger.decision_boundaries),
1867
+    ):
1868
+        if not items:
1869
+            continue
1870
+        table.add_row(
1871
+            label,
1872
+            "\n".join(_format_workflow_ledger_item(item) for item in items[:4]),
1873
+        )
1874
+
1875
+    console.print(Panel.fit(table, title=title, border_style="blue"))
1876
+
1877
+
1878
+def _format_workflow_ledger_item(item) -> str:
1879
+    details = [item.status]
1880
+    if item.updated_phase and item.updated_phase != item.introduced_phase:
1881
+        details.append(f"updated={item.updated_phase}")
1882
+    elif item.introduced_phase:
1883
+        details.append(f"phase={item.introduced_phase}")
1884
+    if item.evidence:
1885
+        details.append(f"evidence={item.evidence[0]}")
1886
+    return f"- {item.text} ({', '.join(details)})"
1887
+
1888
+
18481889
 def _print_workflow_highlights(
18491890
     highlights: list[str],
18501891
     *,
src/loader/runtime/inspection.pymodified
@@ -26,6 +26,7 @@ from .permissions import (
2626
 )
2727
 from .prompting import build_system_prompt_result
2828
 from .session import SessionSnapshot, SessionStore
29
+from .workflow_ledger import WorkflowLedger, workflow_ledger_highlights
2930
 from .workflow_policy import WorkflowTimelineEntry
3031
 
3132
 
@@ -251,6 +252,7 @@ class WorkflowTimelineSnapshot:
251252
     entry_limit: int | None = None
252253
     highlights: list[str] = field(default_factory=list)
253254
     entries: list[WorkflowTimelineEntry] = field(default_factory=list)
255
+    workflow_ledger: WorkflowLedger = field(default_factory=WorkflowLedger)
254256
 
255257
 
256258
 def capability_summary(profile: CapabilityProfile) -> str:
@@ -656,6 +658,7 @@ def collect_workflow_timeline(
656658
             entry_limit=limit,
657659
             highlights=[],
658660
             entries=[],
661
+            workflow_ledger=WorkflowLedger(),
659662
         )
660663
 
661664
     filtered_entries = list(snapshot.workflow_timeline)
@@ -664,6 +667,7 @@ def collect_workflow_timeline(
664667
     if kind:
665668
         filtered_entries = [entry for entry in filtered_entries if entry.kind == kind]
666669
     highlights = _workflow_timeline_highlights(filtered_entries)
670
+    highlights.extend(workflow_ledger_highlights(snapshot.workflow_ledger))
667671
     if limit is not None:
668672
         filtered_entries = filtered_entries[-limit:]
669673
 
@@ -677,8 +681,9 @@ def collect_workflow_timeline(
677681
         selected_mode=mode,
678682
         selected_kind=kind,
679683
         entry_limit=limit,
680
-        highlights=highlights,
684
+        highlights=list(dict.fromkeys(highlights)),
681685
         entries=filtered_entries,
686
+        workflow_ledger=snapshot.workflow_ledger.copy(),
682687
     )
683688
 
684689
 
tests/test_inspection.pymodified
@@ -23,6 +23,7 @@ from loader.runtime.inspection import (
2323
     load_session_detail,
2424
 )
2525
 from loader.runtime.session import SessionSnapshot, SessionStore
26
+from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem
2627
 from loader.runtime.workflow_policy import WorkflowTimelineEntry
2728
 
2829
 
@@ -210,6 +211,40 @@ def _persist_session_with_rich_workflow(temp_dir: Path) -> str:
210211
                 signal_summary=["verify_pressure=low"],
211212
             ),
212213
         ],
214
+        workflow_ledger=WorkflowLedger(
215
+            assumptions=[
216
+                WorkflowLedgerItem(
217
+                    text="notes.txt stays out of scope unless clarified otherwise.",
218
+                    status="contradicted",
219
+                    introduced_phase="clarify",
220
+                    updated_phase="recovery",
221
+                    evidence=["Clarify scope assumed `notes.txt` stayed out of scope."],
222
+                )
223
+            ],
224
+            acceptance_anchors=[
225
+                WorkflowLedgerItem(
226
+                    text="notes.txt exists in the workspace root.",
227
+                    status="changed",
228
+                    introduced_phase="clarify",
229
+                    updated_phase="recovery",
230
+                    evidence=[
231
+                        (
232
+                            "Failed verification exposed missing brief coverage for "
233
+                            "`notes.txt exists`."
234
+                        )
235
+                    ],
236
+                )
237
+            ],
238
+            decision_boundaries=[
239
+                WorkflowLedgerItem(
240
+                    text="Escalate before broad UX changes.",
241
+                    status="reopened",
242
+                    introduced_phase="clarify",
243
+                    updated_phase="recovery",
244
+                    evidence=["The active task framing outgrew the persisted clarify brief."],
245
+                )
246
+            ],
247
+        ),
213248
     )
214249
     SessionStore(temp_dir).save(snapshot)
215250
     return snapshot.session_id
@@ -417,6 +452,11 @@ def test_collect_workflow_timeline_supports_filters_and_highlights(
417452
         "decision_boundaries",
418453
     ]
419454
     assert any(item.startswith("Asked again:") for item in snapshot.highlights)
455
+    assert snapshot.workflow_ledger.assumptions[0].status == "contradicted"
456
+    assert any(
457
+        item.startswith("Contradicted assumptions:")
458
+        for item in snapshot.highlights
459
+    )
420460
 
421461
 
422462
 def test_status_and_session_commands_render_persisted_state(
@@ -477,6 +517,28 @@ def test_status_and_session_commands_render_persisted_state(
477517
     assert "next=verify" in workflow_result.output
478518
 
479519
 
520
+def test_workflow_show_renders_workflow_ledger(
521
+    temp_dir: Path,
522
+    monkeypatch: pytest.MonkeyPatch,
523
+) -> None:
524
+    _write_python_workspace(temp_dir)
525
+    _ensure_loader_dirs(temp_dir)
526
+    _persist_session_with_rich_workflow(temp_dir)
527
+    runner = CliRunner()
528
+
529
+    monkeypatch.chdir(temp_dir)
530
+
531
+    result = runner.invoke(cli_main_module.workflow_cli, ["show"])
532
+
533
+    assert result.exit_code == 0
534
+    assert "Workflow Ledger" in result.output
535
+    assert "Assumptions" in result.output
536
+    assert "contradicted" in result.output
537
+    assert "notes.txt stays out of scope" in result.output
538
+    assert "Acceptance Anchors" in result.output
539
+    assert "Decision Boundaries" in result.output
540
+
541
+
480542
 def test_workflow_show_command_supports_filters_and_highlights(
481543
     temp_dir: Path,
482544
     monkeypatch: pytest.MonkeyPatch,