tenseleyflow/loader / 26a2822

Browse files

Persist first chapter handoffs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
26a2822256ffb6ed075c7a5c1ff31a9967ca20a3
Parents
ea5727e
Tree
b629792

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 75 15
M tests/test_tool_batches.py 10 7
src/loader/runtime/tool_batches.pymodified
@@ -110,6 +110,14 @@ _BOOKKEEPING_NOTE_TOOL_NAMES = {
110110
     "notepad_write_priority",
111111
     "notepad_write_manual",
112112
 }
113
+_SUMMARY_ARTIFACT_NAMES = {
114
+    "index.html",
115
+    "index.htm",
116
+    "readme",
117
+    "readme.md",
118
+    "readme.rst",
119
+    "readme.txt",
120
+}
113121
 
114122
 
115123
 @dataclass
@@ -1029,9 +1037,11 @@ class ToolBatchRunner:
10291037
             next_pending=next_pending,
10301038
             project_root=self.context.project_root,
10311039
         )
1032
-        has_file_artifact_progress = _has_confirmed_file_artifact_progress(
1033
-            dod,
1034
-            project_root=self.context.project_root,
1040
+        has_substantive_file_artifact_progress = (
1041
+            _has_confirmed_substantive_file_artifact_progress(
1042
+                dod,
1043
+                project_root=self.context.project_root,
1044
+            )
10351045
         )
10361046
         if not completed_label or not next_pending or next_pending == completed_label:
10371047
             return
@@ -1041,7 +1051,7 @@ class ToolBatchRunner:
10411051
             missing_artifact=missing_artifact,
10421052
             project_root=self.context.project_root,
10431053
         ):
1044
-            if not has_file_artifact_progress:
1054
+            if not has_substantive_file_artifact_progress:
10451055
                 compact_handoff = _compact_missing_artifact_handoff(
10461056
                     missing_artifact,
10471057
                     project_root=self.context.project_root,
@@ -1160,9 +1170,11 @@ class ToolBatchRunner:
11601170
         )
11611171
 
11621172
         current_label = _current_mutation_label(tool_call)
1163
-        has_file_artifact_progress = _has_confirmed_file_artifact_progress(
1164
-            dod,
1165
-            project_root=self.context.project_root,
1173
+        has_substantive_file_artifact_progress = (
1174
+            _has_confirmed_substantive_file_artifact_progress(
1175
+                dod,
1176
+                project_root=self.context.project_root,
1177
+            )
11661178
         )
11671179
         resume_target = _preferred_resume_target_path(
11681180
             dod,
@@ -1179,7 +1191,7 @@ class ToolBatchRunner:
11791191
             messages=list(getattr(self.context.session, "messages", []) or []),
11801192
         )
11811193
         if (
1182
-            not has_file_artifact_progress
1194
+            not has_substantive_file_artifact_progress
11831195
             and _is_pure_directory_creation_tool_call(tool_call)
11841196
         ):
11851197
             if (
@@ -1224,7 +1236,7 @@ class ToolBatchRunner:
12241236
                 (resume_target, False),
12251237
                 project_root=self.context.project_root,
12261238
                 messages=session_messages,
1227
-                encourage_initial_version=not has_file_artifact_progress,
1239
+                encourage_initial_version=not has_substantive_file_artifact_progress,
12281240
             )
12291241
             if compact_resume:
12301242
                 queue_message(
@@ -1237,7 +1249,7 @@ class ToolBatchRunner:
12371249
             dod,
12381250
             project_root=self.context.project_root,
12391251
         )
1240
-        if not has_file_artifact_progress:
1252
+        if not has_substantive_file_artifact_progress:
12411253
             compact_handoff = _compact_missing_artifact_handoff(
12421254
                 missing_artifact,
12431255
                 project_root=self.context.project_root,
@@ -1385,9 +1397,11 @@ class ToolBatchRunner:
13851397
             )
13861398
             return
13871399
 
1388
-        has_file_artifact_progress = _has_confirmed_file_artifact_progress(
1389
-            dod,
1390
-            project_root=self.context.project_root,
1400
+        has_substantive_file_artifact_progress = (
1401
+            _has_confirmed_substantive_file_artifact_progress(
1402
+                dod,
1403
+                project_root=self.context.project_root,
1404
+            )
13911405
         )
13921406
         todo_refresh = _todo_refresh_guidance(
13931407
             dod,
@@ -1415,7 +1429,7 @@ class ToolBatchRunner:
14151429
                 (resume_target, False),
14161430
                 project_root=self.context.project_root,
14171431
                 messages=session_messages,
1418
-                encourage_initial_version=not has_file_artifact_progress,
1432
+                encourage_initial_version=not has_substantive_file_artifact_progress,
14191433
             )
14201434
             if compact_resume:
14211435
                 self.context.queue_steering_message(
@@ -1692,6 +1706,17 @@ def _has_confirmed_file_artifact_progress(
16921706
     return _confirmed_file_artifact_count(dod, project_root=project_root) > 0
16931707
 
16941708
 
1709
+def _has_confirmed_substantive_file_artifact_progress(
1710
+    dod: DefinitionOfDone,
1711
+    *,
1712
+    project_root: Path,
1713
+) -> bool:
1714
+    return _confirmed_substantive_file_artifact_count(
1715
+        dod,
1716
+        project_root=project_root,
1717
+    ) > 0
1718
+
1719
+
16951720
 def _last_touched_file_path(dod: DefinitionOfDone) -> Path | None:
16961721
     for raw_path in reversed(dod.touched_files):
16971722
         path_text = str(raw_path or "").strip()
@@ -1733,17 +1758,52 @@ def _confirmed_file_artifact_count(
17331758
     )
17341759
 
17351760
 
1761
+def _confirmed_substantive_file_artifact_count(
1762
+    dod: DefinitionOfDone,
1763
+    *,
1764
+    project_root: Path,
1765
+) -> int:
1766
+    count = 0
1767
+    for target, expect_directory in collect_planned_artifact_targets(
1768
+        dod,
1769
+        project_root=project_root,
1770
+        max_paths=12,
1771
+    ):
1772
+        if expect_directory or _is_summary_artifact_path(target):
1773
+            continue
1774
+        if planned_artifact_target_satisfied(
1775
+            dod,
1776
+            target=target,
1777
+            expect_directory=False,
1778
+            project_root=project_root,
1779
+        ):
1780
+            count += 1
1781
+    if count:
1782
+        return count
1783
+    return sum(
1784
+        1
1785
+        for path in dod.touched_files
1786
+        if str(path).strip()
1787
+        and Path(path).expanduser().resolve(strict=False).suffix
1788
+        and not _is_summary_artifact_path(path)
1789
+    )
1790
+
1791
+
17361792
 def _should_use_persistent_missing_artifact_handoff(
17371793
     dod: DefinitionOfDone,
17381794
     *,
17391795
     project_root: Path,
17401796
 ) -> bool:
1741
-    return _confirmed_file_artifact_count(
1797
+    return _confirmed_substantive_file_artifact_count(
17421798
         dod,
17431799
         project_root=project_root,
17441800
     ) == 0
17451801
 
17461802
 
1803
+def _is_summary_artifact_path(path: str | Path) -> bool:
1804
+    return Path(path).name.lower() in _SUMMARY_ARTIFACT_NAMES
1805
+
1806
+
17471807
 def _next_missing_planned_file_within_directory(
17481808
     dod: DefinitionOfDone,
17491809
     *,
tests/test_tool_batches.pymodified
@@ -2251,7 +2251,7 @@ async def test_tool_batch_runner_missing_artifact_nudge_names_next_file_after_se
22512251
 
22522252
 
22532253
 @pytest.mark.asyncio
2254
-async def test_tool_batch_runner_first_chapter_handoff_becomes_ephemeral_after_first_file(
2254
+async def test_tool_batch_runner_first_chapter_handoff_stays_persistent_until_substantive_output_exists(
22552255
     temp_dir: Path,
22562256
 ) -> None:
22572257
     async def assess_confidence(
@@ -2353,15 +2353,16 @@ async def test_tool_batch_runner_first_chapter_handoff_becomes_ephemeral_after_f
23532353
         consecutive_errors=0,
23542354
     )
23552355
 
2356
-    assert persistent_messages == []
2357
-    assert ephemeral_messages
2358
-    message = ephemeral_messages[-1]
2356
+    assert persistent_messages
2357
+    assert ephemeral_messages == []
2358
+    message = persistent_messages[-1]
23592359
     assert "Confirmed progress:" in message
23602360
     assert "Next step: create `01-introduction.html`." in message
23612361
     assert (
23622362
         f"Prefer one `write(file_path=..., content=...)` call for `{(chapters / '01-introduction.html').resolve(strict=False)}` now."
23632363
         in message
23642364
     )
2365
+    assert "Write a compact but real initial version of that file now" in message
23652366
     assert "Do not reread reference material or spend the next turn on bookkeeping." in message
23662367
 
23672368
 
@@ -2694,6 +2695,7 @@ async def test_tool_batch_runner_softens_first_file_handoff_after_recovery_promp
26942695
     assert ephemeral_messages
26952696
     message = ephemeral_messages[-1]
26962697
     assert "Next step: create `01-introduction.html`." in message
2698
+    assert "Write a compact but real initial version of that file now" in message
26972699
 
26982700
 
26992701
 @pytest.mark.asyncio
@@ -3148,10 +3150,11 @@ async def test_tool_batch_runner_mutation_handoff_points_at_next_missing_artifac
31483150
         consecutive_errors=0,
31493151
     )
31503152
 
3151
-    assert persistent_messages == []
3152
-    assert ephemeral_messages
3153
-    message = ephemeral_messages[-1]
3153
+    assert persistent_messages
3154
+    assert ephemeral_messages == []
3155
+    message = persistent_messages[-1]
31543156
     assert "Next step: create `01-getting-started.html`." in message
3157
+    assert "Write a compact but real initial version of that file now" in message
31553158
     assert "refresh `TodoWrite`" not in message
31563159
     assert "Do not reread reference material or spend the next turn on bookkeeping." in message
31573160