tamfis-code 0.6.7__tar.gz → 0.6.13__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 (132) hide show
  1. {tamfis_code-0.6.7/tamfis_code.egg-info → tamfis_code-0.6.13}/PKG-INFO +1 -1
  2. tamfis_code-0.6.13/USAGE_INSTALL_RELEASE.md +85 -0
  3. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/pyproject.toml +1 -1
  4. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/__init__.py +1 -1
  5. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/cli.py +4 -0
  6. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/config.py +23 -0
  7. tamfis_code-0.6.13/tamfis_code/github_commands.py +65 -0
  8. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/interactive.py +17 -6
  9. tamfis_code-0.6.13/tamfis_code/live_input.py +216 -0
  10. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/mcp.py +14 -3
  11. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/model_registry.py +41 -4
  12. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/engine.py +17 -0
  13. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/project.py +30 -30
  14. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/providers.py +68 -13
  15. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/render.py +116 -14
  16. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/runner_local.py +351 -32
  17. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/workspace.py +8 -1
  18. {tamfis_code-0.6.7 → tamfis_code-0.6.13/tamfis_code.egg-info}/PKG-INFO +1 -1
  19. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code.egg-info/SOURCES.txt +3 -3
  20. tamfis_code-0.6.13/tests/test_github_command_surface.py +15 -0
  21. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_live_input.py +4 -4
  22. tamfis_code-0.6.13/tests/test_parity_contracts.py +152 -0
  23. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_routing.py +51 -13
  24. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_runner_local.py +83 -0
  25. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_config.py +22 -0
  26. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_render.py +3 -1
  27. tamfis_code-0.6.7/USAGE_INSTALL_RELEASE.md +0 -67
  28. tamfis_code-0.6.7/tamfis_code/live_input.py +0 -201
  29. tamfis_code-0.6.7/tamfis_code/sessions.py +0 -253
  30. tamfis_code-0.6.7/tests/test_sessions.py +0 -140
  31. tamfis_code-0.6.7/tests/test_workspace_discovery_gaps.py +0 -119
  32. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/MANIFEST.in +0 -0
  33. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/README.md +0 -0
  34. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/setup.cfg +0 -0
  35. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/__main__.py +0 -0
  36. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/agent_definitions.py +0 -0
  37. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/agents.py +0 -0
  38. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/api_client.py +0 -0
  39. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/clipboard.py +0 -0
  40. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/completion.py +0 -0
  41. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/custom_commands.py +0 -0
  42. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/doctor.py +0 -0
  43. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/enforcer.py +0 -0
  44. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/evidence.py +0 -0
  45. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/hooks.py +0 -0
  46. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/indexer.py +0 -0
  47. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/instructions.py +0 -0
  48. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/local_chat.py +0 -0
  49. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/local_tools.py +0 -0
  50. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/mcp_stdio_server.py +0 -0
  51. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/metrics.py +0 -0
  52. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/__init__.py +0 -0
  53. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/agent.py +0 -0
  54. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/agent_server.py +0 -0
  55. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/automation.py +0 -0
  56. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/compat.py +0 -0
  57. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/conversation.py +0 -0
  58. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/delegation.py +0 -0
  59. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/events.py +0 -0
  60. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/leases.py +0 -0
  61. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/security.py +0 -0
  62. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/skills.py +0 -0
  63. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/tools.py +0 -0
  64. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/openhands/workspace.py +0 -0
  65. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/__init__.py +0 -0
  66. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/context.py +0 -0
  67. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/planner.py +0 -0
  68. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/protocols.py +0 -0
  69. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/orchestrator/validator.py +0 -0
  70. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/provider_protocols.py +0 -0
  71. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/pty.py +0 -0
  72. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/references.py +0 -0
  73. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/routing.py +0 -0
  74. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/runner.py +0 -0
  75. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/safety.py +0 -0
  76. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/screenshot.py +0 -0
  77. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/state.py +0 -0
  78. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/swarm.py +0 -0
  79. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/tasks.py +0 -0
  80. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code/tool_policy.py +0 -0
  81. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code.egg-info/dependency_links.txt +0 -0
  82. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code.egg-info/entry_points.txt +0 -0
  83. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code.egg-info/requires.txt +0 -0
  84. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tamfis_code.egg-info/top_level.txt +0 -0
  85. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_agent_definitions.py +0 -0
  86. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_agents.py +0 -0
  87. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_agents_delegation.py +0 -0
  88. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_build_installation.py +0 -0
  89. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_cli_commands.py +0 -0
  90. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_clipboard.py +0 -0
  91. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_completion.py +0 -0
  92. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_context_rollover.py +0 -0
  93. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_custom_commands.py +0 -0
  94. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_cwd_validation.py +0 -0
  95. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_doctor_session_diagnostics.py +0 -0
  96. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_enforcer.py +0 -0
  97. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_fake_tool_detection.py +0 -0
  98. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_find_references.py +0 -0
  99. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_hooks.py +0 -0
  100. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_indexer.py +0 -0
  101. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_instructions.py +0 -0
  102. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_integration_new.py +0 -0
  103. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_interactive_standalone.py +0 -0
  104. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_local_pty.py +0 -0
  105. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_mcp.py +0 -0
  106. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_mcp_search_bounds.py +0 -0
  107. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_mcp_standalone_degradation.py +0 -0
  108. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_mcp_stdio_server.py +0 -0
  109. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_metrics.py +0 -0
  110. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_openhands_runtime.py +0 -0
  111. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_openrouter_cost_routing.py +0 -0
  112. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_orchestrator.py +0 -0
  113. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_plan_mode_gate.py +0 -0
  114. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_plan_steps_and_attempts.py +0 -0
  115. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_provider_protocols.py +0 -0
  116. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_reasoning_effort_capability.py +0 -0
  117. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_reasoning_plan.py +0 -0
  118. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_references.py +0 -0
  119. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_render_task_diagnostics.py +0 -0
  120. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_runner_helpers.py +0 -0
  121. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_safety.py +0 -0
  122. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_safety_manifest.py +0 -0
  123. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_swarm.py +0 -0
  124. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_api_client.py +0 -0
  125. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_approval.py +0 -0
  126. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_approval_flow.py +0 -0
  127. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_intent.py +0 -0
  128. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_repl_exit.py +0 -0
  129. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_tasks.py +0 -0
  130. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_tamfis_code_workspace.py +0 -0
  131. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_version_consistency.py +0 -0
  132. {tamfis_code-0.6.7 → tamfis_code-0.6.13}/tests/test_workspace_scope.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamfis-code
3
- Version: 0.6.7
3
+ Version: 0.6.13
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
@@ -0,0 +1,85 @@
1
+ # Install and release TamfisGPT Code
2
+
3
+ ## Install on any server
4
+
5
+ Use the isolated installer so the CLI does not depend on the host Python
6
+ environment or a server-specific checkout:
7
+
8
+ ```bash
9
+ ./install.sh
10
+ # Root installation: /usr/local/lib/tamfis-code + /usr/local/bin/tamfis-code
11
+ # User installation: ~/.local/share/tamfis-code + ~/.local/bin/tamfis-code
12
+ # Custom location:
13
+ TAMFIS_CODE_PREFIX=/opt/tamfis-code TAMFIS_CODE_BIN_DIR=/usr/local/bin ./install.sh
14
+ ```
15
+
16
+ The same package is portable to macOS and Windows. On Windows, create a
17
+ virtual environment with `py -m venv`, install the project with `py -m pip
18
+ install .`, and expose the virtualenv's `Scripts` directory on PATH. No
19
+ machine-specific checkout path is compiled into the package.
20
+
21
+ When managing several local projects, configure only the approved roots:
22
+
23
+ ```toml
24
+ workspace_roots = ["/path/to/project-a", "/path/to/project-b"]
25
+ ```
26
+
27
+ Place that in the user's `config.toml`, or use the portable environment
28
+ variable `TAMFIS_CODE_WORKSPACE_ROOTS` (path-separated). The launch directory
29
+ is always discovered automatically; other roots are allowed only when listed
30
+ or explicitly approved with `tamfis-code workspace add <path>`.
31
+
32
+ The runtime stores session state in the platform's user configuration
33
+ directory. It does not require a TamfisGPT server process on the same host.
34
+
35
+ ## Configure the TamfisGPT subscription API
36
+
37
+ Create an API key from the TamfisGPT account connected to the user's active
38
+ iOS subscription, then set:
39
+
40
+ ```bash
41
+ export TAMFIS_API_KEY='tamfis_sk_live_...'
42
+ export TAMFIS_API_BASE='https://gpt.tamfitronics.com/api/v1/openai'
43
+ tamfis-code doctor
44
+ ```
45
+
46
+ The API key is the user's entitlement boundary. Admin credentials are not
47
+ needed for ordinary subscribers. The API handles model access and billing;
48
+ the installed CLI keeps workspace files, commands, PTYs, approvals, and the
49
+ mutation ledger on the local machine.
50
+
51
+ ## Interactive follow-up input
52
+
53
+ While a task is running, the `message>` editor remains available in the same
54
+ terminal. Type normally and press Enter; the line is echoed with a durable
55
+ queue ID, then applied at the next safe model-round boundary. Multiple queued
56
+ updates are processed in order. A second terminal can enqueue the same session
57
+ with:
58
+
59
+ ```bash
60
+ tamfis-code queue "also inspect the authentication flow" --classification follow_up
61
+ ```
62
+
63
+ `/queue` lists pending requests in the interactive REPL. `Ctrl+C` remains the
64
+ interrupt/exit control. No special Ctrl+Y shortcut is required.
65
+
66
+ ## Build and publish a release
67
+
68
+ Run the full checks, build a clean distribution, and inspect it before
69
+ publishing:
70
+
71
+ ```bash
72
+ python3 -m pytest -q
73
+ python3 -m build
74
+ python3 -m twine check dist/*
75
+ ```
76
+
77
+ Publishing requires an account token configured in the release environment:
78
+
79
+ ```bash
80
+ python3 -m twine upload dist/*
81
+ ```
82
+
83
+ The GitHub repository should receive the same source revision and release
84
+ notes as the PyPI upload. Never commit API keys, subscription tokens, local
85
+ session state, or generated build artifacts.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tamfis-code"
3
- version = "0.6.7"
3
+ version = "0.6.13"
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.7"
20
+ __version__ = "0.6.13"
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
@@ -2381,3 +2381,7 @@ from .enforcer import TestEnforcer, run_enforcer, add_enforcer_command
2381
2381
  # flags wired to _run_shell_checks/_run_type_checks, neither of which exist on
2382
2382
  # TestEnforcer -- using those flags raised AttributeError at runtime.
2383
2383
  add_enforcer_command(cli)
2384
+
2385
+ # Familiar GitHub CLI command surface, delegated to the installed `gh` binary.
2386
+ from .github_commands import register_github_commands
2387
+ register_github_commands(cli)
@@ -146,6 +146,11 @@ class Config:
146
146
  # TamfisGPT tenant who wants that be the default, the same way Claude
147
147
  # Code/Codex/kimi-code default to their respective hosted accounts.
148
148
  default_backend: str = "standalone"
149
+ # Additional local roots that may be used by this installation. The
150
+ # launch directory is always included separately; these roots make a
151
+ # multi-project service practical without granting access to the whole
152
+ # filesystem.
153
+ workspace_roots: list[str] = field(default_factory=list)
149
154
  sources: dict[str, str] = field(default_factory=dict) # field -> where it came from, for `doctor`/`config`
150
155
 
151
156
  def as_dict(self) -> dict[str, Any]:
@@ -158,6 +163,7 @@ class Config:
158
163
  "debug": self.debug,
159
164
  "enable_subagent_delegation": self.enable_subagent_delegation,
160
165
  "default_backend": self.default_backend,
166
+ "workspace_roots": self.workspace_roots,
161
167
  }
162
168
 
163
169
 
@@ -193,6 +199,14 @@ def load_config(project_root: Optional[Path] = None) -> Config:
193
199
  if data.get("default_backend") in ("standalone", "remote"):
194
200
  cfg.default_backend = str(data["default_backend"])
195
201
  cfg.sources["default_backend"] = source_name
202
+ configured_roots = data.get("workspace_roots")
203
+ if isinstance(configured_roots, list):
204
+ cfg.workspace_roots = [
205
+ str(Path(item).expanduser().resolve())
206
+ for item in configured_roots
207
+ if isinstance(item, str) and item.strip()
208
+ ]
209
+ cfg.sources["workspace_roots"] = source_name
196
210
 
197
211
  env_api_base = os.environ.get("TAMFIS_CODE_API_BASE")
198
212
  if env_api_base:
@@ -214,6 +228,15 @@ def load_config(project_root: Optional[Path] = None) -> Config:
214
228
  cfg.default_backend = env_backend
215
229
  cfg.sources["default_backend"] = "env TAMFIS_CODE_DEFAULT_BACKEND"
216
230
 
231
+ env_roots = os.environ.get("TAMFIS_CODE_WORKSPACE_ROOTS")
232
+ if env_roots:
233
+ cfg.workspace_roots = [
234
+ str(Path(item.strip()).expanduser().resolve())
235
+ for item in env_roots.split(os.pathsep)
236
+ if item.strip()
237
+ ]
238
+ cfg.sources["workspace_roots"] = "env TAMFIS_CODE_WORKSPACE_ROOTS"
239
+
217
240
  return cfg
218
241
 
219
242
 
@@ -0,0 +1,65 @@
1
+ """GitHub CLI parity commands.
2
+
3
+ Tamfis-Code exposes the familiar ``gh`` command surface without reimplementing
4
+ GitHub authentication or API semantics. Each command delegates directly to the
5
+ installed GitHub CLI, preserves the current working directory, streams PTY/TTY
6
+ I/O unchanged, and returns the exact child exit code.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ from typing import Iterable
14
+
15
+ import click
16
+
17
+ GITHUB_COMMANDS: tuple[str, ...] = (
18
+ "alias", "api", "auth", "browse", "cache", "co", "codespace",
19
+ "completion", "config", "extension", "gist", "gpg-key", "issue",
20
+ "label", "org", "pr", "project", "release", "repo", "ruleset",
21
+ "run", "search", "secret", "ssh-key", "status", "variable", "workflow",
22
+ )
23
+
24
+
25
+ def _gh_binary() -> str:
26
+ binary = shutil.which("gh")
27
+ if not binary:
28
+ raise click.ClickException(
29
+ "GitHub CLI (`gh`) is not installed or is not available in PATH."
30
+ )
31
+ return binary
32
+
33
+
34
+ def run_gh(subcommand: str, args: Iterable[str]) -> None:
35
+ """Execute one gh subcommand with transparent terminal behaviour."""
36
+ command = [_gh_binary(), subcommand, *args]
37
+ completed = subprocess.run(command, cwd=os.getcwd(), check=False)
38
+ if completed.returncode:
39
+ raise click.exceptions.Exit(completed.returncode)
40
+
41
+
42
+ def register_github_commands(root: click.Group) -> None:
43
+ """Register all requested gh-compatible top-level commands."""
44
+ existing = set(root.commands)
45
+ for command_name in GITHUB_COMMANDS:
46
+ # Tamfis-Code owns a richer native command with the same name.
47
+ # Keep that native implementation rather than shadowing it.
48
+ if command_name in existing:
49
+ continue
50
+
51
+ def callback(args: tuple[str, ...], _name: str = command_name) -> None:
52
+ run_gh(_name, args)
53
+
54
+ command = click.Command(
55
+ name=command_name,
56
+ callback=callback,
57
+ params=[click.Argument(["args"], nargs=-1, type=click.UNPROCESSED)],
58
+ context_settings={
59
+ "ignore_unknown_options": True,
60
+ "allow_extra_args": True,
61
+ "help_option_names": [],
62
+ },
63
+ help=f"Delegate to `gh {command_name}` with full PTY/TTY passthrough.",
64
+ )
65
+ root.add_command(command)
@@ -111,7 +111,7 @@ $ <command> explicit shell command
111
111
  /mode <name> switch mode: manual | accept-edits | auto | plan
112
112
  Shift+Tab cycle mode without typing a command (shown in the prompt as [mode]);
113
113
  also works while a task is already running, not just at this prompt
114
- Ctrl+T while a task is running: open the "queue next" input editor
114
+ message> while a task is running: type a message and press Enter
115
115
  /model show the active model route
116
116
  /model list [route] list coding models (route: hf or openrouter)
117
117
  /model auto restore Hugging Face -> OpenRouter automatic routing
@@ -372,10 +372,8 @@ async def run_interactive(
372
372
  )
373
373
  console.print(
374
374
  "[dim]Type /help for commands. Paste up to 1,000,000 characters; Alt+Enter adds a newline. "
375
- "Shift+Tab cycles mode (manual/accept-edits/auto/plan) -- works while a task is running too, "
376
- "not just at this prompt. Ctrl+T opens a visible `queue next>` editor while a task is running; "
377
- "your submitted text is acknowledged with a queue id and applied at the next safe round boundary. "
378
- "Ctrl+D or Ctrl+C exits.[/dim]\n"
375
+ "While a task runs, a normal message prompt remains available: type and press Enter; "
376
+ "your text is queued without a special shortcut. Shift+Tab cycles mode. Ctrl+D or Ctrl+C exits.[/dim]\n"
379
377
  )
380
378
 
381
379
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -990,7 +988,7 @@ async def run_interactive(
990
988
  "/mode auto auto-approves everything, never prompts (server safeguards still apply)\n"
991
989
  "/mode plan read-only: propose without executing\n"
992
990
  "Shift+Tab cycle manual -> accept-edits -> auto -> plan without typing a command\n"
993
- " (also works mid-task, not just here); Ctrl+T mid-task queues a follow-up\n"
991
+ " (also works mid-task, not just here); type in the message prompt to queue work\n"
994
992
  "Other raw policy values also work directly (--approval-only, no short alias): safe, workspace,\n"
995
993
  "read-only, plan-only, suggest, full-auto, and 'never' -- note 'never' means DENY everything\n"
996
994
  "(the opposite of what it sounds like; it is not a synonym for 'auto').[/dim]"
@@ -1319,6 +1317,10 @@ async def run_interactive(
1319
1317
  last_response_text = outcome.summary
1320
1318
  if outcome.status == "completed" and outcome.summary and not renderer.streamed_final_text:
1321
1319
  console.print(Markdown(outcome.summary))
1320
+ if outcome.status == "exited":
1321
+ if local_pty is not None:
1322
+ local_pty.close()
1323
+ break
1322
1324
  if outcome.status != "completed":
1323
1325
  print_error(console, outcome.error or f"task {outcome.status}")
1324
1326
  console.print()
@@ -1343,6 +1345,10 @@ async def run_interactive(
1343
1345
  last_response_text = outcome.summary
1344
1346
  if outcome.status == "completed" and outcome.summary and not renderer.streamed_final_text:
1345
1347
  console.print(Markdown(outcome.summary))
1348
+ if outcome.status == "exited":
1349
+ if local_pty is not None:
1350
+ local_pty.close()
1351
+ break
1346
1352
  if outcome.status != "completed":
1347
1353
  print_error(console, outcome.error or f"task {outcome.status}")
1348
1354
  except (AuthRequiredError, RemoteAPIError) as e:
@@ -1363,6 +1369,7 @@ async def run_interactive(
1363
1369
  or reply_state.turn_checkpoint or reply_state.conversation_history
1364
1370
  ),
1365
1371
  )
1372
+ submitted_text = text
1366
1373
  intent = parse_intent(text, custom_commands=custom_commands)
1367
1374
  try:
1368
1375
  if intent.kind == "shell":
@@ -1392,6 +1399,10 @@ async def run_interactive(
1392
1399
  continue
1393
1400
  renderer = StreamRenderer(console, mode_label=mode_label_for_policy(config.approval_policy))
1394
1401
  model_state = local_state.get_session_state(workspace.session_id)
1402
+ renderer.handle_event({
1403
+ "event_type": "user_message",
1404
+ "payload": {"content": submitted_text},
1405
+ })
1395
1406
  if standalone:
1396
1407
  live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
1397
1408
  live_input.start()
@@ -0,0 +1,216 @@
1
+ """Non-blocking follow-up input for a running standalone task.
2
+
3
+ The old implementation put stdin into cbreak mode, discarded ordinary
4
+ characters, and required Ctrl+Y to open a second editor. That made the
5
+ terminal feel frozen and made mouse selection/scrolling fight Rich's live
6
+ redraw. A running task now owns a normal prompt-toolkit line editor instead:
7
+ the user can type at any time, press Enter to queue a follow-up, and keep
8
+ typing the next one while the model continues streaming.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import contextlib
14
+ import sys
15
+ from typing import Optional
16
+
17
+ from prompt_toolkit.formatted_text import HTML
18
+
19
+ from . import state as local_state
20
+ from .config import Config, mode_label_for_policy, next_mode_in_cycle
21
+ from .render import StreamRenderer
22
+
23
+ _SHIFT_TAB = b"\x1b[Z"
24
+ # Retained only for backwards-compatible imports. Ctrl+Y is no longer read
25
+ # specially by the live listener; it is ordinary editable prompt input.
26
+ _CTRL_T = b"\x14"
27
+ _CTRL_Y = b"\x19"
28
+
29
+
30
+ class LiveInputListener:
31
+ """Run a persistent, asynchronous follow-up editor during a task."""
32
+
33
+ def __init__(self, *, session_id: int, renderer: StreamRenderer, cli_config: Config) -> None:
34
+ self.session_id = session_id
35
+ self.renderer = renderer
36
+ self.cli_config = cli_config
37
+ self._is_tty = bool(getattr(sys.stdin, "isatty", lambda: False)())
38
+ self._input_task: Optional[asyncio.Task] = None
39
+ self._interject_task: Optional[asyncio.Task] = None
40
+ self._prompt_session = None
41
+ self._paused = False
42
+ self._active = False
43
+
44
+ def start(self) -> None:
45
+ if not self._is_tty:
46
+ return
47
+ # Stop Rich's repainting while the prompt owns the terminal. Streamed
48
+ # assistant text is intentionally rendered as scrollback in this
49
+ # mode, so the input line and mouse scrolling never compete.
50
+ self.renderer.suspend_live()
51
+ self.renderer.live_input_listener = self
52
+ self._active = True
53
+ self._schedule_prompt()
54
+
55
+ def stop(self) -> None:
56
+ self._active = False
57
+ self._cancel_prompt()
58
+ if self.renderer.live_input_listener is self:
59
+ self.renderer.live_input_listener = None
60
+ self.renderer.resume_live()
61
+
62
+ def pause(self) -> None:
63
+ self._paused = True
64
+ self._cancel_prompt()
65
+
66
+ def resume(self) -> None:
67
+ self._paused = False
68
+ if self._active:
69
+ self._schedule_prompt()
70
+
71
+ def _dispatch(self) -> None:
72
+ """Compatibility hook for older embedders; start() no longer uses it."""
73
+ buf = bytes(getattr(self, "_buf", b""))
74
+ if _SHIFT_TAB in buf:
75
+ self._buf = bytearray()
76
+ self.cli_config.approval_policy = next_mode_in_cycle(self.cli_config.approval_policy)
77
+ self.renderer.handle_event({
78
+ "event_type": "diagnostics",
79
+ "payload": {"content": f"◆ Mode switched to {mode_label_for_policy(self.cli_config.approval_policy)}."},
80
+ })
81
+ if hasattr(self.renderer, "set_mode_label"):
82
+ self.renderer.set_mode_label(mode_label_for_policy(self.cli_config.approval_policy))
83
+ elif buf in {b"\x1b", b"\x1b["}:
84
+ return
85
+ elif _CTRL_Y in buf:
86
+ self._buf = bytearray()
87
+ if self._interject_task is None or self._interject_task.done():
88
+ self._interject_task = asyncio.create_task(self._interject())
89
+ elif buf:
90
+ self._buf = bytearray()
91
+
92
+ def _schedule_prompt(self) -> None:
93
+ if self._input_task is None or self._input_task.done():
94
+ self._input_task = asyncio.create_task(self._input_loop())
95
+
96
+ def _cancel_prompt(self) -> None:
97
+ task = self._input_task
98
+ self._input_task = None
99
+ if task is not None and not task.done():
100
+ task.cancel()
101
+
102
+ def invalidate(self) -> None:
103
+ """Refresh the prompt footer after a streamed phase/status update."""
104
+ app = getattr(self._prompt_session, "app", None)
105
+ if app is not None:
106
+ app.invalidate()
107
+
108
+ def _bottom_toolbar(self):
109
+ status = self.renderer.live_input_status()
110
+ mode = mode_label_for_policy(self.cli_config.approval_policy)
111
+ return HTML(
112
+ f"<b><ansicyan>◆</ansicyan></b> <b>{status}</b> "
113
+ f"<ansigray>│ {mode} │ esc interrupt │ shift+tab mode</ansigray>"
114
+ )
115
+
116
+ async def _input_loop(self) -> None:
117
+ from prompt_toolkit import PromptSession
118
+ from prompt_toolkit.key_binding import KeyBindings
119
+ from prompt_toolkit.patch_stdout import patch_stdout
120
+
121
+ bindings = KeyBindings()
122
+
123
+ # Some SSH/terminal clients emit focus-in/focus-out as CSI sequences
124
+ # (ESC [ I / ESC [ O). Because ``escape`` is also the intentional
125
+ # cancel binding below, prompt_toolkit can otherwise dispatch the
126
+ # first byte as a cancellation and leak the trailing ``I``/``O`` into
127
+ # the prompt. Consume those complete sequences before the bare-Esc
128
+ # binding gets a chance to act.
129
+ @bindings.add("escape", "[", "I")
130
+ def _ignore_focus_in(event) -> None:
131
+ return
132
+
133
+ @bindings.add("escape", "[", "O")
134
+ def _ignore_focus_out(event) -> None:
135
+ return
136
+
137
+ @bindings.add("escape")
138
+ 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="")
144
+
145
+ session = PromptSession(key_bindings=bindings)
146
+ self._prompt_session = session
147
+ while self._active and not self._paused:
148
+ try:
149
+ # patch_stdout keeps concurrent tool/assistant output above
150
+ # the current line and redraws the line beneath it.
151
+ with patch_stdout(raw=True):
152
+ text = await session.prompt_async(
153
+ "message> ", bottom_toolbar=self._bottom_toolbar,
154
+ )
155
+ except asyncio.CancelledError:
156
+ return
157
+ except KeyboardInterrupt:
158
+ # Ctrl+C is the process/REPL exit affordance. The active
159
+ # runner's signal path remains reserved for true process
160
+ # interrupts; queue an explicit exit so the local runner
161
+ # can finish its current safe boundary and the REPL exits.
162
+ self._enqueue_control("exit")
163
+ return
164
+ except EOFError:
165
+ return
166
+ self._enqueue(text)
167
+ self._prompt_session = None
168
+
169
+ def _enqueue_control(self, classification: str) -> None:
170
+ item = local_state.enqueue_instruction(
171
+ self.session_id, "", classification=classification,
172
+ )
173
+ self.renderer.handle_event({
174
+ "event_type": "diagnostics",
175
+ "payload": {"content": f"◆ Queued {classification} for the running task ({item.id})."},
176
+ })
177
+
178
+ def _enqueue(self, text: str) -> None:
179
+ text = text.strip()
180
+ if not text:
181
+ if self._active and not self._paused:
182
+ self._schedule_prompt()
183
+ return
184
+ item = local_state.enqueue_instruction(
185
+ self.session_id, text, classification="follow_up",
186
+ )
187
+ self.renderer.handle_event({
188
+ "event_type": "user_message",
189
+ "payload": {"content": text},
190
+ })
191
+ self.renderer.handle_event({
192
+ "event_type": "diagnostics",
193
+ "payload": {
194
+ "content": (
195
+ f"◆ Queued next instruction {item.id}: {text} "
196
+ "-- applied at the next safe round boundary."
197
+ ),
198
+ },
199
+ })
200
+ if self._active and not self._paused:
201
+ self._schedule_prompt()
202
+
203
+ async def _interject(self) -> None:
204
+ """Compatibility helper for callers/tests that submit one line."""
205
+ from prompt_toolkit import PromptSession
206
+ from prompt_toolkit.patch_stdout import patch_stdout
207
+
208
+ try:
209
+ with patch_stdout(raw=True):
210
+ text = await PromptSession().prompt_async("message> ")
211
+ except KeyboardInterrupt:
212
+ self._enqueue_control("exit")
213
+ return
214
+ except EOFError:
215
+ text = ""
216
+ self._enqueue(text)
@@ -163,6 +163,7 @@ class MCPServer:
163
163
  console: Optional[Any] = None, renderer: Optional[Any] = None, interactive: bool = False,
164
164
  transaction_id: Optional[str] = None,
165
165
  attachment_paths: Optional[List[str]] = None,
166
+ allowed_workspace_roots: Optional[List[str]] = None,
166
167
  ):
167
168
  # workspace_root/session_id are optional so existing callers that
168
169
  # construct MCPServer() with no arguments (tests, the `tools`/
@@ -170,6 +171,13 @@ class MCPServer:
170
171
  # enforcement on write_file/edit_file, no mutation-ledger recording.
171
172
  # The standalone agent loop (runner_local.py) always supplies both.
172
173
  self.workspace_root = workspace_root
174
+ self.allowed_workspace_roots = {
175
+ Path(item).expanduser().resolve()
176
+ for item in (allowed_workspace_roots or [])
177
+ if item
178
+ }
179
+ if workspace_root:
180
+ self.allowed_workspace_roots.add(Path(workspace_root).expanduser().resolve())
173
181
  self.session_id = session_id
174
182
  # Explicit CLI attachments are readable inputs, not extra writable
175
183
  # workspaces. Only these exact files are admitted; their parent
@@ -664,9 +672,12 @@ class MCPServer:
664
672
  p = base / p
665
673
  resolved = p.resolve()
666
674
  if self.workspace_root:
667
- root = base.resolve()
668
- if resolved != root and root not in resolved.parents:
669
- raise PermissionError(f"'{path}' resolves outside the workspace root ({root})")
675
+ roots = self.allowed_workspace_roots or {base.resolve()}
676
+ if not any(resolved == root or root in resolved.parents for root in roots):
677
+ rendered = ", ".join(str(root) for root in sorted(roots, key=str))
678
+ raise PermissionError(
679
+ f"'{path}' resolves outside the workspace; approved roots: ({rendered})"
680
+ )
670
681
  return resolved
671
682
 
672
683
  async def _write_file(self, path: str, content: str) -> str:
@@ -77,10 +77,47 @@ MODELS: dict[str, ModelRecord] = {
77
77
  ("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
78
78
  "frontier", "medium",
79
79
  ),
80
- "Qwen/Qwen2.5-7B-Instruct": ModelRecord(
81
- "Qwen/Qwen2.5-7B-Instruct", "hf",
82
- ModelCapabilities(long_context=False), 32768,
83
- ("single_file_explanation", "repository_search"), "balanced", "low",
80
+ "deepseek-ai/deepseek-v4-pro": ModelRecord(
81
+ "deepseek-ai/deepseek-v4-pro", "nvidia",
82
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 1000000,
83
+ ("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
84
+ "frontier", "high",
85
+ ),
86
+ "deepseek-ai/deepseek-v4-flash": ModelRecord(
87
+ "deepseek-ai/deepseek-v4-flash", "nvidia",
88
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 1000000,
89
+ ("repository_search", "multi_file_edit", "debugging", "tool_heavy_execution"),
90
+ "frontier", "medium",
91
+ ),
92
+ "Qwen/Qwen3.6-35B-A3B": ModelRecord(
93
+ "Qwen/Qwen3.6-35B-A3B", "hf",
94
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 262144,
95
+ ("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
96
+ "frontier", "medium",
97
+ ),
98
+ "Qwen/Qwen3.6-27B": ModelRecord(
99
+ "Qwen/Qwen3.6-27B", "hf",
100
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 262144,
101
+ ("repository_search", "multi_file_edit", "debugging", "planning"),
102
+ "frontier", "medium",
103
+ ),
104
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct": ModelRecord(
105
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct", "hf",
106
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 262144,
107
+ ("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
108
+ "frontier", "high",
109
+ ),
110
+ "deepseek-ai/DeepSeek-V4-Pro": ModelRecord(
111
+ "deepseek-ai/DeepSeek-V4-Pro", "hf",
112
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 1048576,
113
+ ("repository_audit", "multi_file_edit", "debugging", "planning", "tool_heavy_execution"),
114
+ "frontier", "high",
115
+ ),
116
+ "deepseek-ai/DeepSeek-V4-Flash": ModelRecord(
117
+ "deepseek-ai/DeepSeek-V4-Flash", "hf",
118
+ ModelCapabilities(parallel_tool_calls=True, long_context=True), 262144,
119
+ ("repository_search", "multi_file_edit", "debugging", "tool_heavy_execution"),
120
+ "frontier", "medium",
84
121
  ),
85
122
  }
86
123
 
@@ -25,6 +25,7 @@ class OrchestrationRun:
25
25
  validation: ValidationReport | None = None
26
26
  route: dict[str, Any] = field(default_factory=dict)
27
27
  repair_attempts: int = 0
28
+ reasoning_plan: bool = False
28
29
 
29
30
 
30
31
  class AgentOrchestrator:
@@ -165,6 +166,22 @@ class AgentOrchestrator:
165
166
  tool_records=[item.to_dict() for item in self.run.tool_records],
166
167
  any_mutation=any_mutation, final_text=final_text,
167
168
  )
169
+ if (
170
+ self.run.reasoning_plan
171
+ and self.run.profile.task_type.value == "audit"
172
+ and self.run.plan is not None
173
+ ):
174
+ pending = [
175
+ step.name for step in self.run.plan.steps
176
+ if step.status in {"pending", "in_progress"}
177
+ ]
178
+ if pending:
179
+ report.passed = False
180
+ report.unresolved.append(
181
+ "Execution plan incomplete; pending steps: " + "; ".join(pending)
182
+ )
183
+ if report.severity == "pass":
184
+ report.severity = "warning"
168
185
  self.run.validation = report
169
186
  state = local_state.get_session_state(self.session_id)
170
187
  local_state.save_session_state(