tamfis-code 0.6.13__tar.gz → 0.6.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {tamfis_code-0.6.13/tamfis_code.egg-info → tamfis_code-0.6.14}/PKG-INFO +1 -1
  2. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/pyproject.toml +1 -1
  3. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/__init__.py +1 -1
  4. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/agents.py +2 -0
  5. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/enforcer.py +2 -0
  6. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/interactive.py +75 -22
  7. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/live_input.py +55 -7
  8. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/engine.py +18 -13
  9. tamfis_code-0.6.14/tamfis_code/orchestrator/planner.py +871 -0
  10. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/render.py +15 -5
  11. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/runner_local.py +427 -33
  12. {tamfis_code-0.6.13 → tamfis_code-0.6.14/tamfis_code.egg-info}/PKG-INFO +1 -1
  13. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/SOURCES.txt +1 -0
  14. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_github_command_surface.py +5 -2
  15. tamfis_code-0.6.14/tests/test_intelligent_planner.py +304 -0
  16. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_live_input.py +116 -0
  17. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_orchestrator.py +7 -2
  18. tamfis_code-0.6.14/tests/test_workspace_scope.py +497 -0
  19. tamfis_code-0.6.13/tamfis_code/orchestrator/planner.py +0 -195
  20. tamfis_code-0.6.13/tests/test_workspace_scope.py +0 -201
  21. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/MANIFEST.in +0 -0
  22. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/README.md +0 -0
  23. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/USAGE_INSTALL_RELEASE.md +0 -0
  24. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/setup.cfg +0 -0
  25. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/__main__.py +0 -0
  26. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/agent_definitions.py +0 -0
  27. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/api_client.py +0 -0
  28. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/cli.py +0 -0
  29. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/clipboard.py +0 -0
  30. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/completion.py +0 -0
  31. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/config.py +0 -0
  32. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/custom_commands.py +0 -0
  33. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/doctor.py +0 -0
  34. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/evidence.py +0 -0
  35. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/github_commands.py +0 -0
  36. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/hooks.py +0 -0
  37. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/indexer.py +0 -0
  38. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/instructions.py +0 -0
  39. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/local_chat.py +0 -0
  40. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/local_tools.py +0 -0
  41. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/mcp.py +0 -0
  42. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/mcp_stdio_server.py +0 -0
  43. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/metrics.py +0 -0
  44. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/model_registry.py +0 -0
  45. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/__init__.py +0 -0
  46. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/agent.py +0 -0
  47. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/agent_server.py +0 -0
  48. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/automation.py +0 -0
  49. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/compat.py +0 -0
  50. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/conversation.py +0 -0
  51. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/delegation.py +0 -0
  52. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/events.py +0 -0
  53. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/leases.py +0 -0
  54. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/security.py +0 -0
  55. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/skills.py +0 -0
  56. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/tools.py +0 -0
  57. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/openhands/workspace.py +0 -0
  58. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/__init__.py +0 -0
  59. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/context.py +0 -0
  60. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/protocols.py +0 -0
  61. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/orchestrator/validator.py +0 -0
  62. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/project.py +0 -0
  63. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/provider_protocols.py +0 -0
  64. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/providers.py +0 -0
  65. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/pty.py +0 -0
  66. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/references.py +0 -0
  67. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/routing.py +0 -0
  68. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/runner.py +0 -0
  69. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/safety.py +0 -0
  70. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/screenshot.py +0 -0
  71. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/state.py +0 -0
  72. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/swarm.py +0 -0
  73. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/tasks.py +0 -0
  74. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/tool_policy.py +0 -0
  75. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code/workspace.py +0 -0
  76. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/dependency_links.txt +0 -0
  77. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/entry_points.txt +0 -0
  78. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/requires.txt +0 -0
  79. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tamfis_code.egg-info/top_level.txt +0 -0
  80. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agent_definitions.py +0 -0
  81. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agents.py +0 -0
  82. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_agents_delegation.py +0 -0
  83. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_build_installation.py +0 -0
  84. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_cli_commands.py +0 -0
  85. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_clipboard.py +0 -0
  86. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_completion.py +0 -0
  87. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_context_rollover.py +0 -0
  88. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_custom_commands.py +0 -0
  89. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_cwd_validation.py +0 -0
  90. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_doctor_session_diagnostics.py +0 -0
  91. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_enforcer.py +0 -0
  92. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_fake_tool_detection.py +0 -0
  93. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_find_references.py +0 -0
  94. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_hooks.py +0 -0
  95. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_indexer.py +0 -0
  96. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_instructions.py +0 -0
  97. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_integration_new.py +0 -0
  98. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_interactive_standalone.py +0 -0
  99. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_local_pty.py +0 -0
  100. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp.py +0 -0
  101. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_search_bounds.py +0 -0
  102. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_standalone_degradation.py +0 -0
  103. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_mcp_stdio_server.py +0 -0
  104. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_metrics.py +0 -0
  105. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_openhands_runtime.py +0 -0
  106. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_openrouter_cost_routing.py +0 -0
  107. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_parity_contracts.py +0 -0
  108. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_plan_mode_gate.py +0 -0
  109. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_plan_steps_and_attempts.py +0 -0
  110. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_provider_protocols.py +0 -0
  111. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_reasoning_effort_capability.py +0 -0
  112. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_reasoning_plan.py +0 -0
  113. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_references.py +0 -0
  114. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_render_task_diagnostics.py +0 -0
  115. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_routing.py +0 -0
  116. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_runner_helpers.py +0 -0
  117. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_runner_local.py +0 -0
  118. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_safety.py +0 -0
  119. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_safety_manifest.py +0 -0
  120. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_swarm.py +0 -0
  121. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_api_client.py +0 -0
  122. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval.py +0 -0
  123. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval_flow.py +0 -0
  124. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_config.py +0 -0
  125. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_intent.py +0 -0
  126. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_render.py +0 -0
  127. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_repl_exit.py +0 -0
  128. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_tasks.py +0 -0
  129. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_tamfis_code_workspace.py +0 -0
  130. {tamfis_code-0.6.13 → tamfis_code-0.6.14}/tests/test_version_consistency.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamfis-code
3
- Version: 0.6.13
3
+ Version: 0.6.14
4
4
  Summary: TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals
5
5
  Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
6
6
  Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tamfis-code"
3
- version = "0.6.13"
3
+ version = "0.6.14"
4
4
  description = "TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -17,7 +17,7 @@ See docs/REMOTE_AGENT_MASTER_SPEC.md, Phase 21, for the original --remote
17
17
  architecture's spec.
18
18
  """
19
19
 
20
- __version__ = "0.6.13"
20
+ __version__ = "0.6.14"
21
21
 
22
22
  # Bumped whenever a CLI release requires a minimum backend Remote API
23
23
  # contract version. Only meaningful for --remote; the standalone path has no
@@ -128,6 +128,8 @@ class CodeAnalyzer(SubAgent):
128
128
  return issues
129
129
 
130
130
  class TestGenerator(SubAgent):
131
+ # Application component, not a pytest test container.
132
+ __test__ = False
131
133
  """Generates tests for code"""
132
134
 
133
135
  def __init__(self):
@@ -20,6 +20,8 @@ except ImportError:
20
20
 
21
21
 
22
22
  class TestEnforcer:
23
+ # Application component, not a pytest test container.
24
+ __test__ = False
23
25
  """Detects and runs the current workspace's own test/build commands."""
24
26
 
25
27
  def __init__(self, workspace_root: Optional[Path] = None):
@@ -6,6 +6,7 @@ management.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import asyncio
9
10
  import re
10
11
  from pathlib import Path
11
12
  from typing import Optional
@@ -43,7 +44,13 @@ from .custom_commands import (
43
44
  from .doctor import run_doctor
44
45
  from .live_input import LiveInputListener
45
46
  from .render import StreamRenderer, print_banner, print_error, print_recent_thread, print_resume_plan_status, print_unified_diff
46
- from .runner import resolve_approval_decision_async, retry_task_and_stream, run_ai_task_and_stream, run_shell_command
47
+ from .runner import (
48
+ TaskOutcome,
49
+ resolve_approval_decision_async,
50
+ retry_task_and_stream,
51
+ run_ai_task_and_stream,
52
+ run_shell_command,
53
+ )
47
54
  from .runner_local import run_local_agent_turn, run_local_shell_command
48
55
  from .pty import LocalPtyBroker
49
56
  from .safety import revert_mutation as local_revert_mutation
@@ -61,6 +68,48 @@ from .workspace import (
61
68
  # those tools only collapse genuinely large pastes.
62
69
  PASTE_COLLAPSE_LINE_THRESHOLD = 3
63
70
 
71
+ async def _run_cancellable_local_turn(
72
+ *,
73
+ session_id: int,
74
+ renderer: StreamRenderer,
75
+ config: Config,
76
+ turn_coro,
77
+ ) -> TaskOutcome:
78
+ """Run one standalone agent turn with immediate terminal cancellation."""
79
+
80
+ agent_task = asyncio.create_task(turn_coro)
81
+
82
+ def _interrupt(_classification: str) -> None:
83
+ if not agent_task.done():
84
+ agent_task.cancel()
85
+
86
+ live_input = LiveInputListener(
87
+ session_id=session_id,
88
+ renderer=renderer,
89
+ cli_config=config,
90
+ interrupt_callback=_interrupt,
91
+ )
92
+ live_input.start()
93
+
94
+ try:
95
+ return await agent_task
96
+ except asyncio.CancelledError:
97
+ classification = live_input.interrupt_classification or "cancel"
98
+
99
+ if classification == "exit":
100
+ return TaskOutcome(
101
+ status="exited",
102
+ error="Exit requested by user",
103
+ )
104
+
105
+ return TaskOutcome(
106
+ status="cancelled",
107
+ error="Task cancelled by user.",
108
+ )
109
+ finally:
110
+ live_input.stop()
111
+
112
+
64
113
  HELP_TEXT = """\
65
114
  Natural-language text submits a full coding-agent task (mode: coding).
66
115
  $ <command> explicit shell command
@@ -469,19 +518,19 @@ async def run_interactive(
469
518
  model_state = local_state.get_session_state(workspace.session_id)
470
519
  plan_objective = local_state.plan_execution_objective(plan)
471
520
  if standalone:
472
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
473
- live_input.start()
474
- try:
475
- outcome = await run_local_agent_turn(
521
+ outcome = await _run_cancellable_local_turn(
522
+ session_id=workspace.session_id,
523
+ renderer=renderer,
524
+ config=config,
525
+ turn_coro=run_local_agent_turn(
476
526
  provider_manager, provider_type, model,
477
527
  [*conversation_history, {"role": "user", "content": plan_objective}],
478
528
  console, renderer,
479
529
  workspace_root=workspace.workspace_root, session_id=workspace.session_id,
480
530
  approval_policy=config.approval_policy, interactive=True, cli_config=config,
481
531
  allow_swarm_tool=True,
482
- )
483
- finally:
484
- live_input.stop()
532
+ ),
533
+ )
485
534
  _append_turn_to_history(
486
535
  conversation_history, objective=plan_objective,
487
536
  answer=outcome.summary if outcome.status == "completed" else None,
@@ -1294,10 +1343,11 @@ async def run_interactive(
1294
1343
  print_error(console, "Existing uncommitted changes detected; retry is blocked to preserve user edits.")
1295
1344
  continue
1296
1345
  renderer = StreamRenderer(console, mode_label=mode_label_for_policy(config.approval_policy))
1297
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
1298
- live_input.start()
1299
- try:
1300
- outcome = await run_local_agent_turn(
1346
+ outcome = await _run_cancellable_local_turn(
1347
+ session_id=workspace.session_id,
1348
+ renderer=renderer,
1349
+ config=config,
1350
+ turn_coro=run_local_agent_turn(
1301
1351
  provider_manager, provider_type, model,
1302
1352
  [*conversation_history, {"role": "user", "content": objective}],
1303
1353
  console, renderer,
@@ -1305,9 +1355,8 @@ async def run_interactive(
1305
1355
  approval_policy=config.approval_policy, interactive=True,
1306
1356
  read_only=mode in {"chat", "audit", "plan"}, cli_config=config,
1307
1357
  allow_swarm_tool=True,
1308
- )
1309
- finally:
1310
- live_input.stop()
1358
+ ),
1359
+ )
1311
1360
  renderer.finish()
1312
1361
  _append_turn_to_history(
1313
1362
  conversation_history, objective=objective,
@@ -1404,10 +1453,11 @@ async def run_interactive(
1404
1453
  "payload": {"content": submitted_text},
1405
1454
  })
1406
1455
  if standalone:
1407
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
1408
- live_input.start()
1409
- try:
1410
- outcome = await run_local_agent_turn(
1456
+ outcome = await _run_cancellable_local_turn(
1457
+ session_id=workspace.session_id,
1458
+ renderer=renderer,
1459
+ config=config,
1460
+ turn_coro=run_local_agent_turn(
1411
1461
  provider_manager, provider_type, model,
1412
1462
  [*conversation_history, {"role": "user", "content": intent.objective}],
1413
1463
  console, renderer,
@@ -1415,15 +1465,18 @@ async def run_interactive(
1415
1465
  approval_policy=config.approval_policy, interactive=True,
1416
1466
  read_only=intent.mode in {"chat", "audit", "plan"}, cli_config=config,
1417
1467
  allow_swarm_tool=True,
1418
- )
1419
- finally:
1420
- live_input.stop()
1468
+ ),
1469
+ )
1421
1470
  renderer.finish()
1422
1471
  _append_turn_to_history(
1423
1472
  conversation_history, objective=intent.objective,
1424
1473
  answer=outcome.summary if outcome.status == "completed" else None,
1425
1474
  )
1426
1475
  last_turn = (intent.objective, intent.mode)
1476
+ if outcome.status == "exited":
1477
+ if local_pty is not None:
1478
+ local_pty.close()
1479
+ break
1427
1480
  if intent.mode == "plan" and outcome.status == "completed" and outcome.summary:
1428
1481
  saved = local_state.save_plan(workspace.session_id, objective=intent.objective, content=outcome.summary)
1429
1482
  console.print(f"[green]Plan saved[/green] · {saved.id}")
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
  import asyncio
13
13
  import contextlib
14
14
  import sys
15
- from typing import Optional
15
+ from typing import Callable, Optional
16
16
 
17
17
  from prompt_toolkit.formatted_text import HTML
18
18
 
@@ -30,10 +30,19 @@ _CTRL_Y = b"\x19"
30
30
  class LiveInputListener:
31
31
  """Run a persistent, asynchronous follow-up editor during a task."""
32
32
 
33
- def __init__(self, *, session_id: int, renderer: StreamRenderer, cli_config: Config) -> None:
33
+ def __init__(
34
+ self,
35
+ *,
36
+ session_id: int,
37
+ renderer: StreamRenderer,
38
+ cli_config: Config,
39
+ interrupt_callback: Optional[Callable[[str], None]] = None,
40
+ ) -> None:
34
41
  self.session_id = session_id
35
42
  self.renderer = renderer
36
43
  self.cli_config = cli_config
44
+ self._interrupt_callback = interrupt_callback
45
+ self._interrupt_classification: Optional[str] = None
37
46
  self._is_tty = bool(getattr(sys.stdin, "isatty", lambda: False)())
38
47
  self._input_task: Optional[asyncio.Task] = None
39
48
  self._interject_task: Optional[asyncio.Task] = None
@@ -136,11 +145,15 @@ class LiveInputListener:
136
145
 
137
146
  @bindings.add("escape")
138
147
  def _cancel_running_turn(event) -> None:
139
- # Match the interrupt affordance offered by other terminal agents:
140
- # Escape cancels the active turn, while the prompt remains usable
141
- # for subsequent work once the runner reaches a safe boundary.
142
- self._enqueue_control("cancel")
143
- event.app.exit(result="")
148
+ # Escape cancels the active turn immediately and returns control
149
+ # to the ordinary REPL without terminating Tamfis-Code.
150
+ if self._paused or not self._active:
151
+ return
152
+
153
+ self._request_interrupt("cancel")
154
+
155
+ if not event.app.is_done:
156
+ event.app.exit(result="")
144
157
 
145
158
  session = PromptSession(key_bindings=bindings)
146
159
  self._prompt_session = session
@@ -163,9 +176,44 @@ class LiveInputListener:
163
176
  return
164
177
  except EOFError:
165
178
  return
179
+
180
+ # Escape sets _paused before exiting the prompt, so its empty
181
+ # result must not be queued or cause another editor to start.
182
+ if self._paused:
183
+ break
184
+
185
+ # A genuine line already returned by prompt_async remains valid
186
+ # even when the listener was deactivated during that prompt.
187
+ # Queue it first, then let the loop terminate cleanly.
166
188
  self._enqueue(text)
189
+
190
+ if not self._active:
191
+ break
192
+
167
193
  self._prompt_session = None
168
194
 
195
+ @property
196
+ def interrupt_classification(self) -> Optional[str]:
197
+ return self._interrupt_classification
198
+
199
+ def _request_interrupt(self, classification: str) -> None:
200
+ """Record and immediately propagate one terminal interrupt.
201
+
202
+ The durable queue item remains useful for history and recovery, but
203
+ immediate cancellation is delivered through interrupt_callback so a
204
+ blocked provider request or long-running tool does not need to reach
205
+ another orchestration boundary first.
206
+ """
207
+ if self._interrupt_classification is not None:
208
+ return
209
+
210
+ self._interrupt_classification = classification
211
+ self._paused = True
212
+ self._enqueue_control(classification)
213
+
214
+ if self._interrupt_callback is not None:
215
+ self._interrupt_callback(classification)
216
+
169
217
  def _enqueue_control(self, classification: str) -> None:
170
218
  item = local_state.enqueue_instruction(
171
219
  self.session_id, "", classification=classification,
@@ -135,22 +135,27 @@ class AgentOrchestrator:
135
135
  self._advance_plan_step()
136
136
 
137
137
  def _advance_plan_step(self) -> None:
138
- """Best-effort plan-step progress: each observed tool result is real,
139
- concrete evidence that execution has moved forward, so use the
140
- running tool-call count to advance a cursor through the plan's
141
- steps -- reserving the final step (report/validate) so it is never
142
- marked done before completion actually happens. This does not claim
143
- precise step-to-action correspondence (see _sync_plan_progress)."""
138
+ """Expose execution progress without fabricating completion.
139
+
140
+ A tool result proves only that a tool call occurred. It does not prove
141
+ that a particular plan step completed, that a file changed, or that a
142
+ validation command passed. Therefore this method may mark one pending
143
+ step as in progress, but it must never mark a step completed.
144
+
145
+ Final completion is assigned only by complete() after validation.
146
+ """
144
147
  assert self.run is not None
145
148
  if self.run.plan is None or not self.run.plan.steps:
146
149
  return
147
- steps = self.run.plan.steps
148
- cursor = min(len(self.run.tool_records) - 1, max(len(steps) - 2, 0))
149
- for step in steps[:cursor]:
150
- if step.status not in {"completed", "failed"}:
151
- step.status = "completed"
152
- if steps[cursor].status not in {"completed", "failed"}:
153
- steps[cursor].status = "in_progress"
150
+
151
+ # Keep at most one step active. Additional tool calls are evidence
152
+ # that work continues, not that later plan steps have started.
153
+ if not any(step.status == "in_progress" for step in self.run.plan.steps):
154
+ for step in self.run.plan.steps:
155
+ if step.status == "pending":
156
+ step.status = "in_progress"
157
+ break
158
+
154
159
  self._sync_plan_progress()
155
160
 
156
161
  def mark_repair(self, reason: str) -> None: