@@ -110,6 +110,14 @@ _BOOKKEEPING_NOTE_TOOL_NAMES = { |
| 110 | 110 | "notepad_write_priority", |
| 111 | 111 | "notepad_write_manual", |
| 112 | 112 | } |
| 113 | +_SUMMARY_ARTIFACT_NAMES = { |
| 114 | + "index.html", |
| 115 | + "index.htm", |
| 116 | + "readme", |
| 117 | + "readme.md", |
| 118 | + "readme.rst", |
| 119 | + "readme.txt", |
| 120 | +} |
| 113 | 121 | |
| 114 | 122 | |
| 115 | 123 | @dataclass |
@@ -1029,9 +1037,11 @@ class ToolBatchRunner: |
| 1029 | 1037 | next_pending=next_pending, |
| 1030 | 1038 | project_root=self.context.project_root, |
| 1031 | 1039 | ) |
| 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 | + ) |
| 1035 | 1045 | ) |
| 1036 | 1046 | if not completed_label or not next_pending or next_pending == completed_label: |
| 1037 | 1047 | return |
@@ -1041,7 +1051,7 @@ class ToolBatchRunner: |
| 1041 | 1051 | missing_artifact=missing_artifact, |
| 1042 | 1052 | project_root=self.context.project_root, |
| 1043 | 1053 | ): |
| 1044 | | - if not has_file_artifact_progress: |
| 1054 | + if not has_substantive_file_artifact_progress: |
| 1045 | 1055 | compact_handoff = _compact_missing_artifact_handoff( |
| 1046 | 1056 | missing_artifact, |
| 1047 | 1057 | project_root=self.context.project_root, |
@@ -1160,9 +1170,11 @@ class ToolBatchRunner: |
| 1160 | 1170 | ) |
| 1161 | 1171 | |
| 1162 | 1172 | 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 | + ) |
| 1166 | 1178 | ) |
| 1167 | 1179 | resume_target = _preferred_resume_target_path( |
| 1168 | 1180 | dod, |
@@ -1179,7 +1191,7 @@ class ToolBatchRunner: |
| 1179 | 1191 | messages=list(getattr(self.context.session, "messages", []) or []), |
| 1180 | 1192 | ) |
| 1181 | 1193 | if ( |
| 1182 | | - not has_file_artifact_progress |
| 1194 | + not has_substantive_file_artifact_progress |
| 1183 | 1195 | and _is_pure_directory_creation_tool_call(tool_call) |
| 1184 | 1196 | ): |
| 1185 | 1197 | if ( |
@@ -1224,7 +1236,7 @@ class ToolBatchRunner: |
| 1224 | 1236 | (resume_target, False), |
| 1225 | 1237 | project_root=self.context.project_root, |
| 1226 | 1238 | messages=session_messages, |
| 1227 | | - encourage_initial_version=not has_file_artifact_progress, |
| 1239 | + encourage_initial_version=not has_substantive_file_artifact_progress, |
| 1228 | 1240 | ) |
| 1229 | 1241 | if compact_resume: |
| 1230 | 1242 | queue_message( |
@@ -1237,7 +1249,7 @@ class ToolBatchRunner: |
| 1237 | 1249 | dod, |
| 1238 | 1250 | project_root=self.context.project_root, |
| 1239 | 1251 | ) |
| 1240 | | - if not has_file_artifact_progress: |
| 1252 | + if not has_substantive_file_artifact_progress: |
| 1241 | 1253 | compact_handoff = _compact_missing_artifact_handoff( |
| 1242 | 1254 | missing_artifact, |
| 1243 | 1255 | project_root=self.context.project_root, |
@@ -1385,9 +1397,11 @@ class ToolBatchRunner: |
| 1385 | 1397 | ) |
| 1386 | 1398 | return |
| 1387 | 1399 | |
| 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 | + ) |
| 1391 | 1405 | ) |
| 1392 | 1406 | todo_refresh = _todo_refresh_guidance( |
| 1393 | 1407 | dod, |
@@ -1415,7 +1429,7 @@ class ToolBatchRunner: |
| 1415 | 1429 | (resume_target, False), |
| 1416 | 1430 | project_root=self.context.project_root, |
| 1417 | 1431 | messages=session_messages, |
| 1418 | | - encourage_initial_version=not has_file_artifact_progress, |
| 1432 | + encourage_initial_version=not has_substantive_file_artifact_progress, |
| 1419 | 1433 | ) |
| 1420 | 1434 | if compact_resume: |
| 1421 | 1435 | self.context.queue_steering_message( |
@@ -1692,6 +1706,17 @@ def _has_confirmed_file_artifact_progress( |
| 1692 | 1706 | return _confirmed_file_artifact_count(dod, project_root=project_root) > 0 |
| 1693 | 1707 | |
| 1694 | 1708 | |
| 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 | + |
| 1695 | 1720 | def _last_touched_file_path(dod: DefinitionOfDone) -> Path | None: |
| 1696 | 1721 | for raw_path in reversed(dod.touched_files): |
| 1697 | 1722 | path_text = str(raw_path or "").strip() |
@@ -1733,17 +1758,52 @@ def _confirmed_file_artifact_count( |
| 1733 | 1758 | ) |
| 1734 | 1759 | |
| 1735 | 1760 | |
| 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 | + |
| 1736 | 1792 | def _should_use_persistent_missing_artifact_handoff( |
| 1737 | 1793 | dod: DefinitionOfDone, |
| 1738 | 1794 | *, |
| 1739 | 1795 | project_root: Path, |
| 1740 | 1796 | ) -> bool: |
| 1741 | | - return _confirmed_file_artifact_count( |
| 1797 | + return _confirmed_substantive_file_artifact_count( |
| 1742 | 1798 | dod, |
| 1743 | 1799 | project_root=project_root, |
| 1744 | 1800 | ) == 0 |
| 1745 | 1801 | |
| 1746 | 1802 | |
| 1803 | +def _is_summary_artifact_path(path: str | Path) -> bool: |
| 1804 | + return Path(path).name.lower() in _SUMMARY_ARTIFACT_NAMES |
| 1805 | + |
| 1806 | + |
| 1747 | 1807 | def _next_missing_planned_file_within_directory( |
| 1748 | 1808 | dod: DefinitionOfDone, |
| 1749 | 1809 | *, |