@@ -1106,9 +1106,19 @@ class ToolBatchRunner: |
| 1106 | 1106 | dod, |
| 1107 | 1107 | project_root=self.context.project_root, |
| 1108 | 1108 | ) |
| 1109 | + resume_suffix = _pending_item_resume_suffix( |
| 1110 | + dod, |
| 1111 | + next_pending=next_pending, |
| 1112 | + missing_artifact=missing_artifact, |
| 1113 | + project_root=self.context.project_root, |
| 1114 | + messages=list(getattr(self.context.session, "messages", []) or []), |
| 1115 | + ) |
| 1109 | 1116 | queue_message = ( |
| 1110 | 1117 | self.context.queue_steering_message |
| 1111 | | - if not has_file_artifact_progress |
| 1118 | + if _should_use_persistent_missing_artifact_handoff( |
| 1119 | + dod, |
| 1120 | + project_root=self.context.project_root, |
| 1121 | + ) |
| 1112 | 1122 | else self.context.queue_ephemeral_steering_message |
| 1113 | 1123 | ) |
| 1114 | 1124 | todo_refresh = _todo_refresh_guidance( |
@@ -1134,22 +1144,14 @@ class ToolBatchRunner: |
| 1134 | 1144 | ): |
| 1135 | 1145 | queue_message( |
| 1136 | 1146 | f"Confirmed progress: {current_label} is now recorded." |
| 1137 | | - + _missing_artifact_resume_suffix( |
| 1138 | | - missing_artifact, |
| 1139 | | - project_root=self.context.project_root, |
| 1140 | | - messages=list(getattr(self.context.session, "messages", []) or []), |
| 1141 | | - ) |
| 1147 | + + resume_suffix |
| 1142 | 1148 | + " No TodoWrite, no verification, no rereads until that artifact exists." |
| 1143 | 1149 | ) |
| 1144 | 1150 | return |
| 1145 | 1151 | queue_message( |
| 1146 | 1152 | f"Confirmed progress: {current_label} is now recorded." |
| 1147 | 1153 | " One declared output artifact is still missing." |
| 1148 | | - + _missing_artifact_resume_suffix( |
| 1149 | | - missing_artifact, |
| 1150 | | - project_root=self.context.project_root, |
| 1151 | | - messages=list(getattr(self.context.session, "messages", []) or []), |
| 1152 | | - ) |
| 1154 | + + resume_suffix |
| 1153 | 1155 | + todo_refresh |
| 1154 | 1156 | + " Do not move to verification, final confirmation, or TodoWrite-only " |
| 1155 | 1157 | "bookkeeping until that artifact exists." |
@@ -1527,6 +1529,15 @@ def _has_confirmed_file_artifact_progress( |
| 1527 | 1529 | *, |
| 1528 | 1530 | project_root: Path, |
| 1529 | 1531 | ) -> bool: |
| 1532 | + return _confirmed_file_artifact_count(dod, project_root=project_root) > 0 |
| 1533 | + |
| 1534 | + |
| 1535 | +def _confirmed_file_artifact_count( |
| 1536 | + dod: DefinitionOfDone, |
| 1537 | + *, |
| 1538 | + project_root: Path, |
| 1539 | +) -> int: |
| 1540 | + count = 0 |
| 1530 | 1541 | for target, expect_directory in collect_planned_artifact_targets( |
| 1531 | 1542 | dod, |
| 1532 | 1543 | project_root=project_root, |
@@ -1540,14 +1551,61 @@ def _has_confirmed_file_artifact_progress( |
| 1540 | 1551 | expect_directory=False, |
| 1541 | 1552 | project_root=project_root, |
| 1542 | 1553 | ): |
| 1543 | | - return True |
| 1544 | | - return any( |
| 1545 | | - Path(path).expanduser().resolve(strict=False).suffix |
| 1554 | + count += 1 |
| 1555 | + if count: |
| 1556 | + return count |
| 1557 | + return sum( |
| 1558 | + 1 |
| 1546 | 1559 | for path in dod.touched_files |
| 1547 | 1560 | if str(path).strip() |
| 1561 | + and Path(path).expanduser().resolve(strict=False).suffix |
| 1548 | 1562 | ) |
| 1549 | 1563 | |
| 1550 | 1564 | |
| 1565 | +def _should_use_persistent_missing_artifact_handoff( |
| 1566 | + dod: DefinitionOfDone, |
| 1567 | + *, |
| 1568 | + project_root: Path, |
| 1569 | +) -> bool: |
| 1570 | + return _confirmed_file_artifact_count( |
| 1571 | + dod, |
| 1572 | + project_root=project_root, |
| 1573 | + ) < 2 |
| 1574 | + |
| 1575 | + |
| 1576 | +def _next_missing_planned_file_within_directory( |
| 1577 | + dod: DefinitionOfDone, |
| 1578 | + *, |
| 1579 | + target: Path, |
| 1580 | + project_root: Path, |
| 1581 | +) -> Path | None: |
| 1582 | + normalized_target = target.expanduser().resolve(strict=False) |
| 1583 | + if normalized_target.suffix: |
| 1584 | + return None |
| 1585 | + |
| 1586 | + for planned_target, expect_directory in collect_planned_artifact_targets( |
| 1587 | + dod, |
| 1588 | + project_root=project_root, |
| 1589 | + max_paths=12, |
| 1590 | + ): |
| 1591 | + if expect_directory: |
| 1592 | + continue |
| 1593 | + normalized_planned = planned_target.expanduser().resolve(strict=False) |
| 1594 | + try: |
| 1595 | + normalized_planned.relative_to(normalized_target) |
| 1596 | + except ValueError: |
| 1597 | + continue |
| 1598 | + if planned_artifact_target_satisfied( |
| 1599 | + dod, |
| 1600 | + target=normalized_planned, |
| 1601 | + expect_directory=False, |
| 1602 | + project_root=project_root, |
| 1603 | + ): |
| 1604 | + continue |
| 1605 | + return normalized_planned |
| 1606 | + return None |
| 1607 | + |
| 1608 | + |
| 1551 | 1609 | def _missing_artifact_resume_suffix( |
| 1552 | 1610 | missing_artifact: tuple[Path, bool] | None, |
| 1553 | 1611 | *, |
@@ -1589,6 +1647,21 @@ def _pending_item_resume_suffix( |
| 1589 | 1647 | messages=messages, |
| 1590 | 1648 | allow_inferred_child=False, |
| 1591 | 1649 | ) |
| 1650 | + if missing_artifact is not None and missing_artifact[1]: |
| 1651 | + next_planned_file = _next_missing_planned_file_within_directory( |
| 1652 | + dod, |
| 1653 | + target=missing_artifact[0], |
| 1654 | + project_root=project_root, |
| 1655 | + ) |
| 1656 | + if next_planned_file is not None: |
| 1657 | + parent_label = missing_artifact[0].name or str(missing_artifact[0]) |
| 1658 | + return ( |
| 1659 | + f" Resume by creating `{next_planned_file.name}` now." |
| 1660 | + f" It is the next missing declared output under `{parent_label}/`." |
| 1661 | + f" Prefer one `write` call for `{next_planned_file}` instead of more rereads." |
| 1662 | + " Make your next response the concrete mutation tool call itself, not another" |
| 1663 | + " bookkeeping-only turn." |
| 1664 | + ) |
| 1592 | 1665 | return _missing_artifact_resume_suffix( |
| 1593 | 1666 | missing_artifact, |
| 1594 | 1667 | project_root=project_root, |
@@ -1621,6 +1694,14 @@ def _preferred_resume_target_path( |
| 1621 | 1694 | if not expect_directory: |
| 1622 | 1695 | return normalized_target |
| 1623 | 1696 | |
| 1697 | + next_planned_file = _next_missing_planned_file_within_directory( |
| 1698 | + dod, |
| 1699 | + target=normalized_target, |
| 1700 | + project_root=project_root, |
| 1701 | + ) |
| 1702 | + if next_planned_file is not None: |
| 1703 | + return next_planned_file.expanduser().resolve(strict=False) |
| 1704 | + |
| 1624 | 1705 | next_output_file, _ = infer_next_output_file( |
| 1625 | 1706 | target=normalized_target, |
| 1626 | 1707 | project_root=project_root, |