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.
- {tamfis_code-0.6.8/tamfis_code.egg-info → tamfis_code-0.6.14}/PKG-INFO +1 -1
- tamfis_code-0.6.14/USAGE_INSTALL_RELEASE.md +85 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/pyproject.toml +1 -1
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/__init__.py +1 -1
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/agents.py +2 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/cli.py +4 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/config.py +23 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/enforcer.py +2 -0
- tamfis_code-0.6.14/tamfis_code/github_commands.py +65 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/interactive.py +92 -28
- tamfis_code-0.6.14/tamfis_code/live_input.py +264 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/mcp.py +14 -3
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/model_registry.py +41 -4
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/engine.py +35 -13
- tamfis_code-0.6.14/tamfis_code/orchestrator/planner.py +871 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/project.py +30 -30
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/providers.py +68 -13
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/render.py +131 -21
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/runner_local.py +778 -65
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/workspace.py +8 -1
- {tamfis_code-0.6.8 → tamfis_code-0.6.14/tamfis_code.egg-info}/PKG-INFO +1 -1
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/SOURCES.txt +4 -3
- tamfis_code-0.6.14/tests/test_github_command_surface.py +18 -0
- tamfis_code-0.6.14/tests/test_intelligent_planner.py +304 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_live_input.py +116 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_orchestrator.py +7 -2
- tamfis_code-0.6.14/tests/test_parity_contracts.py +152 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_routing.py +51 -13
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_runner_local.py +83 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_config.py +22 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_render.py +3 -1
- tamfis_code-0.6.14/tests/test_workspace_scope.py +497 -0
- tamfis_code-0.6.8/USAGE_INSTALL_RELEASE.md +0 -67
- tamfis_code-0.6.8/tamfis_code/live_input.py +0 -220
- tamfis_code-0.6.8/tamfis_code/orchestrator/planner.py +0 -195
- tamfis_code-0.6.8/tamfis_code/sessions.py +0 -253
- tamfis_code-0.6.8/tests/test_sessions.py +0 -140
- tamfis_code-0.6.8/tests/test_workspace_discovery_gaps.py +0 -119
- tamfis_code-0.6.8/tests/test_workspace_scope.py +0 -201
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/MANIFEST.in +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/README.md +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/setup.cfg +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/__main__.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/agent_definitions.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/api_client.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/clipboard.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/completion.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/custom_commands.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/doctor.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/evidence.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/hooks.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/indexer.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/instructions.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/local_chat.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/local_tools.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/metrics.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/__init__.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/agent.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/agent_server.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/automation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/compat.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/conversation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/delegation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/events.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/leases.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/security.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/skills.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/tools.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/openhands/workspace.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/__init__.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/context.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/protocols.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/orchestrator/validator.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/provider_protocols.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/pty.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/references.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/routing.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/runner.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/safety.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/screenshot.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/state.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/swarm.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/tasks.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code/tool_policy.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/dependency_links.txt +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/entry_points.txt +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/requires.txt +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tamfis_code.egg-info/top_level.txt +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agent_definitions.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agents.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_agents_delegation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_build_installation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_cli_commands.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_clipboard.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_completion.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_context_rollover.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_custom_commands.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_cwd_validation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_doctor_session_diagnostics.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_enforcer.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_fake_tool_detection.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_find_references.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_hooks.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_indexer.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_instructions.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_integration_new.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_interactive_standalone.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_local_pty.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_search_bounds.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_standalone_degradation.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_mcp_stdio_server.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_metrics.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_openhands_runtime.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_openrouter_cost_routing.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_plan_mode_gate.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_plan_steps_and_attempts.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_provider_protocols.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_reasoning_effort_capability.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_reasoning_plan.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_references.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_render_task_diagnostics.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_runner_helpers.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_safety.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_safety_manifest.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_swarm.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_api_client.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_approval_flow.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_intent.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_repl_exit.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_tasks.py +0 -0
- {tamfis_code-0.6.8 → tamfis_code-0.6.14}/tests/test_tamfis_code_workspace.py +0 -0
- {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.
|
|
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.
|
|
@@ -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.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
"
|
|
376
|
-
"
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
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}")
|