tamfis-code 0.6.8__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 (135) hide show
  1. {tamfis_code-0.6.8/tamfis_code.egg-info → tamfis_code-0.6.14}/PKG-INFO +1 -1
  2. tamfis_code-0.6.14/USAGE_INSTALL_RELEASE.md +85 -0
  3. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/pyproject.toml +1 -1
  4. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/__init__.py +1 -1
  5. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/agents.py +2 -0
  6. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/cli.py +4 -0
  7. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/config.py +23 -0
  8. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/enforcer.py +2 -0
  9. tamfis_code-0.6.14/tamfis_code/github_commands.py +65 -0
  10. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/interactive.py +92 -28
  11. tamfis_code-0.6.14/tamfis_code/live_input.py +264 -0
  12. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/mcp.py +14 -3
  13. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/model_registry.py +41 -4
  14. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/engine.py +35 -13
  15. tamfis_code-0.6.14/tamfis_code/orchestrator/planner.py +871 -0
  16. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/project.py +30 -30
  17. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/providers.py +68 -13
  18. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/render.py +131 -21
  19. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/runner_local.py +778 -65
  20. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/workspace.py +8 -1
  21. {tamfis_code-0.6.8 → tamfis_code-0.6.14/tamfis_code.egg-info}/PKG-INFO +1 -1
  22. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/SOURCES.txt +4 -3
  23. tamfis_code-0.6.14/tests/test_github_command_surface.py +18 -0
  24. tamfis_code-0.6.14/tests/test_intelligent_planner.py +304 -0
  25. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_live_input.py +116 -0
  26. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_orchestrator.py +7 -2
  27. tamfis_code-0.6.14/tests/test_parity_contracts.py +152 -0
  28. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_routing.py +51 -13
  29. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_runner_local.py +83 -0
  30. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_config.py +22 -0
  31. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_render.py +3 -1
  32. tamfis_code-0.6.14/tests/test_workspace_scope.py +497 -0
  33. tamfis_code-0.6.8/USAGE_INSTALL_RELEASE.md +0 -67
  34. tamfis_code-0.6.8/tamfis_code/live_input.py +0 -220
  35. tamfis_code-0.6.8/tamfis_code/orchestrator/planner.py +0 -195
  36. tamfis_code-0.6.8/tamfis_code/sessions.py +0 -253
  37. tamfis_code-0.6.8/tests/test_sessions.py +0 -140
  38. tamfis_code-0.6.8/tests/test_workspace_discovery_gaps.py +0 -119
  39. tamfis_code-0.6.8/tests/test_workspace_scope.py +0 -201
  40. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/MANIFEST.in +0 -0
  41. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/README.md +0 -0
  42. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/setup.cfg +0 -0
  43. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/__main__.py +0 -0
  44. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/agent_definitions.py +0 -0
  45. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/api_client.py +0 -0
  46. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/clipboard.py +0 -0
  47. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/completion.py +0 -0
  48. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/custom_commands.py +0 -0
  49. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/doctor.py +0 -0
  50. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/evidence.py +0 -0
  51. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/hooks.py +0 -0
  52. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/indexer.py +0 -0
  53. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/instructions.py +0 -0
  54. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/local_chat.py +0 -0
  55. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/local_tools.py +0 -0
  56. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/mcp_stdio_server.py +0 -0
  57. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/metrics.py +0 -0
  58. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/__init__.py +0 -0
  59. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/agent.py +0 -0
  60. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/agent_server.py +0 -0
  61. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/automation.py +0 -0
  62. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/compat.py +0 -0
  63. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/conversation.py +0 -0
  64. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/delegation.py +0 -0
  65. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/events.py +0 -0
  66. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/leases.py +0 -0
  67. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/security.py +0 -0
  68. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/skills.py +0 -0
  69. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/tools.py +0 -0
  70. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/workspace.py +0 -0
  71. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/__init__.py +0 -0
  72. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/context.py +0 -0
  73. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/protocols.py +0 -0
  74. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/validator.py +0 -0
  75. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/provider_protocols.py +0 -0
  76. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/pty.py +0 -0
  77. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/references.py +0 -0
  78. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/routing.py +0 -0
  79. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/runner.py +0 -0
  80. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/safety.py +0 -0
  81. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/screenshot.py +0 -0
  82. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/state.py +0 -0
  83. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/swarm.py +0 -0
  84. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/tasks.py +0 -0
  85. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/tool_policy.py +0 -0
  86. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/dependency_links.txt +0 -0
  87. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/entry_points.txt +0 -0
  88. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/requires.txt +0 -0
  89. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/top_level.txt +0 -0
  90. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agent_definitions.py +0 -0
  91. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agents.py +0 -0
  92. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agents_delegation.py +0 -0
  93. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_build_installation.py +0 -0
  94. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_cli_commands.py +0 -0
  95. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_clipboard.py +0 -0
  96. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_completion.py +0 -0
  97. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_context_rollover.py +0 -0
  98. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_custom_commands.py +0 -0
  99. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_cwd_validation.py +0 -0
  100. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_doctor_session_diagnostics.py +0 -0
  101. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_enforcer.py +0 -0
  102. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_fake_tool_detection.py +0 -0
  103. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_find_references.py +0 -0
  104. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_hooks.py +0 -0
  105. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_indexer.py +0 -0
  106. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_instructions.py +0 -0
  107. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_integration_new.py +0 -0
  108. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_interactive_standalone.py +0 -0
  109. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_local_pty.py +0 -0
  110. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp.py +0 -0
  111. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_search_bounds.py +0 -0
  112. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_standalone_degradation.py +0 -0
  113. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_stdio_server.py +0 -0
  114. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_metrics.py +0 -0
  115. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_openhands_runtime.py +0 -0
  116. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_openrouter_cost_routing.py +0 -0
  117. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_plan_mode_gate.py +0 -0
  118. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_plan_steps_and_attempts.py +0 -0
  119. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_provider_protocols.py +0 -0
  120. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_reasoning_effort_capability.py +0 -0
  121. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_reasoning_plan.py +0 -0
  122. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_references.py +0 -0
  123. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_render_task_diagnostics.py +0 -0
  124. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_runner_helpers.py +0 -0
  125. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_safety.py +0 -0
  126. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_safety_manifest.py +0 -0
  127. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_swarm.py +0 -0
  128. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_api_client.py +0 -0
  129. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval.py +0 -0
  130. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval_flow.py +0 -0
  131. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_intent.py +0 -0
  132. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_repl_exit.py +0 -0
  133. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_tasks.py +0 -0
  134. {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_workspace.py +0 -0
  135. {tamfis_code-0.6.8 → 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.8
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
@@ -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.8"
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.8"
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):
@@ -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
 
@@ -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):
@@ -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)
@@ -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
@@ -111,7 +160,7 @@ $ <command> explicit shell command
111
160
  /mode <name> switch mode: manual | accept-edits | auto | plan
112
161
  Shift+Tab cycle mode without typing a command (shown in the prompt as [mode]);
113
162
  also works while a task is already running, not just at this prompt
114
- Ctrl+Y while a task is running: open the "queue next" input editor (configurable)
163
+ message> while a task is running: type a message and press Enter
115
164
  /model show the active model route
116
165
  /model list [route] list coding models (route: hf or openrouter)
117
166
  /model auto restore Hugging Face -> OpenRouter automatic routing
@@ -372,10 +421,8 @@ async def run_interactive(
372
421
  )
373
422
  console.print(
374
423
  "[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+Y 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
- "The queue shortcut defaults to Ctrl+Y (Ctrl+T is reserved by Termius); Ctrl+D or Ctrl+C exits.[/dim]\n"
424
+ "While a task runs, a normal message prompt remains available: type and press Enter; "
425
+ "your text is queued without a special shortcut. Shift+Tab cycles mode. Ctrl+D or Ctrl+C exits.[/dim]\n"
379
426
  )
380
427
 
381
428
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -471,19 +518,19 @@ async def run_interactive(
471
518
  model_state = local_state.get_session_state(workspace.session_id)
472
519
  plan_objective = local_state.plan_execution_objective(plan)
473
520
  if standalone:
474
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
475
- live_input.start()
476
- try:
477
- 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(
478
526
  provider_manager, provider_type, model,
479
527
  [*conversation_history, {"role": "user", "content": plan_objective}],
480
528
  console, renderer,
481
529
  workspace_root=workspace.workspace_root, session_id=workspace.session_id,
482
530
  approval_policy=config.approval_policy, interactive=True, cli_config=config,
483
531
  allow_swarm_tool=True,
484
- )
485
- finally:
486
- live_input.stop()
532
+ ),
533
+ )
487
534
  _append_turn_to_history(
488
535
  conversation_history, objective=plan_objective,
489
536
  answer=outcome.summary if outcome.status == "completed" else None,
@@ -990,7 +1037,7 @@ async def run_interactive(
990
1037
  "/mode auto auto-approves everything, never prompts (server safeguards still apply)\n"
991
1038
  "/mode plan read-only: propose without executing\n"
992
1039
  "Shift+Tab cycle manual -> accept-edits -> auto -> plan without typing a command\n"
993
- " (also works mid-task, not just here); Ctrl+Y mid-task queues a follow-up\n"
1040
+ " (also works mid-task, not just here); type in the message prompt to queue work\n"
994
1041
  "Other raw policy values also work directly (--approval-only, no short alias): safe, workspace,\n"
995
1042
  "read-only, plan-only, suggest, full-auto, and 'never' -- note 'never' means DENY everything\n"
996
1043
  "(the opposite of what it sounds like; it is not a synonym for 'auto').[/dim]"
@@ -1296,10 +1343,11 @@ async def run_interactive(
1296
1343
  print_error(console, "Existing uncommitted changes detected; retry is blocked to preserve user edits.")
1297
1344
  continue
1298
1345
  renderer = StreamRenderer(console, mode_label=mode_label_for_policy(config.approval_policy))
1299
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
1300
- live_input.start()
1301
- try:
1302
- 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(
1303
1351
  provider_manager, provider_type, model,
1304
1352
  [*conversation_history, {"role": "user", "content": objective}],
1305
1353
  console, renderer,
@@ -1307,9 +1355,8 @@ async def run_interactive(
1307
1355
  approval_policy=config.approval_policy, interactive=True,
1308
1356
  read_only=mode in {"chat", "audit", "plan"}, cli_config=config,
1309
1357
  allow_swarm_tool=True,
1310
- )
1311
- finally:
1312
- live_input.stop()
1358
+ ),
1359
+ )
1313
1360
  renderer.finish()
1314
1361
  _append_turn_to_history(
1315
1362
  conversation_history, objective=objective,
@@ -1319,6 +1366,10 @@ async def run_interactive(
1319
1366
  last_response_text = outcome.summary
1320
1367
  if outcome.status == "completed" and outcome.summary and not renderer.streamed_final_text:
1321
1368
  console.print(Markdown(outcome.summary))
1369
+ if outcome.status == "exited":
1370
+ if local_pty is not None:
1371
+ local_pty.close()
1372
+ break
1322
1373
  if outcome.status != "completed":
1323
1374
  print_error(console, outcome.error or f"task {outcome.status}")
1324
1375
  console.print()
@@ -1343,6 +1394,10 @@ async def run_interactive(
1343
1394
  last_response_text = outcome.summary
1344
1395
  if outcome.status == "completed" and outcome.summary and not renderer.streamed_final_text:
1345
1396
  console.print(Markdown(outcome.summary))
1397
+ if outcome.status == "exited":
1398
+ if local_pty is not None:
1399
+ local_pty.close()
1400
+ break
1346
1401
  if outcome.status != "completed":
1347
1402
  print_error(console, outcome.error or f"task {outcome.status}")
1348
1403
  except (AuthRequiredError, RemoteAPIError) as e:
@@ -1363,6 +1418,7 @@ async def run_interactive(
1363
1418
  or reply_state.turn_checkpoint or reply_state.conversation_history
1364
1419
  ),
1365
1420
  )
1421
+ submitted_text = text
1366
1422
  intent = parse_intent(text, custom_commands=custom_commands)
1367
1423
  try:
1368
1424
  if intent.kind == "shell":
@@ -1392,11 +1448,16 @@ async def run_interactive(
1392
1448
  continue
1393
1449
  renderer = StreamRenderer(console, mode_label=mode_label_for_policy(config.approval_policy))
1394
1450
  model_state = local_state.get_session_state(workspace.session_id)
1451
+ renderer.handle_event({
1452
+ "event_type": "user_message",
1453
+ "payload": {"content": submitted_text},
1454
+ })
1395
1455
  if standalone:
1396
- live_input = LiveInputListener(session_id=workspace.session_id, renderer=renderer, cli_config=config)
1397
- live_input.start()
1398
- try:
1399
- 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(
1400
1461
  provider_manager, provider_type, model,
1401
1462
  [*conversation_history, {"role": "user", "content": intent.objective}],
1402
1463
  console, renderer,
@@ -1404,15 +1465,18 @@ async def run_interactive(
1404
1465
  approval_policy=config.approval_policy, interactive=True,
1405
1466
  read_only=intent.mode in {"chat", "audit", "plan"}, cli_config=config,
1406
1467
  allow_swarm_tool=True,
1407
- )
1408
- finally:
1409
- live_input.stop()
1468
+ ),
1469
+ )
1410
1470
  renderer.finish()
1411
1471
  _append_turn_to_history(
1412
1472
  conversation_history, objective=intent.objective,
1413
1473
  answer=outcome.summary if outcome.status == "completed" else None,
1414
1474
  )
1415
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
1416
1480
  if intent.mode == "plan" and outcome.status == "completed" and outcome.summary:
1417
1481
  saved = local_state.save_plan(workspace.session_id, objective=intent.objective, content=outcome.summary)
1418
1482
  console.print(f"[green]Plan saved[/green] · {saved.id}")