tamfis-code 0.6.14__tar.gz → 1.3.0__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 (161) hide show
  1. {tamfis_code-0.6.14/tamfis_code.egg-info → tamfis_code-1.3.0}/PKG-INFO +1 -1
  2. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/pyproject.toml +2 -2
  3. tamfis_code-1.3.0/setup.py +2 -0
  4. tamfis_code-1.3.0/tamfis_code/__init__.py +49 -0
  5. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/cli.py +156 -7
  6. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/doctor.py +15 -0
  7. tamfis_code-1.3.0/tamfis_code/indexer.py +37 -0
  8. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/interactive.py +34 -1
  9. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/live_input.py +56 -39
  10. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/local_chat.py +33 -2
  11. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/mcp.py +40 -7
  12. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/agent.py +2 -2
  13. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/__init__.py +2 -0
  14. tamfis_code-1.3.0/tamfis_code/orchestrator/approvals.py +64 -0
  15. tamfis_code-1.3.0/tamfis_code/orchestrator/completion.py +48 -0
  16. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/engine.py +76 -22
  17. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/planner.py +41 -0
  18. tamfis_code-1.3.0/tamfis_code/orchestrator/repair.py +68 -0
  19. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/providers.py +219 -19
  20. tamfis_code-1.3.0/tamfis_code/release_verification.py +276 -0
  21. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/render.py +1 -1
  22. tamfis_code-1.3.0/tamfis_code/routing.py +55 -0
  23. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/runner.py +26 -1
  24. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/runner_local.py +161 -8
  25. tamfis_code-1.3.0/tamfis_code/runtime/__init__.py +19 -0
  26. tamfis_code-1.3.0/tamfis_code/runtime/budgets.py +18 -0
  27. tamfis_code-1.3.0/tamfis_code/runtime/checkpoint.py +79 -0
  28. tamfis_code-1.3.0/tamfis_code/runtime/cognitive.py +147 -0
  29. tamfis_code-1.3.0/tamfis_code/runtime/controller.py +147 -0
  30. tamfis_code-1.3.0/tamfis_code/runtime/evidence.py +77 -0
  31. tamfis_code-1.3.0/tamfis_code/runtime/journal.py +119 -0
  32. tamfis_code-1.3.0/tamfis_code/runtime/memory.py +210 -0
  33. tamfis_code-1.3.0/tamfis_code/runtime/repository_index.py +82 -0
  34. tamfis_code-1.3.0/tamfis_code/runtime/reviewer.py +24 -0
  35. tamfis_code-1.3.0/tamfis_code/runtime/state.py +61 -0
  36. tamfis_code-1.3.0/tamfis_code/runtime/steering.py +31 -0
  37. tamfis_code-1.3.0/tamfis_code/runtime/unified.py +301 -0
  38. tamfis_code-1.3.0/tamfis_code/runtime/workspace_authority.py +132 -0
  39. tamfis_code-1.3.0/tamfis_code/runtime/worktree.py +109 -0
  40. {tamfis_code-0.6.14 → tamfis_code-1.3.0/tamfis_code.egg-info}/PKG-INFO +1 -1
  41. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code.egg-info/SOURCES.txt +31 -58
  42. tamfis_code-1.3.0/tests/test_phase1_reliability.py +94 -0
  43. tamfis_code-1.3.0/tests/test_phase2_unified_runtime.py +80 -0
  44. tamfis_code-1.3.0/tests/test_phase3_claude_behaviour.py +110 -0
  45. tamfis_code-1.3.0/tests/test_phase4_release_verification.py +26 -0
  46. tamfis_code-1.3.0/tests/test_phase5_cognitive_orchestration.py +92 -0
  47. tamfis_code-1.3.0/tests/test_phase6_workspace_authority.py +65 -0
  48. tamfis_code-1.3.0/tests/test_phase7_memory.py +65 -0
  49. tamfis_code-1.3.0/tests/test_phase8_worktree.py +66 -0
  50. tamfis_code-1.3.0/tests/test_phase9_decision_logic.py +49 -0
  51. tamfis_code-1.3.0/tests/test_runtime_controller.py +55 -0
  52. tamfis_code-0.6.14/MANIFEST.in +0 -3
  53. tamfis_code-0.6.14/USAGE_INSTALL_RELEASE.md +0 -85
  54. tamfis_code-0.6.14/tamfis_code/__init__.py +0 -54
  55. tamfis_code-0.6.14/tamfis_code/indexer.py +0 -632
  56. tamfis_code-0.6.14/tamfis_code/routing.py +0 -165
  57. tamfis_code-0.6.14/tests/test_agent_definitions.py +0 -76
  58. tamfis_code-0.6.14/tests/test_agents.py +0 -248
  59. tamfis_code-0.6.14/tests/test_agents_delegation.py +0 -83
  60. tamfis_code-0.6.14/tests/test_build_installation.py +0 -108
  61. tamfis_code-0.6.14/tests/test_cli_commands.py +0 -601
  62. tamfis_code-0.6.14/tests/test_clipboard.py +0 -50
  63. tamfis_code-0.6.14/tests/test_completion.py +0 -49
  64. tamfis_code-0.6.14/tests/test_context_rollover.py +0 -327
  65. tamfis_code-0.6.14/tests/test_custom_commands.py +0 -90
  66. tamfis_code-0.6.14/tests/test_cwd_validation.py +0 -179
  67. tamfis_code-0.6.14/tests/test_doctor_session_diagnostics.py +0 -210
  68. tamfis_code-0.6.14/tests/test_enforcer.py +0 -178
  69. tamfis_code-0.6.14/tests/test_fake_tool_detection.py +0 -171
  70. tamfis_code-0.6.14/tests/test_find_references.py +0 -122
  71. tamfis_code-0.6.14/tests/test_github_command_surface.py +0 -18
  72. tamfis_code-0.6.14/tests/test_hooks.py +0 -144
  73. tamfis_code-0.6.14/tests/test_indexer.py +0 -185
  74. tamfis_code-0.6.14/tests/test_instructions.py +0 -75
  75. tamfis_code-0.6.14/tests/test_integration_new.py +0 -88
  76. tamfis_code-0.6.14/tests/test_intelligent_planner.py +0 -304
  77. tamfis_code-0.6.14/tests/test_interactive_standalone.py +0 -538
  78. tamfis_code-0.6.14/tests/test_live_input.py +0 -354
  79. tamfis_code-0.6.14/tests/test_local_pty.py +0 -36
  80. tamfis_code-0.6.14/tests/test_mcp.py +0 -611
  81. tamfis_code-0.6.14/tests/test_mcp_search_bounds.py +0 -131
  82. tamfis_code-0.6.14/tests/test_mcp_standalone_degradation.py +0 -46
  83. tamfis_code-0.6.14/tests/test_mcp_stdio_server.py +0 -113
  84. tamfis_code-0.6.14/tests/test_metrics.py +0 -80
  85. tamfis_code-0.6.14/tests/test_openhands_runtime.py +0 -113
  86. tamfis_code-0.6.14/tests/test_openrouter_cost_routing.py +0 -95
  87. tamfis_code-0.6.14/tests/test_orchestrator.py +0 -320
  88. tamfis_code-0.6.14/tests/test_parity_contracts.py +0 -152
  89. tamfis_code-0.6.14/tests/test_plan_mode_gate.py +0 -115
  90. tamfis_code-0.6.14/tests/test_plan_steps_and_attempts.py +0 -110
  91. tamfis_code-0.6.14/tests/test_provider_protocols.py +0 -94
  92. tamfis_code-0.6.14/tests/test_reasoning_effort_capability.py +0 -58
  93. tamfis_code-0.6.14/tests/test_reasoning_plan.py +0 -310
  94. tamfis_code-0.6.14/tests/test_references.py +0 -202
  95. tamfis_code-0.6.14/tests/test_render_task_diagnostics.py +0 -53
  96. tamfis_code-0.6.14/tests/test_routing.py +0 -268
  97. tamfis_code-0.6.14/tests/test_runner_helpers.py +0 -155
  98. tamfis_code-0.6.14/tests/test_runner_local.py +0 -2098
  99. tamfis_code-0.6.14/tests/test_safety.py +0 -364
  100. tamfis_code-0.6.14/tests/test_safety_manifest.py +0 -110
  101. tamfis_code-0.6.14/tests/test_swarm.py +0 -431
  102. tamfis_code-0.6.14/tests/test_tamfis_code_api_client.py +0 -174
  103. tamfis_code-0.6.14/tests/test_tamfis_code_approval.py +0 -234
  104. tamfis_code-0.6.14/tests/test_tamfis_code_approval_flow.py +0 -229
  105. tamfis_code-0.6.14/tests/test_tamfis_code_config.py +0 -329
  106. tamfis_code-0.6.14/tests/test_tamfis_code_intent.py +0 -100
  107. tamfis_code-0.6.14/tests/test_tamfis_code_render.py +0 -550
  108. tamfis_code-0.6.14/tests/test_tamfis_code_repl_exit.py +0 -70
  109. tamfis_code-0.6.14/tests/test_tamfis_code_tasks.py +0 -77
  110. tamfis_code-0.6.14/tests/test_tamfis_code_workspace.py +0 -487
  111. tamfis_code-0.6.14/tests/test_version_consistency.py +0 -40
  112. tamfis_code-0.6.14/tests/test_workspace_scope.py +0 -497
  113. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/README.md +0 -0
  114. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/setup.cfg +0 -0
  115. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/__main__.py +0 -0
  116. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/agent_definitions.py +0 -0
  117. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/agents.py +0 -0
  118. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/api_client.py +0 -0
  119. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/clipboard.py +0 -0
  120. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/completion.py +0 -0
  121. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/config.py +0 -0
  122. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/custom_commands.py +0 -0
  123. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/enforcer.py +0 -0
  124. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/evidence.py +0 -0
  125. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/github_commands.py +0 -0
  126. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/hooks.py +0 -0
  127. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/instructions.py +0 -0
  128. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/local_tools.py +0 -0
  129. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/mcp_stdio_server.py +0 -0
  130. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/metrics.py +0 -0
  131. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/model_registry.py +0 -0
  132. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/__init__.py +0 -0
  133. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/agent_server.py +0 -0
  134. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/automation.py +0 -0
  135. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/compat.py +0 -0
  136. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/conversation.py +0 -0
  137. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/delegation.py +0 -0
  138. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/events.py +0 -0
  139. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/leases.py +0 -0
  140. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/security.py +0 -0
  141. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/skills.py +0 -0
  142. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/tools.py +0 -0
  143. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/openhands/workspace.py +0 -0
  144. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/context.py +0 -0
  145. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/protocols.py +0 -0
  146. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/orchestrator/validator.py +0 -0
  147. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/project.py +0 -0
  148. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/provider_protocols.py +0 -0
  149. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/pty.py +0 -0
  150. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/references.py +0 -0
  151. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/safety.py +0 -0
  152. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/screenshot.py +0 -0
  153. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/state.py +0 -0
  154. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/swarm.py +0 -0
  155. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/tasks.py +0 -0
  156. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/tool_policy.py +0 -0
  157. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code/workspace.py +0 -0
  158. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code.egg-info/dependency_links.txt +0 -0
  159. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code.egg-info/entry_points.txt +0 -0
  160. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code.egg-info/requires.txt +0 -0
  161. {tamfis_code-0.6.14 → tamfis_code-1.3.0}/tamfis_code.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamfis-code
3
- Version: 0.6.14
3
+ Version: 1.3.0
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.14"
3
+ version = "1.3.0"
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"
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "rich>=13.0",
11
11
  "prompt_toolkit>=3.0",
12
12
  # Provider calls (providers.py) go through the openai SDK's client against
13
- # each vendor's OpenAI-compatible endpoint (HF, NVIDIA NIM, OpenRouter)
13
+ # each provider's OpenAI-compatible endpoint (Ollama Cloud, NVIDIA NIM, HF, OpenRouter)
14
14
  # -- this was imported but never declared, only working because a
15
15
  # copy happened to already be installed system-wide.
16
16
  "openai>=1.30",
@@ -0,0 +1,2 @@
1
+ from setuptools import setup
2
+ setup()
@@ -0,0 +1,49 @@
1
+ """Tamfis-Code, a standalone terminal coding agent.
2
+
3
+ The package keeps import-time side effects deliberately minimal. Public
4
+ components are loaded lazily so a missing optional module cannot prevent the
5
+ CLI or deterministic runtime from starting.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from typing import Any
11
+
12
+ __version__ = "1.3.0"
13
+ MIN_COMPATIBLE_API_VERSION = "remote-ai-v2"
14
+
15
+ _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
16
+ "ShellCompleter": (".completion", "ShellCompleter"),
17
+ "MetricsTracker": (".metrics", "MetricsTracker"),
18
+ "StreamMetrics": (".metrics", "StreamMetrics"),
19
+ "AgentManager": (".agents", "AgentManager"),
20
+ "SubAgent": (".agents", "SubAgent"),
21
+ "CodeAnalyzer": (".agents", "CodeAnalyzer"),
22
+ "TestGenerator": (".agents", "TestGenerator"),
23
+ "DocGenerator": (".agents", "DocGenerator"),
24
+ "MCPServer": (".mcp", "MCPServer"),
25
+ "ToolDefinition": (".mcp", "ToolDefinition"),
26
+ "call_tool": (".mcp", "call_tool"),
27
+ "CodeIndexer": (".indexer", "CodeIndexer"),
28
+ "CodeSymbol": (".indexer", "CodeSymbol"),
29
+ "CodeFile": (".indexer", "CodeFile"),
30
+ }
31
+
32
+ __all__ = ["__version__", "MIN_COMPATIBLE_API_VERSION", *_LAZY_EXPORTS]
33
+
34
+
35
+ def __getattr__(name: str) -> Any:
36
+ target = _LAZY_EXPORTS.get(name)
37
+ if target is None:
38
+ raise AttributeError(name)
39
+ module_name, attribute = target
40
+ try:
41
+ module = import_module(module_name, __name__)
42
+ except ModuleNotFoundError as exc:
43
+ raise AttributeError(
44
+ f"Optional Tamfis-Code component {name!r} is unavailable because "
45
+ f"module {module_name!r} is not installed."
46
+ ) from exc
47
+ value = getattr(module, attribute)
48
+ globals()[name] = value
49
+ return value
@@ -36,6 +36,8 @@ from .api_client import (
36
36
  )
37
37
  from .config import APPROVAL_MODES, CONFIG_DIR, Config, Credentials, load_config
38
38
  from .doctor import run_doctor
39
+ from .runtime.memory import MemoryRecord, MemoryType, get_memory_store
40
+ from .runtime.worktree import WorktreeError, create_worktree, list_worktrees, remove_worktree
39
41
  from .render import StreamRenderer, print_banner, print_error, print_recent_thread, print_resume_plan_status, print_unified_diff
40
42
  from .runner import (
41
43
  ACTIVE_TASK_STATUSES,
@@ -53,7 +55,7 @@ from .local_chat import _PROVIDER_ALIASES
53
55
  # validation and then failed with "Unknown local provider" inside
54
56
  # resolve_provider_type every time.
55
57
  _PROVIDER_CHOICES = sorted(_PROVIDER_ALIASES.keys())
56
- _PROVIDER_HELP = "tamfis/tamfisgpt (subscription API), hf, nvidia, openrouter, or auto (default)."
58
+ _PROVIDER_HELP = "tamfis/tamfisgpt (subscription API), ollama_cloud, nvidia, hf, openrouter, or auto (default)."
57
59
 
58
60
  EXIT_OK = 0
59
61
  EXIT_TASK_FAILED = 1
@@ -100,7 +102,7 @@ def async_command(fn):
100
102
  @click.option("--approval", "approval_policy", type=click.Choice(APPROVAL_MODES), default=None, help="Override the configured approval policy for this invocation. Note: 'never' means deny everything outright -- the opposite of 'auto'/'full-auto' (which mean never PROMPT, i.e. auto-approve). It is not a synonym for auto-approve.")
101
103
  @click.option("--api-base", "api_base", default=None, help="Override the configured Remote API base URL.")
102
104
  @click.option("--cwd", "cwd_override", type=click.Path(exists=True, file_okay=False), default=None, help="Treat this directory as the workspace instead of the current directory.")
103
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default) -- which provider the bare (no-subcommand) interactive REPL calls directly.")
105
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default) -- which provider the bare (no-subcommand) interactive REPL calls directly.")
104
106
  @click.option("--model", default=None, help="Provider-specific model id for the bare interactive REPL; defaults to that provider's default model.")
105
107
  @click.option("--remote", is_flag=True, default=False, help="Use the legacy TamfisGPT Remote Workspace backend for the bare interactive REPL instead of calling a provider directly.")
106
108
  @click.version_option(__version__, prog_name="tamfis-code")
@@ -343,6 +345,125 @@ def workspace_remove(ctx: click.Context, path: Path):
343
345
  console.print(f"[green]Workspace removed:[/green] {target}")
344
346
 
345
347
 
348
+ @cli.group(name="memory")
349
+ def memory_group():
350
+ """Manage durable, cross-session memory records."""
351
+
352
+
353
+ @memory_group.command(name="list")
354
+ @click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), default=None)
355
+ def memory_list(memory_type: Optional[str]):
356
+ console = Console()
357
+ store = get_memory_store()
358
+ records = store.list(MemoryType(memory_type) if memory_type else None)
359
+ if not records:
360
+ console.print("[dim]No memory records.[/dim]")
361
+ return
362
+ table = Table(show_header=True, header_style="bold")
363
+ table.add_column("name")
364
+ table.add_column("type")
365
+ table.add_column("description")
366
+ for record in records:
367
+ table.add_row(record.name, record.type.value, record.description)
368
+ console.print(table)
369
+
370
+
371
+ @memory_group.command(name="show")
372
+ @click.argument("name")
373
+ def memory_show(name: str):
374
+ console = Console()
375
+ record = get_memory_store().load(name)
376
+ if record is None:
377
+ print_error(console, f"No memory record named {name!r}.")
378
+ raise SystemExit(EXIT_TASK_FAILED)
379
+ console.print(f"[bold]{record.name}[/bold] ({record.type.value})")
380
+ console.print(record.description)
381
+ console.print()
382
+ console.print(record.content)
383
+
384
+
385
+ @memory_group.command(name="add")
386
+ @click.argument("name")
387
+ @click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), required=True)
388
+ @click.option("--description", required=True)
389
+ @click.option("--content", required=True)
390
+ def memory_add(name: str, memory_type: str, description: str, content: str):
391
+ console = Console()
392
+ record = get_memory_store().save(
393
+ MemoryRecord(name=name, type=MemoryType(memory_type), description=description, content=content)
394
+ )
395
+ console.print(f"[green]Saved memory record:[/green] {record.name}")
396
+
397
+
398
+ @memory_group.command(name="forget")
399
+ @click.argument("name")
400
+ def memory_forget(name: str):
401
+ console = Console()
402
+ if get_memory_store().delete(name):
403
+ console.print(f"[green]Forgot memory record:[/green] {name}")
404
+ else:
405
+ print_error(console, f"No memory record named {name!r}.")
406
+ raise SystemExit(EXIT_TASK_FAILED)
407
+
408
+
409
+ @cli.group(name="worktree")
410
+ def worktree_group():
411
+ """Manage isolated git worktrees for background/parallel execution."""
412
+
413
+
414
+ @worktree_group.command(name="list")
415
+ @click.pass_context
416
+ def worktree_list_cmd(ctx: click.Context):
417
+ console = Console(no_color=not ctx.obj["config"].colour)
418
+ try:
419
+ handles = list_worktrees(ctx.obj["workspace_root"])
420
+ except WorktreeError as exc:
421
+ print_error(console, str(exc))
422
+ raise SystemExit(EXIT_TASK_FAILED)
423
+ if not handles:
424
+ console.print("[dim]No worktrees.[/dim]")
425
+ return
426
+ table = Table(show_header=True, header_style="bold")
427
+ table.add_column("branch")
428
+ table.add_column("path")
429
+ for handle in handles:
430
+ table.add_row(handle.branch, str(handle.path))
431
+ console.print(table)
432
+
433
+
434
+ @worktree_group.command(name="create")
435
+ @click.argument("branch")
436
+ @click.option("--base", default="HEAD", help="Ref the new worktree branch is created from.")
437
+ @click.pass_context
438
+ def worktree_create_cmd(ctx: click.Context, branch: str, base: str):
439
+ console = Console(no_color=not ctx.obj["config"].colour)
440
+ try:
441
+ handle = create_worktree(ctx.obj["workspace_root"], branch=branch, base=base)
442
+ except WorktreeError as exc:
443
+ print_error(console, str(exc))
444
+ raise SystemExit(EXIT_TASK_FAILED)
445
+ console.print(f"[green]Worktree created:[/green] {handle.path} (branch {handle.branch})")
446
+
447
+
448
+ @worktree_group.command(name="remove")
449
+ @click.argument("branch")
450
+ @click.option("--force", is_flag=True, default=False, help="Remove even if the worktree has uncommitted changes.")
451
+ @click.pass_context
452
+ def worktree_remove_cmd(ctx: click.Context, branch: str, force: bool):
453
+ console = Console(no_color=not ctx.obj["config"].colour)
454
+ try:
455
+ handles = {h.branch: h for h in list_worktrees(ctx.obj["workspace_root"])}
456
+ handle = handles.get(branch)
457
+ if handle is None:
458
+ print_error(console, f"No worktree found for branch {branch!r}.")
459
+ raise SystemExit(EXIT_TASK_FAILED)
460
+ remove_worktree(handle, force=force)
461
+ except WorktreeError as exc:
462
+ print_error(console, str(exc))
463
+ raise SystemExit(EXIT_TASK_FAILED)
464
+ console.print(f"[green]Worktree removed:[/green] {branch}")
465
+
466
+
346
467
  @cli.command(name="cwd")
347
468
  @click.argument("path", required=False, type=click.Path(exists=True, file_okay=False, path_type=Path))
348
469
  @click.option("--remote", is_flag=True, default=False, help="Update the working directory on the legacy TamfisGPT Remote Workspace backend instead of the local session.")
@@ -422,7 +543,7 @@ async def init(ctx: click.Context, remote: bool):
422
543
 
423
544
 
424
545
  @cli.command()
425
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
546
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
426
547
  @click.option("--remote", is_flag=True, default=False, help="Check the legacy TamfisGPT Remote Workspace backend instead of local provider connectivity.")
427
548
  @click.pass_context
428
549
  @async_command
@@ -1359,7 +1480,7 @@ async def run(ctx: click.Context, command: str, background: bool, remote: bool):
1359
1480
 
1360
1481
  @cli.command()
1361
1482
  @click.argument("session_id", type=int, required=False, default=None)
1362
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
1483
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1363
1484
  @click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
1364
1485
  @click.option("--remote", is_flag=True, default=False, help="Resume a session on the legacy TamfisGPT Remote Workspace backend instead of a local one.")
1365
1486
  @click.pass_context
@@ -1426,7 +1547,7 @@ async def resume(ctx: click.Context, session_id: Optional[int], provider: str, m
1426
1547
 
1427
1548
  @cli.command()
1428
1549
  @click.argument("task_id", required=False, default=None)
1429
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
1550
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1430
1551
  @click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
1431
1552
  @click.option("--remote", is_flag=True, default=False, help="Retry a task on the legacy TamfisGPT Remote Workspace backend instead of resending locally.")
1432
1553
  @click.pass_context
@@ -1948,7 +2069,7 @@ def completion_cmd(shell: str):
1948
2069
  @click.option('--task', '-t', 'tasks', multiple=True, help='Task description (repeatable for delegate)')
1949
2070
  @click.option('--file', '-f', help='File to operate on')
1950
2071
  @click.option('--max-concurrency', default=1, show_default=True, help='Max concurrent delegated sub-tasks')
1951
- @click.option('--provider', default="auto", help="hf, nvidia, openrouter, or auto (default).")
2072
+ @click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1952
2073
  @click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
1953
2074
  @click.pass_context
1954
2075
  def agent_cmd(ctx: click.Context, action: str, tasks: tuple[str, ...], file: str, max_concurrency: int, provider: str, model: Optional[str]):
@@ -2189,7 +2310,7 @@ def providers_command(ctx: click.Context):
2189
2310
 
2190
2311
  @cli.command('local')
2191
2312
  @click.argument('objective', required=False)
2192
- @click.option('--provider', default="auto", help="hf, nvidia, openrouter, or auto (default).")
2313
+ @click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
2193
2314
  @click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
2194
2315
  @click.option('--no-tools', 'no_tools', is_flag=True, default=False, help="Disable read-only repo tools (read_file/list_directory/search_code/get_git_info) for this turn.")
2195
2316
  @click.option('--agent', 'full_agent', is_flag=True, default=False, help="Full read/write/execute tool access (write_file/edit_file/execute_command) via the local risk/approval/mutation-ledger layer, instead of read-only Q&A. Standalone -- no TamfisGPT backend involved.")
@@ -2382,6 +2503,34 @@ from .enforcer import TestEnforcer, run_enforcer, add_enforcer_command
2382
2503
  # TestEnforcer -- using those flags raised AttributeError at runtime.
2383
2504
  add_enforcer_command(cli)
2384
2505
 
2506
+ @cli.command("verify-release")
2507
+ @click.option("--artifact", "artifacts", multiple=True, type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Built wheel, source archive, checksum, or manifest to verify.")
2508
+ @click.option("--output-dir", type=click.Path(file_okay=False, path_type=Path), default=None, help="Directory for JSON and Markdown verification reports.")
2509
+ @click.pass_context
2510
+ def verify_release_command(ctx: click.Context, artifacts: tuple[Path, ...], output_dir: Optional[Path]):
2511
+ """Run the Phase 4 release gate without contacting any AI provider."""
2512
+ from .release_verification import run_release_verification
2513
+
2514
+ root: Path = ctx.obj["workspace_root"]
2515
+ report = run_release_verification(root, artifacts)
2516
+ console = Console(no_color=not ctx.obj["config"].colour)
2517
+ table = Table(show_header=True, header_style="bold")
2518
+ table.add_column("Category")
2519
+ table.add_column("Check")
2520
+ table.add_column("Result")
2521
+ table.add_column("Detail")
2522
+ for check in report.checks:
2523
+ table.add_row(check.category, check.name, "PASS" if check.passed else "FAIL", check.detail)
2524
+ console.print(table)
2525
+
2526
+ destination = output_dir or (root / "dist")
2527
+ json_path = report.write_json(destination / f"tamfis_code-{report.version}-verification-report.json")
2528
+ md_path = report.write_markdown(destination / f"tamfis_code-{report.version}-verification-report.md")
2529
+ console.print(f"Verification reports: {json_path} {md_path}")
2530
+ if not report.passed:
2531
+ raise SystemExit(EXIT_TASK_FAILED)
2532
+
2533
+
2385
2534
  # Familiar GitHub CLI command surface, delegated to the installed `gh` binary.
2386
2535
  from .github_commands import register_github_commands
2387
2536
  register_github_commands(cli)
@@ -21,6 +21,7 @@ from .api_client import AuthRequiredError, RemoteAPIClient, RemoteAPIError
21
21
  from .config import Config, load_credentials
22
22
  from .providers import get_provider_status
23
23
  from .workspace import resolve_local_workspace
24
+ from .runtime.journal import JOURNAL_PATH, recent_failures
24
25
 
25
26
 
26
27
  @dataclass
@@ -190,6 +191,20 @@ def _diagnose_local_session(workspace_root: Path) -> list[CheckResult]:
190
191
  summary = ", ".join(f"{count} {name}" for name, count in sorted(by_status.items())) or "no steps"
191
192
  results.append(CheckResult("Active plan step progress", "PASS", summary))
192
193
 
194
+ failures = recent_failures(5)
195
+ if failures:
196
+ latest = failures[0]
197
+ detail = (
198
+ f"{len(failures)} recent failed/cancelled execution(s); latest "
199
+ f"{latest.get('mode', 'unknown')} at {latest.get('timestamp', 'unknown')}: "
200
+ f"{latest.get('error') or latest.get('status') or 'unknown failure'}"
201
+ )
202
+ results.append(CheckResult("Unified runtime recent failures", "WARNING", detail))
203
+ elif JOURNAL_PATH.exists():
204
+ results.append(CheckResult("Unified runtime recent failures", "PASS", "none in the recent execution journal"))
205
+ else:
206
+ results.append(CheckResult("Unified runtime execution journal", "WARNING", "no unified runtime executions recorded yet"))
207
+
193
208
  return results
194
209
 
195
210
 
@@ -0,0 +1,37 @@
1
+ """Small dependency-free code index used by optional public exports."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ import re
6
+
7
+ @dataclass(frozen=True)
8
+ class CodeSymbol:
9
+ name: str
10
+ kind: str
11
+ line: int
12
+
13
+ @dataclass
14
+ class CodeFile:
15
+ path: str
16
+ language: str
17
+ symbols: list[CodeSymbol] = field(default_factory=list)
18
+
19
+ class CodeIndexer:
20
+ def __init__(self, root: str | Path): self.root=Path(root).resolve()
21
+ def index_file(self, path: str | Path) -> CodeFile:
22
+ p=Path(path).resolve(); text=p.read_text(encoding='utf-8',errors='replace')
23
+ suffix=p.suffix.lower(); language={'.py':'python','.ts':'typescript','.tsx':'typescript','.js':'javascript','.jsx':'javascript'}.get(suffix,'text')
24
+ symbols=[]
25
+ patterns=[('class',re.compile(r'^\s*class\s+([A-Za-z_]\w*)')),('function',re.compile(r'^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)')),('function',re.compile(r'^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)'))]
26
+ for n,line in enumerate(text.splitlines(),1):
27
+ for kind,pat in patterns:
28
+ m=pat.search(line)
29
+ if m: symbols.append(CodeSymbol(m.group(1),kind,n)); break
30
+ return CodeFile(str(p),language,symbols)
31
+ def index(self) -> list[CodeFile]:
32
+ result=[]
33
+ for p in self.root.rglob('*'):
34
+ if p.is_file() and p.suffix.lower() in {'.py','.ts','.tsx','.js','.jsx'} and not any(part.startswith('.') for part in p.relative_to(self.root).parts):
35
+ try: result.append(self.index_file(p))
36
+ except OSError: pass
37
+ return result
@@ -107,7 +107,7 @@ async def _run_cancellable_local_turn(
107
107
  error="Task cancelled by user.",
108
108
  )
109
109
  finally:
110
- live_input.stop()
110
+ await live_input.stop()
111
111
 
112
112
 
113
113
  HELP_TEXT = """\
@@ -589,8 +589,41 @@ async def run_interactive(
589
589
  break
590
590
  except EOFError:
591
591
  break
592
+ resolved_paste_count = 0
593
+ resolved_paste_chars = 0
594
+ resolved_paste_lines = 0
595
+
592
596
  for placeholder, real_text in pending_pastes.items():
597
+ if placeholder not in text:
598
+ continue
599
+
593
600
  text = text.replace(placeholder, real_text)
601
+ resolved_paste_count += 1
602
+ resolved_paste_chars += len(real_text)
603
+ resolved_paste_lines += real_text.count("\n") + (
604
+ 0 if real_text.endswith("\n") else 1
605
+ )
606
+
607
+ unresolved_pastes = re.findall(
608
+ r"\[Pasted text #\d+ \+\d+ lines\]",
609
+ text,
610
+ )
611
+ if unresolved_pastes:
612
+ print_error(
613
+ console,
614
+ "Pasted-text resolution failed. The underlying paste was "
615
+ "not available, so no task was submitted. Unresolved: "
616
+ + ", ".join(unresolved_pastes),
617
+ )
618
+ continue
619
+
620
+ if resolved_paste_count:
621
+ console.print(
622
+ "[dim]diagnostics: Resolved "
623
+ f"{resolved_paste_count} pasted-text block(s), "
624
+ f"{resolved_paste_lines:,} line(s), "
625
+ f"{resolved_paste_chars:,} character(s).[/dim]"
626
+ )
594
627
 
595
628
  text = text.strip()
596
629
  if not text:
@@ -61,21 +61,29 @@ class LiveInputListener:
61
61
  self._active = True
62
62
  self._schedule_prompt()
63
63
 
64
- def stop(self) -> None:
64
+ async def stop(self) -> None:
65
+ """Stop input ownership and wait until prompt-toolkit releases stdin."""
65
66
  self._active = False
66
- self._cancel_prompt()
67
+ await self._shutdown_prompt()
67
68
  if self.renderer.live_input_listener is self:
68
69
  self.renderer.live_input_listener = None
69
- self.renderer.resume_live()
70
+ self.renderer.resume_live()
70
71
 
71
72
  def pause(self) -> None:
73
+ """Synchronously request prompt shutdown before another UI reads stdin."""
72
74
  self._paused = True
75
+ self._request_prompt_exit()
73
76
  self._cancel_prompt()
74
77
 
75
78
  def resume(self) -> None:
76
79
  self._paused = False
77
- if self._active:
78
- self._schedule_prompt()
80
+ if not self._active:
81
+ return
82
+ task = self._input_task
83
+ if task is not None and not task.done():
84
+ task.add_done_callback(lambda _task: self._schedule_prompt() if self._active and not self._paused else None)
85
+ return
86
+ self._schedule_prompt()
79
87
 
80
88
  def _dispatch(self) -> None:
81
89
  """Compatibility hook for older embedders; start() no longer uses it."""
@@ -102,11 +110,28 @@ class LiveInputListener:
102
110
  if self._input_task is None or self._input_task.done():
103
111
  self._input_task = asyncio.create_task(self._input_loop())
104
112
 
113
+ def _request_prompt_exit(self) -> None:
114
+ session = self._prompt_session
115
+ app = getattr(session, "app", None)
116
+ if app is not None and not getattr(app, "is_done", False):
117
+ with contextlib.suppress(Exception):
118
+ app.exit(result="")
119
+
105
120
  def _cancel_prompt(self) -> None:
121
+ task = self._input_task
122
+ if task is not None and not task.done():
123
+ task.cancel()
124
+
125
+ async def _shutdown_prompt(self) -> None:
126
+ self._request_prompt_exit()
106
127
  task = self._input_task
107
128
  self._input_task = None
108
129
  if task is not None and not task.done():
109
130
  task.cancel()
131
+ if task is not None:
132
+ with contextlib.suppress(asyncio.CancelledError, Exception):
133
+ await task
134
+ self._prompt_session = None
110
135
 
111
136
  def invalidate(self) -> None:
112
137
  """Refresh the prompt footer after a streamed phase/status update."""
@@ -157,40 +182,32 @@ class LiveInputListener:
157
182
 
158
183
  session = PromptSession(key_bindings=bindings)
159
184
  self._prompt_session = session
160
- while self._active and not self._paused:
161
- try:
162
- # patch_stdout keeps concurrent tool/assistant output above
163
- # the current line and redraws the line beneath it.
164
- with patch_stdout(raw=True):
165
- text = await session.prompt_async(
166
- "message> ", bottom_toolbar=self._bottom_toolbar,
167
- )
168
- except asyncio.CancelledError:
169
- return
170
- except KeyboardInterrupt:
171
- # Ctrl+C is the process/REPL exit affordance. The active
172
- # runner's signal path remains reserved for true process
173
- # interrupts; queue an explicit exit so the local runner
174
- # can finish its current safe boundary and the REPL exits.
175
- self._enqueue_control("exit")
176
- return
177
- except EOFError:
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.
188
- self._enqueue(text)
189
-
190
- if not self._active:
191
- break
192
-
193
- self._prompt_session = None
185
+ try:
186
+ while self._active and not self._paused:
187
+ try:
188
+ with patch_stdout(raw=True):
189
+ text = await session.prompt_async(
190
+ "message> ", bottom_toolbar=self._bottom_toolbar,
191
+ )
192
+ except asyncio.CancelledError:
193
+ raise
194
+ except KeyboardInterrupt:
195
+ self._request_interrupt("cancel")
196
+ return
197
+ except EOFError:
198
+ return
199
+
200
+ if self._paused:
201
+ break
202
+ self._enqueue(text)
203
+ if not self._active:
204
+ break
205
+ finally:
206
+ if self._prompt_session is session:
207
+ self._prompt_session = None
208
+ current = asyncio.current_task()
209
+ if self._input_task is current:
210
+ self._input_task = None
194
211
 
195
212
  @property
196
213
  def interrupt_classification(self) -> Optional[str]:
@@ -38,7 +38,7 @@ def resolve_provider_type(name: Optional[str]) -> ProviderType:
38
38
  return _PROVIDER_ALIASES[key]
39
39
 
40
40
 
41
- async def run_local_turn(
41
+ async def _run_local_turn_impl(
42
42
  manager: ProviderManager,
43
43
  provider: ProviderType,
44
44
  messages: List[Dict[str, Any]],
@@ -108,7 +108,7 @@ async def run_local_turn(
108
108
  return "(Stopped after exhausting the local tool-call round limit -- try a narrower question.)"
109
109
 
110
110
 
111
- async def stream_local_turn(
111
+ async def _stream_local_turn_impl(
112
112
  manager: ProviderManager,
113
113
  provider: ProviderType,
114
114
  messages: List[Dict[str, Any]],
@@ -119,3 +119,34 @@ async def stream_local_turn(
119
119
  common single-shot/no-tools case still gets real token streaming)."""
120
120
  async for chunk in manager.chat_completion(provider, messages, model=model, stream=True):
121
121
  yield chunk
122
+
123
+
124
+ async def run_local_turn(
125
+ manager: ProviderManager,
126
+ provider: ProviderType,
127
+ messages: List[Dict[str, Any]],
128
+ model: Optional[str],
129
+ console: Console,
130
+ *,
131
+ use_tools: bool = True,
132
+ ) -> str:
133
+ """Compatibility adapter routed through the unified runtime."""
134
+ from .runtime.unified import get_unified_runtime
135
+ return await get_unified_runtime().execute_local_chat(
136
+ manager=manager, provider=provider, messages=messages, model=model,
137
+ console=console, use_tools=use_tools,
138
+ )
139
+
140
+
141
+ async def stream_local_turn(
142
+ manager: ProviderManager,
143
+ provider: ProviderType,
144
+ messages: List[Dict[str, Any]],
145
+ model: Optional[str],
146
+ ) -> AsyncIterator[str]:
147
+ """Compatibility streaming adapter routed through the unified runtime."""
148
+ from .runtime.unified import get_unified_runtime
149
+ async for chunk in get_unified_runtime().stream_local_chat(
150
+ manager=manager, provider=provider, messages=messages, model=model,
151
+ ):
152
+ yield chunk