tamfis-code 0.7.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/PKG-INFO +1 -1
  2. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/pyproject.toml +2 -2
  3. tamfis_code-1.3.0/setup.py +2 -0
  4. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/__init__.py +1 -1
  5. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/cli.py +156 -7
  6. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/doctor.py +15 -0
  7. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/interactive.py +34 -1
  8. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/live_input.py +56 -39
  9. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/local_chat.py +33 -2
  10. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/mcp.py +40 -7
  11. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/agent.py +2 -2
  12. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/__init__.py +2 -0
  13. tamfis_code-1.3.0/tamfis_code/orchestrator/approvals.py +64 -0
  14. tamfis_code-1.3.0/tamfis_code/orchestrator/completion.py +48 -0
  15. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/engine.py +15 -0
  16. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/planner.py +41 -0
  17. tamfis_code-1.3.0/tamfis_code/orchestrator/repair.py +68 -0
  18. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/providers.py +219 -19
  19. tamfis_code-1.3.0/tamfis_code/release_verification.py +276 -0
  20. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/render.py +1 -1
  21. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runner.py +26 -1
  22. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runner_local.py +127 -6
  23. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/__init__.py +6 -0
  24. tamfis_code-1.3.0/tamfis_code/runtime/checkpoint.py +79 -0
  25. tamfis_code-1.3.0/tamfis_code/runtime/cognitive.py +147 -0
  26. tamfis_code-1.3.0/tamfis_code/runtime/journal.py +119 -0
  27. tamfis_code-1.3.0/tamfis_code/runtime/memory.py +210 -0
  28. tamfis_code-1.3.0/tamfis_code/runtime/repository_index.py +82 -0
  29. tamfis_code-1.3.0/tamfis_code/runtime/reviewer.py +24 -0
  30. tamfis_code-1.3.0/tamfis_code/runtime/steering.py +31 -0
  31. tamfis_code-1.3.0/tamfis_code/runtime/unified.py +301 -0
  32. tamfis_code-1.3.0/tamfis_code/runtime/workspace_authority.py +132 -0
  33. tamfis_code-1.3.0/tamfis_code/runtime/worktree.py +109 -0
  34. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/PKG-INFO +1 -1
  35. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/SOURCES.txt +25 -0
  36. tamfis_code-1.3.0/tests/test_phase1_reliability.py +94 -0
  37. tamfis_code-1.3.0/tests/test_phase2_unified_runtime.py +80 -0
  38. tamfis_code-1.3.0/tests/test_phase3_claude_behaviour.py +110 -0
  39. tamfis_code-1.3.0/tests/test_phase4_release_verification.py +26 -0
  40. tamfis_code-1.3.0/tests/test_phase5_cognitive_orchestration.py +92 -0
  41. tamfis_code-1.3.0/tests/test_phase6_workspace_authority.py +65 -0
  42. tamfis_code-1.3.0/tests/test_phase7_memory.py +65 -0
  43. tamfis_code-1.3.0/tests/test_phase8_worktree.py +66 -0
  44. tamfis_code-1.3.0/tests/test_phase9_decision_logic.py +49 -0
  45. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/README.md +0 -0
  46. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/setup.cfg +0 -0
  47. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/__main__.py +0 -0
  48. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/agent_definitions.py +0 -0
  49. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/agents.py +0 -0
  50. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/api_client.py +0 -0
  51. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/clipboard.py +0 -0
  52. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/completion.py +0 -0
  53. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/config.py +0 -0
  54. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/custom_commands.py +0 -0
  55. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/enforcer.py +0 -0
  56. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/evidence.py +0 -0
  57. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/github_commands.py +0 -0
  58. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/hooks.py +0 -0
  59. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/indexer.py +0 -0
  60. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/instructions.py +0 -0
  61. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/local_tools.py +0 -0
  62. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/mcp_stdio_server.py +0 -0
  63. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/metrics.py +0 -0
  64. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/model_registry.py +0 -0
  65. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/__init__.py +0 -0
  66. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/agent_server.py +0 -0
  67. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/automation.py +0 -0
  68. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/compat.py +0 -0
  69. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/conversation.py +0 -0
  70. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/delegation.py +0 -0
  71. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/events.py +0 -0
  72. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/leases.py +0 -0
  73. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/security.py +0 -0
  74. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/skills.py +0 -0
  75. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/tools.py +0 -0
  76. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/openhands/workspace.py +0 -0
  77. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/context.py +0 -0
  78. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/protocols.py +0 -0
  79. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/orchestrator/validator.py +0 -0
  80. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/project.py +0 -0
  81. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/provider_protocols.py +0 -0
  82. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/pty.py +0 -0
  83. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/references.py +0 -0
  84. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/routing.py +0 -0
  85. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/budgets.py +0 -0
  86. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/controller.py +0 -0
  87. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/evidence.py +0 -0
  88. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/runtime/state.py +0 -0
  89. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/safety.py +0 -0
  90. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/screenshot.py +0 -0
  91. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/state.py +0 -0
  92. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/swarm.py +0 -0
  93. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/tasks.py +0 -0
  94. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/tool_policy.py +0 -0
  95. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code/workspace.py +0 -0
  96. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/dependency_links.txt +0 -0
  97. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/entry_points.txt +0 -0
  98. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/requires.txt +0 -0
  99. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tamfis_code.egg-info/top_level.txt +0 -0
  100. {tamfis_code-0.7.0 → tamfis_code-1.3.0}/tests/test_runtime_controller.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamfis-code
3
- Version: 0.7.0
3
+ Version: 1.3.0
4
4
  Summary: TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals
5
5
  Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
6
6
  Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tamfis-code"
3
- version = "0.7.0"
3
+ version = "1.3.0"
4
4
  description = "TamfisGPT Code -- portable subscription-backed terminal coding agent with local tools, PTY, plans, queues, and approvals"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "rich>=13.0",
11
11
  "prompt_toolkit>=3.0",
12
12
  # Provider calls (providers.py) go through the openai SDK's client against
13
- # each vendor's OpenAI-compatible endpoint (HF, NVIDIA NIM, OpenRouter)
13
+ # each provider's OpenAI-compatible endpoint (Ollama Cloud, NVIDIA NIM, HF, OpenRouter)
14
14
  # -- this was imported but never declared, only working because a
15
15
  # copy happened to already be installed system-wide.
16
16
  "openai>=1.30",
@@ -0,0 +1,2 @@
1
+ from setuptools import setup
2
+ setup()
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from importlib import import_module
10
10
  from typing import Any
11
11
 
12
- __version__ = "0.7.0"
12
+ __version__ = "1.3.0"
13
13
  MIN_COMPATIBLE_API_VERSION = "remote-ai-v2"
14
14
 
15
15
  _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
@@ -36,6 +36,8 @@ from .api_client import (
36
36
  )
37
37
  from .config import APPROVAL_MODES, CONFIG_DIR, Config, Credentials, load_config
38
38
  from .doctor import run_doctor
39
+ from .runtime.memory import MemoryRecord, MemoryType, get_memory_store
40
+ from .runtime.worktree import WorktreeError, create_worktree, list_worktrees, remove_worktree
39
41
  from .render import StreamRenderer, print_banner, print_error, print_recent_thread, print_resume_plan_status, print_unified_diff
40
42
  from .runner import (
41
43
  ACTIVE_TASK_STATUSES,
@@ -53,7 +55,7 @@ from .local_chat import _PROVIDER_ALIASES
53
55
  # validation and then failed with "Unknown local provider" inside
54
56
  # resolve_provider_type every time.
55
57
  _PROVIDER_CHOICES = sorted(_PROVIDER_ALIASES.keys())
56
- _PROVIDER_HELP = "tamfis/tamfisgpt (subscription API), hf, nvidia, openrouter, or auto (default)."
58
+ _PROVIDER_HELP = "tamfis/tamfisgpt (subscription API), ollama_cloud, nvidia, hf, openrouter, or auto (default)."
57
59
 
58
60
  EXIT_OK = 0
59
61
  EXIT_TASK_FAILED = 1
@@ -100,7 +102,7 @@ def async_command(fn):
100
102
  @click.option("--approval", "approval_policy", type=click.Choice(APPROVAL_MODES), default=None, help="Override the configured approval policy for this invocation. Note: 'never' means deny everything outright -- the opposite of 'auto'/'full-auto' (which mean never PROMPT, i.e. auto-approve). It is not a synonym for auto-approve.")
101
103
  @click.option("--api-base", "api_base", default=None, help="Override the configured Remote API base URL.")
102
104
  @click.option("--cwd", "cwd_override", type=click.Path(exists=True, file_okay=False), default=None, help="Treat this directory as the workspace instead of the current directory.")
103
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default) -- which provider the bare (no-subcommand) interactive REPL calls directly.")
105
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default) -- which provider the bare (no-subcommand) interactive REPL calls directly.")
104
106
  @click.option("--model", default=None, help="Provider-specific model id for the bare interactive REPL; defaults to that provider's default model.")
105
107
  @click.option("--remote", is_flag=True, default=False, help="Use the legacy TamfisGPT Remote Workspace backend for the bare interactive REPL instead of calling a provider directly.")
106
108
  @click.version_option(__version__, prog_name="tamfis-code")
@@ -343,6 +345,125 @@ def workspace_remove(ctx: click.Context, path: Path):
343
345
  console.print(f"[green]Workspace removed:[/green] {target}")
344
346
 
345
347
 
348
+ @cli.group(name="memory")
349
+ def memory_group():
350
+ """Manage durable, cross-session memory records."""
351
+
352
+
353
+ @memory_group.command(name="list")
354
+ @click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), default=None)
355
+ def memory_list(memory_type: Optional[str]):
356
+ console = Console()
357
+ store = get_memory_store()
358
+ records = store.list(MemoryType(memory_type) if memory_type else None)
359
+ if not records:
360
+ console.print("[dim]No memory records.[/dim]")
361
+ return
362
+ table = Table(show_header=True, header_style="bold")
363
+ table.add_column("name")
364
+ table.add_column("type")
365
+ table.add_column("description")
366
+ for record in records:
367
+ table.add_row(record.name, record.type.value, record.description)
368
+ console.print(table)
369
+
370
+
371
+ @memory_group.command(name="show")
372
+ @click.argument("name")
373
+ def memory_show(name: str):
374
+ console = Console()
375
+ record = get_memory_store().load(name)
376
+ if record is None:
377
+ print_error(console, f"No memory record named {name!r}.")
378
+ raise SystemExit(EXIT_TASK_FAILED)
379
+ console.print(f"[bold]{record.name}[/bold] ({record.type.value})")
380
+ console.print(record.description)
381
+ console.print()
382
+ console.print(record.content)
383
+
384
+
385
+ @memory_group.command(name="add")
386
+ @click.argument("name")
387
+ @click.option("--type", "memory_type", type=click.Choice([t.value for t in MemoryType]), required=True)
388
+ @click.option("--description", required=True)
389
+ @click.option("--content", required=True)
390
+ def memory_add(name: str, memory_type: str, description: str, content: str):
391
+ console = Console()
392
+ record = get_memory_store().save(
393
+ MemoryRecord(name=name, type=MemoryType(memory_type), description=description, content=content)
394
+ )
395
+ console.print(f"[green]Saved memory record:[/green] {record.name}")
396
+
397
+
398
+ @memory_group.command(name="forget")
399
+ @click.argument("name")
400
+ def memory_forget(name: str):
401
+ console = Console()
402
+ if get_memory_store().delete(name):
403
+ console.print(f"[green]Forgot memory record:[/green] {name}")
404
+ else:
405
+ print_error(console, f"No memory record named {name!r}.")
406
+ raise SystemExit(EXIT_TASK_FAILED)
407
+
408
+
409
+ @cli.group(name="worktree")
410
+ def worktree_group():
411
+ """Manage isolated git worktrees for background/parallel execution."""
412
+
413
+
414
+ @worktree_group.command(name="list")
415
+ @click.pass_context
416
+ def worktree_list_cmd(ctx: click.Context):
417
+ console = Console(no_color=not ctx.obj["config"].colour)
418
+ try:
419
+ handles = list_worktrees(ctx.obj["workspace_root"])
420
+ except WorktreeError as exc:
421
+ print_error(console, str(exc))
422
+ raise SystemExit(EXIT_TASK_FAILED)
423
+ if not handles:
424
+ console.print("[dim]No worktrees.[/dim]")
425
+ return
426
+ table = Table(show_header=True, header_style="bold")
427
+ table.add_column("branch")
428
+ table.add_column("path")
429
+ for handle in handles:
430
+ table.add_row(handle.branch, str(handle.path))
431
+ console.print(table)
432
+
433
+
434
+ @worktree_group.command(name="create")
435
+ @click.argument("branch")
436
+ @click.option("--base", default="HEAD", help="Ref the new worktree branch is created from.")
437
+ @click.pass_context
438
+ def worktree_create_cmd(ctx: click.Context, branch: str, base: str):
439
+ console = Console(no_color=not ctx.obj["config"].colour)
440
+ try:
441
+ handle = create_worktree(ctx.obj["workspace_root"], branch=branch, base=base)
442
+ except WorktreeError as exc:
443
+ print_error(console, str(exc))
444
+ raise SystemExit(EXIT_TASK_FAILED)
445
+ console.print(f"[green]Worktree created:[/green] {handle.path} (branch {handle.branch})")
446
+
447
+
448
+ @worktree_group.command(name="remove")
449
+ @click.argument("branch")
450
+ @click.option("--force", is_flag=True, default=False, help="Remove even if the worktree has uncommitted changes.")
451
+ @click.pass_context
452
+ def worktree_remove_cmd(ctx: click.Context, branch: str, force: bool):
453
+ console = Console(no_color=not ctx.obj["config"].colour)
454
+ try:
455
+ handles = {h.branch: h for h in list_worktrees(ctx.obj["workspace_root"])}
456
+ handle = handles.get(branch)
457
+ if handle is None:
458
+ print_error(console, f"No worktree found for branch {branch!r}.")
459
+ raise SystemExit(EXIT_TASK_FAILED)
460
+ remove_worktree(handle, force=force)
461
+ except WorktreeError as exc:
462
+ print_error(console, str(exc))
463
+ raise SystemExit(EXIT_TASK_FAILED)
464
+ console.print(f"[green]Worktree removed:[/green] {branch}")
465
+
466
+
346
467
  @cli.command(name="cwd")
347
468
  @click.argument("path", required=False, type=click.Path(exists=True, file_okay=False, path_type=Path))
348
469
  @click.option("--remote", is_flag=True, default=False, help="Update the working directory on the legacy TamfisGPT Remote Workspace backend instead of the local session.")
@@ -422,7 +543,7 @@ async def init(ctx: click.Context, remote: bool):
422
543
 
423
544
 
424
545
  @cli.command()
425
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
546
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
426
547
  @click.option("--remote", is_flag=True, default=False, help="Check the legacy TamfisGPT Remote Workspace backend instead of local provider connectivity.")
427
548
  @click.pass_context
428
549
  @async_command
@@ -1359,7 +1480,7 @@ async def run(ctx: click.Context, command: str, background: bool, remote: bool):
1359
1480
 
1360
1481
  @cli.command()
1361
1482
  @click.argument("session_id", type=int, required=False, default=None)
1362
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
1483
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1363
1484
  @click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
1364
1485
  @click.option("--remote", is_flag=True, default=False, help="Resume a session on the legacy TamfisGPT Remote Workspace backend instead of a local one.")
1365
1486
  @click.pass_context
@@ -1426,7 +1547,7 @@ async def resume(ctx: click.Context, session_id: Optional[int], provider: str, m
1426
1547
 
1427
1548
  @cli.command()
1428
1549
  @click.argument("task_id", required=False, default=None)
1429
- @click.option("--provider", default="auto", help="hf, nvidia, openrouter, or auto (default).")
1550
+ @click.option("--provider", default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1430
1551
  @click.option("--model", default=None, help="Provider-specific model id; defaults to that provider's default model.")
1431
1552
  @click.option("--remote", is_flag=True, default=False, help="Retry a task on the legacy TamfisGPT Remote Workspace backend instead of resending locally.")
1432
1553
  @click.pass_context
@@ -1948,7 +2069,7 @@ def completion_cmd(shell: str):
1948
2069
  @click.option('--task', '-t', 'tasks', multiple=True, help='Task description (repeatable for delegate)')
1949
2070
  @click.option('--file', '-f', help='File to operate on')
1950
2071
  @click.option('--max-concurrency', default=1, show_default=True, help='Max concurrent delegated sub-tasks')
1951
- @click.option('--provider', default="auto", help="hf, nvidia, openrouter, or auto (default).")
2072
+ @click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
1952
2073
  @click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
1953
2074
  @click.pass_context
1954
2075
  def agent_cmd(ctx: click.Context, action: str, tasks: tuple[str, ...], file: str, max_concurrency: int, provider: str, model: Optional[str]):
@@ -2189,7 +2310,7 @@ def providers_command(ctx: click.Context):
2189
2310
 
2190
2311
  @cli.command('local')
2191
2312
  @click.argument('objective', required=False)
2192
- @click.option('--provider', default="auto", help="hf, nvidia, openrouter, or auto (default).")
2313
+ @click.option('--provider', default="auto", help="ollama_cloud, nvidia, hf, openrouter, or auto (default).")
2193
2314
  @click.option('--model', default=None, help="Provider-specific model id; defaults to that provider's default model.")
2194
2315
  @click.option('--no-tools', 'no_tools', is_flag=True, default=False, help="Disable read-only repo tools (read_file/list_directory/search_code/get_git_info) for this turn.")
2195
2316
  @click.option('--agent', 'full_agent', is_flag=True, default=False, help="Full read/write/execute tool access (write_file/edit_file/execute_command) via the local risk/approval/mutation-ledger layer, instead of read-only Q&A. Standalone -- no TamfisGPT backend involved.")
@@ -2382,6 +2503,34 @@ from .enforcer import TestEnforcer, run_enforcer, add_enforcer_command
2382
2503
  # TestEnforcer -- using those flags raised AttributeError at runtime.
2383
2504
  add_enforcer_command(cli)
2384
2505
 
2506
+ @cli.command("verify-release")
2507
+ @click.option("--artifact", "artifacts", multiple=True, type=click.Path(exists=True, dir_okay=False, path_type=Path), help="Built wheel, source archive, checksum, or manifest to verify.")
2508
+ @click.option("--output-dir", type=click.Path(file_okay=False, path_type=Path), default=None, help="Directory for JSON and Markdown verification reports.")
2509
+ @click.pass_context
2510
+ def verify_release_command(ctx: click.Context, artifacts: tuple[Path, ...], output_dir: Optional[Path]):
2511
+ """Run the Phase 4 release gate without contacting any AI provider."""
2512
+ from .release_verification import run_release_verification
2513
+
2514
+ root: Path = ctx.obj["workspace_root"]
2515
+ report = run_release_verification(root, artifacts)
2516
+ console = Console(no_color=not ctx.obj["config"].colour)
2517
+ table = Table(show_header=True, header_style="bold")
2518
+ table.add_column("Category")
2519
+ table.add_column("Check")
2520
+ table.add_column("Result")
2521
+ table.add_column("Detail")
2522
+ for check in report.checks:
2523
+ table.add_row(check.category, check.name, "PASS" if check.passed else "FAIL", check.detail)
2524
+ console.print(table)
2525
+
2526
+ destination = output_dir or (root / "dist")
2527
+ json_path = report.write_json(destination / f"tamfis_code-{report.version}-verification-report.json")
2528
+ md_path = report.write_markdown(destination / f"tamfis_code-{report.version}-verification-report.md")
2529
+ console.print(f"Verification reports: {json_path} {md_path}")
2530
+ if not report.passed:
2531
+ raise SystemExit(EXIT_TASK_FAILED)
2532
+
2533
+
2385
2534
  # Familiar GitHub CLI command surface, delegated to the installed `gh` binary.
2386
2535
  from .github_commands import register_github_commands
2387
2536
  register_github_commands(cli)
@@ -21,6 +21,7 @@ from .api_client import AuthRequiredError, RemoteAPIClient, RemoteAPIError
21
21
  from .config import Config, load_credentials
22
22
  from .providers import get_provider_status
23
23
  from .workspace import resolve_local_workspace
24
+ from .runtime.journal import JOURNAL_PATH, recent_failures
24
25
 
25
26
 
26
27
  @dataclass
@@ -190,6 +191,20 @@ def _diagnose_local_session(workspace_root: Path) -> list[CheckResult]:
190
191
  summary = ", ".join(f"{count} {name}" for name, count in sorted(by_status.items())) or "no steps"
191
192
  results.append(CheckResult("Active plan step progress", "PASS", summary))
192
193
 
194
+ failures = recent_failures(5)
195
+ if failures:
196
+ latest = failures[0]
197
+ detail = (
198
+ f"{len(failures)} recent failed/cancelled execution(s); latest "
199
+ f"{latest.get('mode', 'unknown')} at {latest.get('timestamp', 'unknown')}: "
200
+ f"{latest.get('error') or latest.get('status') or 'unknown failure'}"
201
+ )
202
+ results.append(CheckResult("Unified runtime recent failures", "WARNING", detail))
203
+ elif JOURNAL_PATH.exists():
204
+ results.append(CheckResult("Unified runtime recent failures", "PASS", "none in the recent execution journal"))
205
+ else:
206
+ results.append(CheckResult("Unified runtime execution journal", "WARNING", "no unified runtime executions recorded yet"))
207
+
193
208
  return results
194
209
 
195
210
 
@@ -107,7 +107,7 @@ async def _run_cancellable_local_turn(
107
107
  error="Task cancelled by user.",
108
108
  )
109
109
  finally:
110
- live_input.stop()
110
+ await live_input.stop()
111
111
 
112
112
 
113
113
  HELP_TEXT = """\
@@ -589,8 +589,41 @@ async def run_interactive(
589
589
  break
590
590
  except EOFError:
591
591
  break
592
+ resolved_paste_count = 0
593
+ resolved_paste_chars = 0
594
+ resolved_paste_lines = 0
595
+
592
596
  for placeholder, real_text in pending_pastes.items():
597
+ if placeholder not in text:
598
+ continue
599
+
593
600
  text = text.replace(placeholder, real_text)
601
+ resolved_paste_count += 1
602
+ resolved_paste_chars += len(real_text)
603
+ resolved_paste_lines += real_text.count("\n") + (
604
+ 0 if real_text.endswith("\n") else 1
605
+ )
606
+
607
+ unresolved_pastes = re.findall(
608
+ r"\[Pasted text #\d+ \+\d+ lines\]",
609
+ text,
610
+ )
611
+ if unresolved_pastes:
612
+ print_error(
613
+ console,
614
+ "Pasted-text resolution failed. The underlying paste was "
615
+ "not available, so no task was submitted. Unresolved: "
616
+ + ", ".join(unresolved_pastes),
617
+ )
618
+ continue
619
+
620
+ if resolved_paste_count:
621
+ console.print(
622
+ "[dim]diagnostics: Resolved "
623
+ f"{resolved_paste_count} pasted-text block(s), "
624
+ f"{resolved_paste_lines:,} line(s), "
625
+ f"{resolved_paste_chars:,} character(s).[/dim]"
626
+ )
594
627
 
595
628
  text = text.strip()
596
629
  if not text:
@@ -61,21 +61,29 @@ class LiveInputListener:
61
61
  self._active = True
62
62
  self._schedule_prompt()
63
63
 
64
- def stop(self) -> None:
64
+ async def stop(self) -> None:
65
+ """Stop input ownership and wait until prompt-toolkit releases stdin."""
65
66
  self._active = False
66
- self._cancel_prompt()
67
+ await self._shutdown_prompt()
67
68
  if self.renderer.live_input_listener is self:
68
69
  self.renderer.live_input_listener = None
69
- self.renderer.resume_live()
70
+ self.renderer.resume_live()
70
71
 
71
72
  def pause(self) -> None:
73
+ """Synchronously request prompt shutdown before another UI reads stdin."""
72
74
  self._paused = True
75
+ self._request_prompt_exit()
73
76
  self._cancel_prompt()
74
77
 
75
78
  def resume(self) -> None:
76
79
  self._paused = False
77
- if self._active:
78
- self._schedule_prompt()
80
+ if not self._active:
81
+ return
82
+ task = self._input_task
83
+ if task is not None and not task.done():
84
+ task.add_done_callback(lambda _task: self._schedule_prompt() if self._active and not self._paused else None)
85
+ return
86
+ self._schedule_prompt()
79
87
 
80
88
  def _dispatch(self) -> None:
81
89
  """Compatibility hook for older embedders; start() no longer uses it."""
@@ -102,11 +110,28 @@ class LiveInputListener:
102
110
  if self._input_task is None or self._input_task.done():
103
111
  self._input_task = asyncio.create_task(self._input_loop())
104
112
 
113
+ def _request_prompt_exit(self) -> None:
114
+ session = self._prompt_session
115
+ app = getattr(session, "app", None)
116
+ if app is not None and not getattr(app, "is_done", False):
117
+ with contextlib.suppress(Exception):
118
+ app.exit(result="")
119
+
105
120
  def _cancel_prompt(self) -> None:
121
+ task = self._input_task
122
+ if task is not None and not task.done():
123
+ task.cancel()
124
+
125
+ async def _shutdown_prompt(self) -> None:
126
+ self._request_prompt_exit()
106
127
  task = self._input_task
107
128
  self._input_task = None
108
129
  if task is not None and not task.done():
109
130
  task.cancel()
131
+ if task is not None:
132
+ with contextlib.suppress(asyncio.CancelledError, Exception):
133
+ await task
134
+ self._prompt_session = None
110
135
 
111
136
  def invalidate(self) -> None:
112
137
  """Refresh the prompt footer after a streamed phase/status update."""
@@ -157,40 +182,32 @@ class LiveInputListener:
157
182
 
158
183
  session = PromptSession(key_bindings=bindings)
159
184
  self._prompt_session = session
160
- while self._active and not self._paused:
161
- try:
162
- # patch_stdout keeps concurrent tool/assistant output above
163
- # the current line and redraws the line beneath it.
164
- with patch_stdout(raw=True):
165
- text = await session.prompt_async(
166
- "message> ", bottom_toolbar=self._bottom_toolbar,
167
- )
168
- except asyncio.CancelledError:
169
- return
170
- except KeyboardInterrupt:
171
- # Ctrl+C is the process/REPL exit affordance. The active
172
- # runner's signal path remains reserved for true process
173
- # interrupts; queue an explicit exit so the local runner
174
- # can finish its current safe boundary and the REPL exits.
175
- self._enqueue_control("exit")
176
- return
177
- except EOFError:
178
- return
179
-
180
- # Escape sets _paused before exiting the prompt, so its empty
181
- # result must not be queued or cause another editor to start.
182
- if self._paused:
183
- break
184
-
185
- # A genuine line already returned by prompt_async remains valid
186
- # even when the listener was deactivated during that prompt.
187
- # Queue it first, then let the loop terminate cleanly.
188
- self._enqueue(text)
189
-
190
- if not self._active:
191
- break
192
-
193
- self._prompt_session = None
185
+ try:
186
+ while self._active and not self._paused:
187
+ try:
188
+ with patch_stdout(raw=True):
189
+ text = await session.prompt_async(
190
+ "message> ", bottom_toolbar=self._bottom_toolbar,
191
+ )
192
+ except asyncio.CancelledError:
193
+ raise
194
+ except KeyboardInterrupt:
195
+ self._request_interrupt("cancel")
196
+ return
197
+ except EOFError:
198
+ return
199
+
200
+ if self._paused:
201
+ break
202
+ self._enqueue(text)
203
+ if not self._active:
204
+ break
205
+ finally:
206
+ if self._prompt_session is session:
207
+ self._prompt_session = None
208
+ current = asyncio.current_task()
209
+ if self._input_task is current:
210
+ self._input_task = None
194
211
 
195
212
  @property
196
213
  def interrupt_classification(self) -> Optional[str]:
@@ -38,7 +38,7 @@ def resolve_provider_type(name: Optional[str]) -> ProviderType:
38
38
  return _PROVIDER_ALIASES[key]
39
39
 
40
40
 
41
- async def run_local_turn(
41
+ async def _run_local_turn_impl(
42
42
  manager: ProviderManager,
43
43
  provider: ProviderType,
44
44
  messages: List[Dict[str, Any]],
@@ -108,7 +108,7 @@ async def run_local_turn(
108
108
  return "(Stopped after exhausting the local tool-call round limit -- try a narrower question.)"
109
109
 
110
110
 
111
- async def stream_local_turn(
111
+ async def _stream_local_turn_impl(
112
112
  manager: ProviderManager,
113
113
  provider: ProviderType,
114
114
  messages: List[Dict[str, Any]],
@@ -119,3 +119,34 @@ async def stream_local_turn(
119
119
  common single-shot/no-tools case still gets real token streaming)."""
120
120
  async for chunk in manager.chat_completion(provider, messages, model=model, stream=True):
121
121
  yield chunk
122
+
123
+
124
+ async def run_local_turn(
125
+ manager: ProviderManager,
126
+ provider: ProviderType,
127
+ messages: List[Dict[str, Any]],
128
+ model: Optional[str],
129
+ console: Console,
130
+ *,
131
+ use_tools: bool = True,
132
+ ) -> str:
133
+ """Compatibility adapter routed through the unified runtime."""
134
+ from .runtime.unified import get_unified_runtime
135
+ return await get_unified_runtime().execute_local_chat(
136
+ manager=manager, provider=provider, messages=messages, model=model,
137
+ console=console, use_tools=use_tools,
138
+ )
139
+
140
+
141
+ async def stream_local_turn(
142
+ manager: ProviderManager,
143
+ provider: ProviderType,
144
+ messages: List[Dict[str, Any]],
145
+ model: Optional[str],
146
+ ) -> AsyncIterator[str]:
147
+ """Compatibility streaming adapter routed through the unified runtime."""
148
+ from .runtime.unified import get_unified_runtime
149
+ async for chunk in get_unified_runtime().stream_local_chat(
150
+ manager=manager, provider=provider, messages=messages, model=model,
151
+ ):
152
+ yield chunk
@@ -9,6 +9,8 @@ import re
9
9
  import subprocess
10
10
  import fnmatch
11
11
  import asyncio
12
+ import os
13
+ import tempfile
12
14
  import sys
13
15
  import shutil
14
16
  import tarfile
@@ -680,13 +682,31 @@ class MCPServer:
680
682
  )
681
683
  return resolved
682
684
 
683
- async def _write_file(self, path: str, content: str) -> str:
685
+ def _atomic_write_text(self, target: Path, content: str) -> None:
686
+ target.parent.mkdir(parents=True, exist_ok=True)
687
+ fd, temp_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=str(target.parent))
688
+ try:
689
+ with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
690
+ handle.write(content)
691
+ handle.flush()
692
+ os.fsync(handle.fileno())
693
+ os.replace(temp_name, target)
694
+ except BaseException:
695
+ with contextlib.suppress(OSError):
696
+ os.unlink(temp_name)
697
+ raise
698
+
699
+ async def _write_file(self, path: str, content: str | None = None, **aliases: Any) -> str:
700
+ content = content if content is not None else aliases.pop("text", None)
701
+ content = content if content is not None else aliases.pop("new_content", None)
702
+ content = content if content is not None else aliases.pop("file_content", None)
703
+ if content is None:
704
+ return "❌ Error: write_file requires content"
684
705
  p = self._resolve_in_workspace(path)
685
706
  original_content = p.read_text(encoding="utf-8", errors="ignore") if p.is_file() else None
686
- p.parent.mkdir(parents=True, exist_ok=True)
687
- p.write_text(content, encoding='utf-8')
688
- if not p.exists():
689
- return f"❌ Failed to write to '{path}'"
707
+ self._atomic_write_text(p, content)
708
+ if p.read_text(encoding="utf-8", errors="strict") != content:
709
+ return f"❌ Failed to verify write to '{path}'"
690
710
  if self.session_id is not None:
691
711
  from .safety import record_mutation
692
712
  record_mutation(
@@ -696,7 +716,18 @@ class MCPServer:
696
716
  )
697
717
  return f"✅ Successfully wrote {len(content)} bytes to '{path}'"
698
718
 
699
- async def _edit_file(self, path: str, old_string: str, new_string: str) -> str:
719
+ async def _edit_file(
720
+ self, path: str, old_string: str | None = None, new_string: str | None = None, **aliases: Any
721
+ ) -> str:
722
+ old_string = old_string if old_string is not None else aliases.pop("old_text", None)
723
+ new_string = new_string if new_string is not None else aliases.pop("new_text", None)
724
+ new_string = new_string if new_string is not None else aliases.pop("replacement", None)
725
+ full_content = aliases.pop("content", None)
726
+ full_content = full_content if full_content is not None else aliases.pop("new_content", None)
727
+ if full_content is not None and old_string is None:
728
+ return await self._write_file(path, content=full_content)
729
+ if old_string is None or new_string is None:
730
+ return "❌ Error: edit_file requires old_string and new_string, or content for full replacement"
700
731
  p = self._resolve_in_workspace(path)
701
732
  if not p.is_file():
702
733
  return f"❌ Error: File '{path}' not found"
@@ -710,7 +741,9 @@ class MCPServer:
710
741
  "Include more surrounding context to disambiguate."
711
742
  )
712
743
  new_content = original_content.replace(old_string, new_string, 1)
713
- p.write_text(new_content, encoding="utf-8")
744
+ self._atomic_write_text(p, new_content)
745
+ if p.read_text(encoding="utf-8", errors="strict") != new_content:
746
+ return f"❌ Failed to verify edit to '{path}'"
714
747
  if self.session_id is not None:
715
748
  from .safety import record_mutation
716
749
  record_mutation(
@@ -61,7 +61,7 @@ class TamfisAgent:
61
61
  ) -> AgentRunResult:
62
62
  from rich.console import Console
63
63
  from tamfis_code.providers import ProviderManager, ProviderType
64
- from tamfis_code.runner_local import run_local_agent_turn
64
+ from tamfis_code.runtime.unified import get_unified_runtime
65
65
 
66
66
  if self.conversation.state == ConversationState.RUNNING:
67
67
  raise RuntimeError("conversation is already running")
@@ -74,7 +74,7 @@ class TamfisAgent:
74
74
  except ValueError as exc:
75
75
  raise ValueError(f"unsupported provider: {provider}") from exc
76
76
  try:
77
- outcome = await run_local_agent_turn(
77
+ outcome = await get_unified_runtime().execute_local(
78
78
  manager,
79
79
  selected,
80
80
  model,
@@ -1,4 +1,5 @@
1
1
  """Tamfis-Code orchestration runtime."""
2
+ from .approvals import ApprovalAction, ApprovalBatch, describe_batch
2
3
  from .engine import AgentOrchestrator, OrchestrationRun
3
4
  from .planner import ExecutionPlan, build_reasoning_plan_prompt, parse_reasoning_plan, should_plan
4
5
  from .protocols import AgentPhase, CanonicalEvent, EventType, ToolEnvelope
@@ -6,4 +7,5 @@ from .protocols import AgentPhase, CanonicalEvent, EventType, ToolEnvelope
6
7
  __all__ = [
7
8
  "AgentOrchestrator", "OrchestrationRun", "AgentPhase", "CanonicalEvent", "EventType", "ToolEnvelope",
8
9
  "ExecutionPlan", "build_reasoning_plan_prompt", "parse_reasoning_plan", "should_plan",
10
+ "ApprovalAction", "ApprovalBatch", "describe_batch",
9
11
  ]