tenseleyflow/loader / 75139ae

Browse files

Add prompt and artifact diff inspection surfaces

Authored by espadonne
SHA
75139aee1829354e1efaee5cb07f9df2feb9050a
Parents
6f05b29
Tree
359b983

3 changed files

StatusFile+-
M src/loader/cli/main.py 147 0
M src/loader/runtime/inspection.py 307 0
M tests/test_inspection.py 178 0
src/loader/cli/main.pymodified
@@ -17,13 +17,17 @@ from ..runtime.inspection import (
1717
     DoctorReport,
1818
     PermissionCheckResult,
1919
     PermissionSnapshot,
20
+    PromptDiffSnapshot,
2021
     PromptPreview,
2122
     StatusSnapshot,
23
+    WorkflowArtifactDiffSnapshot,
2224
     WorkflowTimelineSnapshot,
2325
     collect_doctor_report,
2426
     collect_permission_snapshot,
27
+    collect_prompt_diff,
2528
     collect_prompt_preview,
2629
     collect_status_snapshot,
30
+    collect_workflow_artifact_diffs,
2731
     collect_workflow_timeline,
2832
     dry_run_permission_check,
2933
     list_session_summaries,
@@ -953,6 +957,15 @@ def prompt_show_cli(
953957
     )
954958
 
955959
 
960
+@prompt_cli.command("diff")
961
+@click.option("--full", is_flag=True, help="Show the full unified prompt diff")
962
+@click.argument("session_id", required=False)
963
+def prompt_diff_cli(full: bool, session_id: str | None) -> None:
964
+    """Compare the latest persisted prompt contracts."""
965
+
966
+    _prompt_diff_main(session_id=session_id, full=full)
967
+
968
+
956969
 @workflow_cli.command("show")
957970
 @click.option(
958971
     "--mode",
@@ -984,11 +997,15 @@ def prompt_show_cli(
984997
     show_default=True,
985998
     help="Show only the most recent matching entries",
986999
 )
1000
+@click.option("--diff", "show_diff", is_flag=True, help="Show persisted artifact diffs")
1001
+@click.option("--full-diff", is_flag=True, help="Show the full unified artifact diffs")
9871002
 @click.argument("session_id", required=False)
9881003
 def workflow_show_cli(
9891004
     mode: str | None,
9901005
     kind: str | None,
9911006
     limit: int,
1007
+    show_diff: bool,
1008
+    full_diff: bool,
9921009
     session_id: str | None,
9931010
 ) -> None:
9941011
     """Show the persisted workflow timeline for the latest or named session."""
@@ -998,6 +1015,8 @@ def workflow_show_cli(
9981015
         mode=mode,
9991016
         kind=kind,
10001017
         limit=limit,
1018
+        show_diff=show_diff,
1019
+        full_diff=full_diff,
10011020
     )
10021021
 
10031022
 
@@ -1049,6 +1068,7 @@ def _loader_help_text() -> str:
10491068
             "  loader status              Show persisted runtime status",
10501069
             "  loader explore <prompt>    Run a fast read-only lookup query",
10511070
             "  loader prompt show         Preview the current prompt contract",
1071
+            "  loader prompt diff         Compare the latest persisted prompt contracts",
10521072
             "  loader permissions show    Display normalized permission rules",
10531073
             "  loader permissions check   Dry-run one permission decision",
10541074
             "  loader workflow show       Show the persisted workflow timeline",
@@ -1079,6 +1099,7 @@ def _prompt_help_text() -> str:
10791099
             "",
10801100
             "Commands:",
10811101
             "  show [task]  Render the current prompt contract without a model call",
1102
+            "  diff [id]    Compare the latest persisted prompt contracts",
10821103
         ]
10831104
     )
10841105
 
@@ -1090,6 +1111,7 @@ def _workflow_help_text() -> str:
10901111
             "",
10911112
             "Commands:",
10921113
             "  show [id]  Show the persisted workflow timeline with optional filters",
1114
+            "            Add --diff to compare the latest persisted artifacts",
10931115
         ]
10941116
     )
10951117
 
@@ -1518,6 +1540,8 @@ def _workflow_show_main(
15181540
     mode: str | None,
15191541
     kind: str | None,
15201542
     limit: int | None,
1543
+    show_diff: bool,
1544
+    full_diff: bool,
15211545
 ) -> None:
15221546
     try:
15231547
         snapshot: WorkflowTimelineSnapshot = collect_workflow_timeline(
@@ -1570,6 +1594,13 @@ def _workflow_show_main(
15701594
             snapshot.workflow_ledger,
15711595
             title="[bold blue]Workflow Ledger[/bold blue]",
15721596
         )
1597
+    if show_diff:
1598
+        console.print()
1599
+        artifact_diffs = collect_workflow_artifact_diffs(session_id=session_id)
1600
+        _print_workflow_artifact_diffs(
1601
+            artifact_diffs,
1602
+            show_full=full_diff,
1603
+        )
15731604
     console.print()
15741605
     _print_workflow_timeline_entries(
15751606
         snapshot.entries,
@@ -1600,6 +1631,24 @@ def _prompt_show_main(
16001631
     _print_prompt_preview(preview)
16011632
 
16021633
 
1634
+def _prompt_diff_main(
1635
+    *,
1636
+    session_id: str | None,
1637
+    full: bool,
1638
+) -> None:
1639
+    try:
1640
+        snapshot: PromptDiffSnapshot = collect_prompt_diff(session_id=session_id)
1641
+    except FileNotFoundError:
1642
+        console.print(f"[red]Session not found:[/red] {session_id}")
1643
+        raise SystemExit(1) from None
1644
+
1645
+    if snapshot.session_id is None or snapshot.current is None:
1646
+        console.print("[yellow]No persisted prompt history found.[/yellow]")
1647
+        return
1648
+
1649
+    _print_prompt_diff(snapshot, show_full=full)
1650
+
1651
+
16031652
 def _permissions_check_main(
16041653
     *,
16051654
     tool_name: str,
@@ -1787,6 +1836,104 @@ def _print_prompt_preview(preview: PromptPreview) -> None:
17871836
     )
17881837
 
17891838
 
1839
+def _print_prompt_diff(snapshot: PromptDiffSnapshot, *, show_full: bool) -> None:
1840
+    table = Table(show_header=False, box=None)
1841
+    table.add_column("Field", style="bold cyan")
1842
+    table.add_column("Value", style="white")
1843
+    table.add_row("Workspace", str(snapshot.project_root))
1844
+    table.add_row("Session", snapshot.session_id or "none")
1845
+    table.add_row("Task", snapshot.current_task or "none")
1846
+    table.add_row(
1847
+        "Current",
1848
+        _format_prompt_snapshot(snapshot.current) if snapshot.current else "none",
1849
+    )
1850
+    table.add_row(
1851
+        "Previous",
1852
+        _format_prompt_snapshot(snapshot.previous) if snapshot.previous else "none",
1853
+    )
1854
+    console.print(
1855
+        Panel.fit(
1856
+            table,
1857
+            title="[bold blue]Prompt Diff[/bold blue]",
1858
+            border_style="blue",
1859
+        )
1860
+    )
1861
+
1862
+    if snapshot.highlights:
1863
+        console.print()
1864
+        _print_workflow_highlights(
1865
+            snapshot.highlights,
1866
+            title="[bold blue]Prompt Changes[/bold blue]",
1867
+        )
1868
+
1869
+    if show_full:
1870
+        body = snapshot.unified_diff or "[dim]No prompt body diff available.[/dim]"
1871
+        console.print()
1872
+        console.print(
1873
+            Panel(
1874
+                body,
1875
+                title="[bold blue]Prompt Unified Diff[/bold blue]",
1876
+                border_style="blue",
1877
+            )
1878
+        )
1879
+
1880
+
1881
+def _format_prompt_snapshot(snapshot) -> str:
1882
+    parts = [snapshot.workflow_mode, snapshot.permission_mode, snapshot.prompt_format]
1883
+    if snapshot.prompt_sections:
1884
+        parts.append(f"sections={len(snapshot.prompt_sections)}")
1885
+    return " / ".join(parts)
1886
+
1887
+
1888
+def _print_workflow_artifact_diffs(
1889
+    snapshot: WorkflowArtifactDiffSnapshot,
1890
+    *,
1891
+    show_full: bool,
1892
+) -> None:
1893
+    if not snapshot.entries:
1894
+        console.print("[dim]No persisted artifact diffs are available for this session.[/dim]")
1895
+        return
1896
+
1897
+    if snapshot.highlights:
1898
+        _print_workflow_highlights(
1899
+            snapshot.highlights,
1900
+            title="[bold blue]Artifact Changes[/bold blue]",
1901
+        )
1902
+
1903
+    table = Table(show_header=True, header_style="bold cyan")
1904
+    table.add_column("Kind", style="white")
1905
+    table.add_column("Current", style="white")
1906
+    table.add_column("Previous", style="white")
1907
+    table.add_column("Summary", style="dim")
1908
+
1909
+    for entry in snapshot.entries:
1910
+        table.add_row(
1911
+            entry.kind,
1912
+            entry.current_path.name,
1913
+            entry.previous_path.name if entry.previous_path else "none",
1914
+            "; ".join(entry.highlights[:2]) or "none",
1915
+        )
1916
+
1917
+    console.print(
1918
+        Panel(
1919
+            table,
1920
+            title="[bold blue]Artifact Diff Summary[/bold blue]",
1921
+            border_style="blue",
1922
+        )
1923
+    )
1924
+
1925
+    if show_full:
1926
+        for entry in snapshot.entries:
1927
+            console.print()
1928
+            console.print(
1929
+                Panel(
1930
+                    entry.unified_diff or "[dim]No diff available.[/dim]",
1931
+                    title=f"[bold blue]{entry.kind} Diff[/bold blue]",
1932
+                    border_style="blue",
1933
+                )
1934
+            )
1935
+
1936
+
17901937
 def _print_workflow_timeline_entries(
17911938
     entries: list,
17921939
     *,
src/loader/runtime/inspection.pymodified
@@ -2,6 +2,7 @@
22
 
33
 from __future__ import annotations
44
 
5
+import difflib
56
 import json
67
 import os
78
 from dataclasses import dataclass, field
@@ -24,6 +25,7 @@ from .permissions import (
2425
     permission_path_hint,
2526
     summarize_permission_input,
2627
 )
28
+from .prompt_history import PromptSnapshot
2729
 from .prompting import build_system_prompt_result
2830
 from .session import SessionSnapshot, SessionStore
2931
 from .workflow_ledger import WorkflowLedger, workflow_ledger_highlights
@@ -255,6 +257,41 @@ class WorkflowTimelineSnapshot:
255257
     workflow_ledger: WorkflowLedger = field(default_factory=WorkflowLedger)
256258
 
257259
 
260
+@dataclass(slots=True)
261
+class PromptDiffSnapshot:
262
+    """Operator-facing diff between persisted prompt contracts."""
263
+
264
+    project_root: Path
265
+    session_id: str | None
266
+    current_task: str | None
267
+    current: PromptSnapshot | None
268
+    previous: PromptSnapshot | None
269
+    highlights: list[str] = field(default_factory=list)
270
+    unified_diff: str = ""
271
+
272
+
273
+@dataclass(slots=True)
274
+class ArtifactDiffEntry:
275
+    """One persisted artifact diff entry."""
276
+
277
+    kind: str
278
+    current_path: Path
279
+    previous_path: Path | None
280
+    highlights: list[str] = field(default_factory=list)
281
+    unified_diff: str = ""
282
+
283
+
284
+@dataclass(slots=True)
285
+class WorkflowArtifactDiffSnapshot:
286
+    """Operator-facing diff across persisted workflow artifacts."""
287
+
288
+    project_root: Path
289
+    session_id: str | None
290
+    current_task: str | None
291
+    entries: list[ArtifactDiffEntry] = field(default_factory=list)
292
+    highlights: list[str] = field(default_factory=list)
293
+
294
+
258295
 def capability_summary(profile: CapabilityProfile) -> str:
259296
     """Render a short human-readable capability summary."""
260297
 
@@ -597,6 +634,121 @@ def collect_prompt_preview(
597634
     )
598635
 
599636
 
637
+def collect_prompt_diff(
638
+    session_id: str | None = None,
639
+    *,
640
+    project_root: Path | str | None = None,
641
+) -> PromptDiffSnapshot:
642
+    """Load the latest persisted prompt-contract diff for one session."""
643
+
644
+    resolved_root = Path(project_root or Path.cwd()).expanduser().resolve()
645
+    store = SessionStore(resolved_root)
646
+    snapshot = store.load(session_id) if session_id else store.load_latest()
647
+    if snapshot is None:
648
+        return PromptDiffSnapshot(
649
+            project_root=resolved_root,
650
+            session_id=None,
651
+            current_task=None,
652
+            current=None,
653
+            previous=None,
654
+            highlights=[],
655
+            unified_diff="",
656
+        )
657
+
658
+    current = snapshot.prompt_history[-1] if snapshot.prompt_history else None
659
+    previous = _previous_prompt_snapshot(snapshot.prompt_history)
660
+    highlights = _prompt_diff_highlights(previous, current)
661
+    unified_diff = _unified_diff(
662
+        previous.content if previous is not None else "",
663
+        current.content if current is not None else "",
664
+        from_label=_prompt_snapshot_label(previous, fallback="previous prompt"),
665
+        to_label=_prompt_snapshot_label(current, fallback="current prompt"),
666
+    )
667
+    return PromptDiffSnapshot(
668
+        project_root=resolved_root,
669
+        session_id=snapshot.session_id,
670
+        current_task=snapshot.current_task,
671
+        current=current,
672
+        previous=previous,
673
+        highlights=highlights,
674
+        unified_diff=unified_diff,
675
+    )
676
+
677
+
678
+def collect_workflow_artifact_diffs(
679
+    session_id: str | None = None,
680
+    *,
681
+    project_root: Path | str | None = None,
682
+) -> WorkflowArtifactDiffSnapshot:
683
+    """Load persisted workflow-artifact diffs for the latest or named session."""
684
+
685
+    resolved_root = Path(project_root or Path.cwd()).expanduser().resolve()
686
+    store = SessionStore(resolved_root)
687
+    snapshot = store.load(session_id) if session_id else store.load_latest()
688
+    if snapshot is None:
689
+        return WorkflowArtifactDiffSnapshot(
690
+            project_root=resolved_root,
691
+            session_id=None,
692
+            current_task=None,
693
+            entries=[],
694
+            highlights=[],
695
+        )
696
+
697
+    dod = _load_dod(snapshot.active_dod_path, project_root=resolved_root)
698
+    if dod is None:
699
+        return WorkflowArtifactDiffSnapshot(
700
+            project_root=resolved_root,
701
+            session_id=snapshot.session_id,
702
+            current_task=snapshot.current_task,
703
+            entries=[],
704
+            highlights=[],
705
+        )
706
+
707
+    entries: list[ArtifactDiffEntry] = []
708
+    for kind, path_str in (
709
+        ("clarify_brief", dod.clarify_brief),
710
+        ("implementation_plan", dod.implementation_plan),
711
+        ("verification_plan", dod.verification_plan),
712
+    ):
713
+        if not path_str:
714
+            continue
715
+        current_path = Path(path_str)
716
+        if not current_path.exists():
717
+            continue
718
+        previous_path = _previous_artifact_path(current_path)
719
+        previous_text = previous_path.read_text() if previous_path and previous_path.exists() else ""
720
+        current_text = current_path.read_text()
721
+        entries.append(
722
+            ArtifactDiffEntry(
723
+                kind=kind,
724
+                current_path=current_path,
725
+                previous_path=previous_path,
726
+                highlights=_artifact_diff_highlights(
727
+                    kind=kind,
728
+                    current_path=current_path,
729
+                    previous_path=previous_path,
730
+                    previous_text=previous_text,
731
+                    current_text=current_text,
732
+                ),
733
+                unified_diff=_unified_diff(
734
+                    previous_text,
735
+                    current_text,
736
+                    from_label=str(previous_path) if previous_path else f"previous {kind}",
737
+                    to_label=str(current_path),
738
+                ),
739
+            )
740
+        )
741
+
742
+    highlights = [entry.highlights[0] for entry in entries if entry.highlights]
743
+    return WorkflowArtifactDiffSnapshot(
744
+        project_root=resolved_root,
745
+        session_id=snapshot.session_id,
746
+        current_task=snapshot.current_task,
747
+        entries=entries,
748
+        highlights=highlights,
749
+    )
750
+
751
+
600752
 def collect_permission_snapshot(
601753
     project_root: Path | str | None = None,
602754
     *,
@@ -751,6 +903,161 @@ def dry_run_permission_check(
751903
     )
752904
 
753905
 
906
+def _previous_prompt_snapshot(
907
+    history: list[PromptSnapshot],
908
+) -> PromptSnapshot | None:
909
+    if len(history) < 2:
910
+        return None
911
+    current = history[-1]
912
+    for snapshot in reversed(history[:-1]):
913
+        if not snapshot.matches_contract(current):
914
+            return snapshot
915
+    return history[-2]
916
+
917
+
918
+def _prompt_snapshot_label(
919
+    snapshot: PromptSnapshot | None,
920
+    *,
921
+    fallback: str,
922
+) -> str:
923
+    if snapshot is None:
924
+        return fallback
925
+    return (
926
+        f"{snapshot.timestamp} "
927
+        f"{snapshot.workflow_mode}/{snapshot.permission_mode}/{snapshot.prompt_format}"
928
+    )
929
+
930
+
931
+def _prompt_diff_highlights(
932
+    previous: PromptSnapshot | None,
933
+    current: PromptSnapshot | None,
934
+) -> list[str]:
935
+    if current is None:
936
+        return []
937
+    if previous is None:
938
+        return ["No earlier prompt snapshot is available for comparison."]
939
+
940
+    highlights: list[str] = []
941
+    if previous.workflow_mode != current.workflow_mode:
942
+        highlights.append(
943
+            f"Workflow mode changed: {previous.workflow_mode} -> {current.workflow_mode}"
944
+        )
945
+    if previous.permission_mode != current.permission_mode:
946
+        highlights.append(
947
+            "Permission mode changed: "
948
+            f"{previous.permission_mode} -> {current.permission_mode}"
949
+        )
950
+    if previous.prompt_format != current.prompt_format:
951
+        highlights.append(
952
+            f"Prompt format changed: {previous.prompt_format} -> {current.prompt_format}"
953
+        )
954
+    if previous.current_task != current.current_task:
955
+        highlights.append("Task framing changed across prompt snapshots.")
956
+    added_sections = [item for item in current.prompt_sections if item not in previous.prompt_sections]
957
+    removed_sections = [item for item in previous.prompt_sections if item not in current.prompt_sections]
958
+    if added_sections:
959
+        highlights.append("Added sections: " + ", ".join(added_sections))
960
+    if removed_sections:
961
+        highlights.append("Removed sections: " + ", ".join(removed_sections))
962
+    additions, removals = _line_change_counts(previous.content, current.content)
963
+    highlights.append(f"Prompt body lines changed: +{additions} / -{removals}")
964
+    return highlights
965
+
966
+
967
+def _previous_artifact_path(current_path: Path) -> Path | None:
968
+    name = current_path.name
969
+    if name in {"implementation.md", "verification.md"}:
970
+        parent = current_path.parent
971
+        if "-" not in parent.name:
972
+            return None
973
+        slug = parent.name.split("-", maxsplit=1)[1]
974
+        candidates = sorted(
975
+            item
976
+            for item in parent.parent.glob(f"*-{slug}")
977
+            if item.is_dir() and (item / name).exists()
978
+        )
979
+        previous_dir = _previous_sorted_item(candidates, parent)
980
+        if previous_dir is None:
981
+            return None
982
+        return previous_dir / name
983
+
984
+    stem = current_path.stem
985
+    if "-" not in stem:
986
+        return None
987
+    slug = stem.split("-", maxsplit=1)[1]
988
+    candidates = sorted(
989
+        item
990
+        for item in current_path.parent.glob(f"*-{slug}{current_path.suffix}")
991
+        if item.is_file()
992
+    )
993
+    return _previous_sorted_item(candidates, current_path)
994
+
995
+
996
+def _previous_sorted_item(items: list[Any], current: Any) -> Any | None:
997
+    previous: Any | None = None
998
+    for item in items:
999
+        if item == current:
1000
+            return previous
1001
+        previous = item
1002
+    return previous
1003
+
1004
+
1005
+def _artifact_diff_highlights(
1006
+    *,
1007
+    kind: str,
1008
+    current_path: Path,
1009
+    previous_path: Path | None,
1010
+    previous_text: str,
1011
+    current_text: str,
1012
+) -> list[str]:
1013
+    label = kind.replace("_", " ")
1014
+    if previous_path is None:
1015
+        return [f"{label}: no previous artifact version is available."]
1016
+    additions, removals = _line_change_counts(previous_text, current_text)
1017
+    if not additions and not removals:
1018
+        return [f"{label}: no content changes between persisted versions."]
1019
+    return [
1020
+        f"{label}: +{additions} / -{removals} lines vs {previous_path.name}",
1021
+        f"current={current_path.name}",
1022
+    ]
1023
+
1024
+
1025
+def _line_change_counts(previous_text: str, current_text: str) -> tuple[int, int]:
1026
+    additions = 0
1027
+    removals = 0
1028
+    diff_lines = difflib.unified_diff(
1029
+        previous_text.splitlines(),
1030
+        current_text.splitlines(),
1031
+        lineterm="",
1032
+    )
1033
+    for line in diff_lines:
1034
+        if line.startswith(("---", "+++", "@@")):
1035
+            continue
1036
+        if line.startswith("+"):
1037
+            additions += 1
1038
+        elif line.startswith("-"):
1039
+            removals += 1
1040
+    return additions, removals
1041
+
1042
+
1043
+def _unified_diff(
1044
+    previous_text: str,
1045
+    current_text: str,
1046
+    *,
1047
+    from_label: str,
1048
+    to_label: str,
1049
+) -> str:
1050
+    return "\n".join(
1051
+        difflib.unified_diff(
1052
+            previous_text.splitlines(),
1053
+            current_text.splitlines(),
1054
+            fromfile=from_label,
1055
+            tofile=to_label,
1056
+            lineterm="",
1057
+        )
1058
+    )
1059
+
1060
+
7541061
 def _coerce_permission_mode(value: PermissionMode | str) -> PermissionMode:
7551062
     if isinstance(value, PermissionMode):
7561063
         return value
tests/test_inspection.pymodified
@@ -15,13 +15,16 @@ from loader.runtime.inspection import (
1515
     CheckStatus,
1616
     collect_doctor_report,
1717
     collect_permission_snapshot,
18
+    collect_prompt_diff,
1819
     collect_prompt_preview,
1920
     collect_status_snapshot,
21
+    collect_workflow_artifact_diffs,
2022
     collect_workflow_timeline,
2123
     dry_run_permission_check,
2224
     list_session_summaries,
2325
     load_session_detail,
2426
 )
27
+from loader.runtime.prompt_history import PromptSnapshot
2528
 from loader.runtime.session import SessionSnapshot, SessionStore
2629
 from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem
2730
 from loader.runtime.workflow_policy import WorkflowTimelineEntry
@@ -136,6 +139,31 @@ def _persist_session_with_dod(temp_dir: Path) -> tuple[str, str]:
136139
         permission_rules_source=str(temp_dir / ".loader" / "permission-rules.json"),
137140
         prompt_format="native",
138141
         prompt_sections=["Runtime Config", "Workflow Context", "Mode Guidance"],
142
+        prompt_history=[
143
+            PromptSnapshot(
144
+                timestamp="2026-04-06T12:04:00Z",
145
+                workflow_mode="verify",
146
+                permission_mode="prompt",
147
+                current_task="Fix the failing tests",
148
+                prompt_format="native",
149
+                prompt_sections=["Runtime Config", "Workflow Context", "Mode Guidance"],
150
+                content="# Introduction\nverify parser fix\n",
151
+            ),
152
+            PromptSnapshot(
153
+                timestamp="2026-04-06T12:05:00Z",
154
+                workflow_mode="execute",
155
+                permission_mode="prompt",
156
+                current_task="Fix the failing tests",
157
+                prompt_format="native",
158
+                prompt_sections=[
159
+                    "Runtime Config",
160
+                    "Workflow Context",
161
+                    "Mode Guidance",
162
+                    "Project Context",
163
+                ],
164
+                content="# Introduction\nexecute parser fix\n# Project Context\npython\n",
165
+            ),
166
+        ],
139167
         workflow_reason_code="verification_failed_reentry",
140168
         workflow_reason_summary="verification failed; returning to execute for fixes",
141169
         workflow_decision_kind="reentry",
@@ -153,6 +181,40 @@ def _persist_session_with_dod(temp_dir: Path) -> tuple[str, str]:
153181
 
154182
 
155183
 def _persist_session_with_rich_workflow(temp_dir: Path) -> str:
184
+    slug = "tighten-loader-workflow-behavior"
185
+    brief_old = temp_dir / ".loader" / "briefs" / f"20260406T150000Z-{slug}.md"
186
+    brief_new = temp_dir / ".loader" / "briefs" / f"20260406T150200Z-{slug}.md"
187
+    brief_old.write_text(
188
+        "# Task Brief\n\n## Likely Touchpoints\n- planned.txt\n\n## Acceptance Criteria\n- planned.txt exists.\n"
189
+    )
190
+    brief_new.write_text(
191
+        "# Task Brief\n\n## Likely Touchpoints\n- notes.txt\n\n## Acceptance Criteria\n- notes.txt exists.\n"
192
+    )
193
+    plan_old_root = temp_dir / ".loader" / "plans" / f"20260406T150100Z-{slug}"
194
+    plan_new_root = temp_dir / ".loader" / "plans" / f"20260406T150300Z-{slug}"
195
+    plan_old_root.mkdir(parents=True, exist_ok=True)
196
+    plan_new_root.mkdir(parents=True, exist_ok=True)
197
+    (plan_old_root / "implementation.md").write_text(
198
+        "# Implementation Plan\n\n## File Changes\n- Create planned.txt.\n"
199
+    )
200
+    (plan_old_root / "verification.md").write_text(
201
+        "# Verification Plan\n\n## Acceptance Criteria\n- planned.txt exists.\n"
202
+    )
203
+    (plan_new_root / "implementation.md").write_text(
204
+        "# Implementation Plan\n\n## File Changes\n- Keep notes.txt as the runtime artifact.\n"
205
+    )
206
+    (plan_new_root / "verification.md").write_text(
207
+        "# Verification Plan\n\n## Acceptance Criteria\n- notes.txt exists.\n"
208
+    )
209
+
210
+    dod = create_definition_of_done("Tighten Loader workflow behavior")
211
+    dod.status = "fixing"
212
+    dod.clarify_brief = str(brief_new)
213
+    dod.implementation_plan = str(plan_new_root / "implementation.md")
214
+    dod.verification_plan = str(plan_new_root / "verification.md")
215
+    dod.acceptance_criteria = ["notes.txt exists in the workspace root."]
216
+    dod_path = DefinitionOfDoneStore(temp_dir).save(dod)
217
+
156218
     snapshot = SessionSnapshot(
157219
         session_id="20260406T150000Z-feedface",
158220
         created_at="2026-04-06T15:00:00Z",
@@ -161,10 +223,38 @@ def _persist_session_with_rich_workflow(temp_dir: Path) -> str:
161223
             Message(role=Role.USER, content="Tighten Loader workflow behavior"),
162224
             Message(role=Role.ASSISTANT, content="I refreshed the workflow contract."),
163225
         ],
226
+        active_dod_path=str(dod_path),
164227
         current_task="Tighten Loader workflow behavior",
165228
         workflow_mode="execute",
166229
         permission_mode="prompt",
167230
         permission_prompting_enabled=True,
231
+        prompt_format="native",
232
+        prompt_sections=["Runtime Config", "Workflow Context", "Mode Guidance"],
233
+        prompt_history=[
234
+            PromptSnapshot(
235
+                timestamp="2026-04-06T15:02:00Z",
236
+                workflow_mode="plan",
237
+                permission_mode="prompt",
238
+                current_task="Tighten Loader workflow behavior",
239
+                prompt_format="native",
240
+                prompt_sections=["Runtime Config", "Workflow Context", "Mode Guidance"],
241
+                content="# Introduction\nplan around planned.txt\n",
242
+            ),
243
+            PromptSnapshot(
244
+                timestamp="2026-04-06T15:04:00Z",
245
+                workflow_mode="execute",
246
+                permission_mode="prompt",
247
+                current_task="Tighten Loader workflow behavior",
248
+                prompt_format="native",
249
+                prompt_sections=[
250
+                    "Runtime Config",
251
+                    "Workflow Context",
252
+                    "Mode Guidance",
253
+                    "Project Context",
254
+                ],
255
+                content="# Introduction\nexecute around notes.txt\n# Project Context\npython\n",
256
+            ),
257
+        ],
168258
         workflow_reason_code="full_replan_completed",
169259
         workflow_reason_summary="clarify and plan artifacts refreshed; returning to execute",
170260
         workflow_decision_kind="handoff",
@@ -200,6 +290,11 @@ def _persist_session_with_rich_workflow(temp_dir: Path) -> str:
200290
                     ),
201291
                 ],
202292
                 signal_summary=["recent_reentry=1", "stale_plan=true"],
293
+                artifact_paths=[
294
+                    str(brief_new),
295
+                    str(plan_new_root / "implementation.md"),
296
+                    str(plan_new_root / "verification.md"),
297
+                ],
203298
             ),
204299
             WorkflowTimelineEntry(
205300
                 timestamp="2026-04-06T15:03:00Z",
@@ -459,6 +554,43 @@ def test_collect_workflow_timeline_supports_filters_and_highlights(
459554
     )
460555
 
461556
 
557
+def test_collect_prompt_diff_uses_persisted_prompt_history(temp_dir: Path) -> None:
558
+    _write_python_workspace(temp_dir)
559
+    _ensure_loader_dirs(temp_dir)
560
+    session_id, _ = _persist_session_with_dod(temp_dir)
561
+
562
+    diff = collect_prompt_diff(project_root=temp_dir)
563
+
564
+    assert diff.session_id == session_id
565
+    assert diff.previous is not None
566
+    assert diff.current is not None
567
+    assert diff.current.workflow_mode == "execute"
568
+    assert diff.previous.workflow_mode == "verify"
569
+    assert any("Workflow mode changed:" in item for item in diff.highlights)
570
+    assert "---" in diff.unified_diff
571
+    assert "execute parser fix" in diff.unified_diff
572
+
573
+
574
+def test_collect_workflow_artifact_diffs_reads_versioned_artifacts(
575
+    temp_dir: Path,
576
+) -> None:
577
+    _write_python_workspace(temp_dir)
578
+    _ensure_loader_dirs(temp_dir)
579
+    session_id = _persist_session_with_rich_workflow(temp_dir)
580
+
581
+    snapshot = collect_workflow_artifact_diffs(project_root=temp_dir)
582
+
583
+    assert snapshot.session_id == session_id
584
+    assert len(snapshot.entries) == 3
585
+    assert {entry.kind for entry in snapshot.entries} == {
586
+        "clarify_brief",
587
+        "implementation_plan",
588
+        "verification_plan",
589
+    }
590
+    assert any("notes.txt" in entry.unified_diff for entry in snapshot.entries)
591
+    assert snapshot.highlights
592
+
593
+
462594
 def test_status_and_session_commands_render_persisted_state(
463595
     temp_dir: Path,
464596
     monkeypatch: pytest.MonkeyPatch,
@@ -575,6 +707,31 @@ def test_workflow_show_command_supports_filters_and_highlights(
575707
     assert "gates=non_goals,decision_boundaries" in clarify_result.output
576708
 
577709
 
710
+def test_workflow_show_can_render_artifact_diffs(
711
+    temp_dir: Path,
712
+    monkeypatch: pytest.MonkeyPatch,
713
+) -> None:
714
+    _write_python_workspace(temp_dir)
715
+    _ensure_loader_dirs(temp_dir)
716
+    _persist_session_with_rich_workflow(temp_dir)
717
+    runner = CliRunner()
718
+
719
+    monkeypatch.chdir(temp_dir)
720
+
721
+    result = runner.invoke(
722
+        cli_main_module.workflow_cli,
723
+        ["show", "--diff", "--full-diff"],
724
+    )
725
+
726
+    assert result.exit_code == 0
727
+    assert "Artifact Changes" in result.output
728
+    assert "Artifact Diff Summary" in result.output
729
+    assert "clarify_brief" in result.output
730
+    assert "implementation_plan" in result.output
731
+    assert "verification_plan" in result.output
732
+    assert "notes.txt" in result.output
733
+
734
+
578735
 def test_collect_prompt_preview_uses_persisted_runtime_state(temp_dir: Path) -> None:
579736
     _write_python_workspace(temp_dir)
580737
     _ensure_loader_dirs(temp_dir)
@@ -634,6 +791,27 @@ def test_prompt_show_command_renders_preview_without_model_call(
634791
     assert "Execute Mode" in result.output
635792
 
636793
 
794
+def test_prompt_diff_command_renders_persisted_prompt_changes(
795
+    temp_dir: Path,
796
+    monkeypatch: pytest.MonkeyPatch,
797
+) -> None:
798
+    _write_python_workspace(temp_dir)
799
+    _ensure_loader_dirs(temp_dir)
800
+    _persist_session_with_dod(temp_dir)
801
+    runner = CliRunner()
802
+
803
+    monkeypatch.chdir(temp_dir)
804
+
805
+    result = runner.invoke(cli_main_module.prompt_cli, ["diff", "--full"])
806
+
807
+    assert result.exit_code == 0
808
+    assert "Prompt Diff" in result.output
809
+    assert "Prompt Changes" in result.output
810
+    assert "Workflow mode changed:" in result.output
811
+    assert "Prompt Unified Diff" in result.output
812
+    assert "execute parser fix" in result.output
813
+
814
+
637815
 def test_permission_snapshot_and_dry_run_reflect_rules(temp_dir: Path) -> None:
638816
     _write_python_workspace(temp_dir)
639817
     _ensure_loader_dirs(temp_dir)