jac-coder 0.1.9__tar.gz → 0.2.1__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 (126) hide show
  1. {jac_coder-0.1.9 → jac_coder-0.2.1}/PKG-INFO +1 -1
  2. jac_coder-0.2.1/README.md +158 -0
  3. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/api.impl.jac +18 -29
  4. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/api.jac +1 -6
  5. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/core/nodes.impl.jac +12 -1
  6. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/core/nodes.jac +11 -17
  7. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/core/walkers.impl.jac +0 -12
  8. jac_coder-0.2.1/jac_coder/infra/kv.jac +111 -0
  9. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/infra/mcp_manager.impl.jac +11 -12
  10. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/lib/coder.impl.jac +91 -11
  11. jac_coder-0.2.1/jac_coder/runtime/cost_tracker.impl.jac +223 -0
  12. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/cost_tracker.jac +12 -12
  13. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/events.jac +69 -5
  14. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/prompt.jac +19 -4
  15. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/skills.impl.jac +5 -5
  16. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/skills.jac +7 -2
  17. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/ROADMAP.md +1 -1
  18. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-components/SKILL.md +3 -1
  19. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-core-cheatsheet/SKILL.md +1 -0
  20. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-fullstack-patterns/SKILL.md +3 -1
  21. jac_coder-0.2.1/jac_coder/skills/jac-scaffold/SKILL.md +71 -0
  22. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/__init__.jac +0 -1
  23. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/git.impl.jac +15 -14
  24. jac_coder-0.2.1/jac_coder/tool/git.jac +34 -0
  25. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/delegation.impl.jac +6 -1
  26. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/delegation.jac +1 -0
  27. jac_coder-0.2.1/jac_coder/tool/meta/think.impl.jac +19 -0
  28. jac_coder-0.2.1/jac_coder/tool/meta/think.jac +9 -0
  29. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/filesystem.impl.jac +15 -11
  30. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/load_jac_skill.impl.jac +6 -0
  31. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/load_jac_skill.jac +1 -0
  32. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/write/checked.impl.jac +3 -5
  33. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/PKG-INFO +1 -1
  34. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/SOURCES.txt +2 -11
  35. {jac_coder-0.1.9 → jac_coder-0.2.1}/pyproject.toml +1 -1
  36. jac_coder-0.1.9/README.md +0 -141
  37. jac_coder-0.1.9/jac_coder/runtime/cost_tracker.impl.jac +0 -134
  38. jac_coder-0.1.9/jac_coder/skills/jac-scaffold/SKILL.md +0 -50
  39. jac_coder-0.1.9/jac_coder/tool/git.jac +0 -18
  40. jac_coder-0.1.9/jac_coder/tool/meta/think.impl.jac +0 -4
  41. jac_coder-0.1.9/jac_coder/tool/meta/think.jac +0 -5
  42. jac_coder-0.1.9/jac_coder/tool/write/scaffold.impl.jac +0 -236
  43. jac_coder-0.1.9/jac_coder/tool/write/scaffold.jac +0 -12
  44. jac_coder-0.1.9/tests/test_context.py +0 -53
  45. jac_coder-0.1.9/tests/test_events.py +0 -40
  46. jac_coder-0.1.9/tests/test_graph.py +0 -33
  47. jac_coder-0.1.9/tests/test_interact.py +0 -40
  48. jac_coder-0.1.9/tests/test_jaccoder.py +0 -72
  49. jac_coder-0.1.9/tests/test_memory.py +0 -53
  50. jac_coder-0.1.9/tests/test_selfcorrect.py +0 -72
  51. jac_coder-0.1.9/tests/test_tools.py +0 -45
  52. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/__init__.jac +0 -0
  53. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/__init__.py +0 -0
  54. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/cli_entry.py +0 -0
  55. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/core/__init__.jac +0 -0
  56. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/core/walkers.jac +0 -0
  57. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/infra/__init__.jac +0 -0
  58. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/infra/config.impl.jac +0 -0
  59. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/infra/config.jac +0 -0
  60. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/infra/mcp_manager.jac +0 -0
  61. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/lib/__init__.jac +0 -0
  62. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/lib/coder.jac +0 -0
  63. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/__init__.jac +0 -0
  64. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/context.impl.jac +0 -0
  65. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/context.jac +0 -0
  66. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/memory.impl.jac +0 -0
  67. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/memory.jac +0 -0
  68. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/permission.impl.jac +0 -0
  69. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/permission.jac +0 -0
  70. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/runtime/prompt.impl.jac +0 -0
  71. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/serve_entry.jac +0 -0
  72. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/server.jac +0 -0
  73. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-by-llm/SKILL.md +0 -0
  74. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-auth/SKILL.md +0 -0
  75. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-organization/SKILL.md +0 -0
  76. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-cl-routing/SKILL.md +0 -0
  77. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-has-fields/SKILL.md +0 -0
  78. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-impl-files/SKILL.md +0 -0
  79. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-node-edge-patterns/SKILL.md +0 -0
  80. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-auth/SKILL.md +0 -0
  81. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-endpoints/SKILL.md +0 -0
  82. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-sv-persistence/SKILL.md +0 -0
  83. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-types/SKILL.md +0 -0
  84. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/skills/jac-walker-patterns/SKILL.md +0 -0
  85. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/mcp.impl.jac +0 -0
  86. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/mcp.jac +0 -0
  87. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/__init__.jac +0 -0
  88. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/question.impl.jac +0 -0
  89. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/question.jac +0 -0
  90. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/task.impl.jac +0 -0
  91. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/task.jac +0 -0
  92. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/todo.impl.jac +0 -0
  93. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/todo.jac +0 -0
  94. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/validate.impl.jac +0 -0
  95. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/meta/validate.jac +0 -0
  96. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/net/__init__.jac +0 -0
  97. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/net/preview.impl.jac +0 -0
  98. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/net/preview.jac +0 -0
  99. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/net/web.impl.jac +0 -0
  100. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/net/web.jac +0 -0
  101. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/__init__.jac +0 -0
  102. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/filesystem.jac +0 -0
  103. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/jac_analyzer.impl.jac +0 -0
  104. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/jac_analyzer.jac +0 -0
  105. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/search.impl.jac +0 -0
  106. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/read/search.jac +0 -0
  107. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/__init__.jac +0 -0
  108. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/guarded.impl.jac +0 -0
  109. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/guarded.jac +0 -0
  110. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/jac_tools.impl.jac +0 -0
  111. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/jac_tools.jac +0 -0
  112. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/shell.impl.jac +0 -0
  113. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/run/shell.jac +0 -0
  114. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/write/__init__.jac +0 -0
  115. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/tool/write/checked.jac +0 -0
  116. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/__init__.jac +0 -0
  117. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/colors.jac +0 -0
  118. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/sandbox.impl.jac +0 -0
  119. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/sandbox.jac +0 -0
  120. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/tool_output.impl.jac +0 -0
  121. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder/util/tool_output.jac +0 -0
  122. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/dependency_links.txt +0 -0
  123. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/entry_points.txt +0 -0
  124. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/requires.txt +0 -0
  125. {jac_coder-0.1.9 → jac_coder-0.2.1}/jac_coder.egg-info/top_level.txt +0 -0
  126. {jac_coder-0.1.9 → jac_coder-0.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jac-coder
3
- Version: 0.1.9
3
+ Version: 0.2.1
4
4
  Summary: AI coding agent backend for Jac, powered by jac-byllm
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -0,0 +1,158 @@
1
+ # JacCoder
2
+
3
+ ![JacCoder CLI](assets/cli.png)
4
+
5
+ AI coding agent for the Jaseci stack, built entirely in [Jac](https://github.com/jaseci-labs/jaseci) using Object-Spatial Programming. Features an orchestrator-worker architecture with compiler-level Jac Intelligence, self-correcting code writes, and in-process SubAgent delegation.
6
+
7
+ ## Architecture
8
+
9
+ ![JacCoder Graph Architecture](assets/graph.png)
10
+
11
+ ## Features
12
+
13
+ - **Orchestrator-Worker Architecture** — MainAgent (27 tools) handles tasks directly or delegates to focused WorkerAgent / ExplorerAgent walkers
14
+ - **Jac Intelligence** — compiler-level AST analysis via jaclang (`analyze_project`, `find_symbol`)
15
+ - **Skills System** — Claude-Code-compatible `SKILL.md` directories, lazy-loaded so the LLM gets authoritative Jac syntax instead of stale training data
16
+ - **Self-Correcting Writes** — automatic JS-to-Jac sanitization, in-process syntax check, and `.jac-server.log` error monitoring on every write
17
+ - **Browser Validation Loop** — `browser_validate` shells out to agent-browser with cross-turn FAIL escalation (1st: hint → 2nd: warning → 3rd+: mandatory bisect)
18
+ - **In-Process SubAgents** — walkers, not subprocesses; share event stream + cost tracker + graph context
19
+ - **MCP Integration** — built-in `jac-mcp` plus user-added stdio/http/sse servers, configs persisted in the graph
20
+ - **Multi-Provider LLM** — 100+ models via [byllm](https://github.com/jaseci-labs/byllm); per-session model + API-key override is thread-local (no `os.environ` mutation)
21
+ - **Public API** — clean interface for CLI, VS Code extension, JacBuilder, library mode
22
+ - **Context Management** — smart tiered compaction with LLM-summary fallback; mode-aware dynamic prompt assembly
23
+
24
+ ## Prerequisites
25
+
26
+ - Python 3.12+
27
+
28
+ ```bash
29
+ pip install jac-coder
30
+ # or for development:
31
+ pip install -e .
32
+ ```
33
+
34
+ Dependencies (auto-installed): `jaclang`, `byllm`, `mcp>=1.0.0`, `jac-mcp`, `python-dotenv`.
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ # Set API key
40
+ export OPENAI_API_KEY="sk-..."
41
+
42
+ # Interactive REPL
43
+ jac cli.jac
44
+
45
+ # Single prompt (non-interactive)
46
+ jac cli.jac run "build a hello world jac app at /tmp/myapp"
47
+
48
+ # Resume a session
49
+ jac cli.jac session <id-prefix>
50
+ ```
51
+
52
+ ## Architecture
53
+
54
+ ```
55
+ Root → Session → MainAgent
56
+ ├── handles simple tasks directly (read, search, edit, git, browser)
57
+ └── spawn_agent() → WorkerAgent (write+run) or ExplorerAgent (read-only)
58
+ └── walker runs in-process, returns result to MainAgent
59
+ ```
60
+
61
+ - **MainAgent (node)** — orchestrator with 27 tools. Handles simple tasks directly, delegates complex work via `spawn_agent`. `max_react_iterations=80`.
62
+ - **WorkerAgent (walker)** — in-process SubAgent with 14 tools (can write/edit/run). For known changes.
63
+ - **ExplorerAgent (walker)** — in-process SubAgent with 10 tools (read-only + web search). For root-cause investigation.
64
+ - **Session (node)** — persistent chat state, history, active files, errors, mode hint.
65
+ - **ProjectMemory (node)** — AST-derived codebase knowledge (nodes, walkers, edges, imports). Backed by `.jaccoder/progress.md` as primary source of truth.
66
+ - **McpRegistry (node)** — persisted MCP server configs.
67
+
68
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full picture.
69
+
70
+ ## Tools
71
+
72
+ MainAgent has 27 tools, grouped by domain:
73
+
74
+ | Group | Tool | Description |
75
+ |-------|------|-------------|
76
+ | Reason | `think` | Explicit reasoning step (loop-guarded) |
77
+ | Read | `read_file` | Read files with line numbers and pagination |
78
+ | Read | `list_files` | List directory contents |
79
+ | Read | `grep_search` | Regex search across files |
80
+ | Read | `find_files` | Find files by glob pattern |
81
+ | Read | `analyze_project` | Full AST analysis (nodes, walkers, edges, imports) |
82
+ | Read | `find_symbol` | Find symbol definition, fields, usages, exact import |
83
+ | Read | `load_jac_skill` | Load full body of a Jac skill by name |
84
+ | Write | `write_code` | Write file — anti-pattern blocking, auto sanitize, syntax check |
85
+ | Write | `edit_code` | Find-and-replace with the same self-correcting pipeline |
86
+ | Run | `run_command` | Execute shell commands (permission-guarded, auto-detects servers → background) |
87
+ | Run | `jac_run` | Run `.jac` files |
88
+ | Git | `git_status`, `git_diff`, `git_log`, `git_commit` | First-class git ops (`git_commit` is the only sanctioned way to commit) |
89
+ | Web | `web_fetch`, `web_search` | HTTP fetch + DuckDuckGo search |
90
+ | Browser | `browser_open`, `browser_do`, `browser_state`, `browser_validate`, `browser_close` | Visual QA via agent-browser; `browser_validate` is the primary pass/fail check |
91
+ | Delegate | `spawn_agent` | In-process WorkerAgent / ExplorerAgent walker |
92
+ | Interact | `ask_question`, `update_todos` | User prompt + multi-step task tracking |
93
+ | MCP | `mcp_call` | Call any tool from a connected MCP server |
94
+
95
+ ## Public API
96
+
97
+ External apps import only from `jac_coder.api`:
98
+
99
+ ```python
100
+ from jac_coder.api import initialize, create_session, chat, close_session
101
+
102
+ initialize("web")
103
+ session = create_session("/path/to/project", title="My App")
104
+ result = chat(session["session_id"], "build a calculator")
105
+ print(result["response"])
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ Config sources (highest priority first):
111
+
112
+ 1. Environment: `MODEL`, `TEMPERATURE`, `MAX_TOKENS`, `MAX_REACT_ITERATIONS`
113
+ 2. Project: `./jaccoder.json`
114
+ 3. Global: `~/.jaccoder/config.json`
115
+
116
+ ## Testing
117
+
118
+ ```bash
119
+ python -m pytest tests/ -v # 10 unit suites
120
+ ```
121
+
122
+ ## Project Structure
123
+
124
+ ```
125
+ jac-code/
126
+ ├── cli.jac # CLI entry point (REPL + subcommands)
127
+ ├── jac_coder/ # Core package
128
+ │ ├── api.jac # Public API — the only module external apps import
129
+ │ ├── server.jac # JSON-RPC stdio server for the VS Code extension
130
+ │ ├── core/
131
+ │ │ ├── nodes.jac # MainAgent, WorkerAgent, ExplorerAgent, Session, ProjectMemory
132
+ │ │ └── walkers.jac # interact, new_session, list_sessions, etc.
133
+ │ ├── lib/
134
+ │ │ └── coder.jac # JacCoder library-mode class (stateless + service modes)
135
+ │ ├── infra/
136
+ │ │ ├── config.jac # Multi-source config + SessionAwareModel (thread-local LLM override)
137
+ │ │ └── mcp_manager.jac # MCP server registry (graph-persisted)
138
+ │ ├── runtime/
139
+ │ │ ├── context.jac # Tiered compaction with LLM-summary fallback
140
+ │ │ ├── events.jac # Event bus, doom-loop detection, abort signal
141
+ │ │ ├── memory.jac # AST-first ProjectMemory (LLM fallback)
142
+ │ │ ├── permission.jac # Permission rule engine (allow / ask / deny)
143
+ │ │ ├── prompt.jac # Mode-aware dynamic prompt assembly
144
+ │ │ ├── skills.jac # SKILL.md registry + listing injection
145
+ │ │ └── cost_tracker.jac # Opt-in token + USD cost tracking
146
+ │ ├── tool/ # 28 tools organized by domain
147
+ │ │ ├── meta/ # think, spawn_agent, update_todos, ask_question, validate
148
+ │ │ ├── read/ # filesystem, search, jac_analyzer, load_jac_skill
149
+ │ │ ├── write/ # checked (write_code/edit_code), scaffold
150
+ │ │ ├── run/ # shell, guarded, jac_tools
151
+ │ │ ├── net/ # web, preview (browser_*)
152
+ │ │ ├── git.jac # git_status, git_diff, git_log, git_commit
153
+ │ │ └── mcp.jac # mcp_call
154
+ │ └── skills/ # Bundled SKILL.md directories (Claude-Code-compatible)
155
+ ├── tests/ # 10 unit suites (pytest wrappers + check_*.jac)
156
+ ├── vscode-jac-coder/ # VS Code extension (TypeScript)
157
+ └── docs/ # ARCHITECTURE, ROADMAP, PROGRESS, LIBRARY_MODE
158
+ ```
@@ -107,9 +107,6 @@ impl create_session(directory: str, title: str = "", agent: str = "main") -> dic
107
107
  return {"error": "Failed to create session"};
108
108
  }
109
109
 
110
- # Store in registry so background threads can find it
111
- _session_registry[session_id] = session_obj;
112
-
113
110
  return {"session_id": session_id, "title": session_title, "status": "created"};
114
111
  }
115
112
 
@@ -166,7 +163,6 @@ impl close_session(session_id: str) -> dict {
166
163
  }
167
164
  matches[0].status = "closed";
168
165
  matches[0].updated_at = datetime.now().isoformat();
169
- _session_registry.pop(session_id, None);
170
166
  return {"status": "closed", "id": matches[0].id};
171
167
  }
172
168
 
@@ -185,15 +181,9 @@ impl chat(
185
181
  edit_mode: str = "auto",
186
182
  env_overrides: dict = {}
187
183
  ) -> dict {
188
- # Find session — try registry first (works across threads), then graph
189
- session = _session_registry.get(session_id);
190
- if not session {
191
- matches = [root()-->][?:Session][?id==session_id];
192
- if matches {
193
- session = matches[0];
194
- _session_registry[session_id] = session;
195
- }
196
- }
184
+ # Find session — query the graph directly. The graph is shared across pods
185
+ # via MongoDB persistence, so this works in single-pod and multi-pod alike.
186
+ session = _find_session(session_id);
197
187
  if not session {
198
188
  return {"error": "Session not found", "agent": "error"};
199
189
  }
@@ -203,7 +193,6 @@ impl chat(
203
193
  try {
204
194
  _ = session.id;
205
195
  } except Exception {
206
- _session_registry.pop(session_id, None);
207
196
  return {"error": "Session is stale after a server update. Please close this session and open a new one.", "agent": "error"};
208
197
  }
209
198
 
@@ -220,8 +209,12 @@ impl chat(
220
209
  if env_overrides {
221
210
  import from jac_coder.infra.config { set_session_llm }
222
211
  import logging as _chatlog;
223
- _api_key = str(env_overrides.get("OPENAI_API_KEY", env_overrides.get("ANTHROPIC_API_KEY", "")));
224
212
  _model_name = str(env_overrides.get("MODEL", ""));
213
+ if _model_name.startswith("claude-") {
214
+ _api_key = str(env_overrides.get("ANTHROPIC_API_KEY", ""));
215
+ } else {
216
+ _api_key = str(env_overrides.get("OPENAI_API_KEY", env_overrides.get("ANTHROPIC_API_KEY", "")));
217
+ }
225
218
  _chatlog.getLogger("jac_coder.api").info(f"env_overrides received: model={_model_name} has_key={bool(_api_key)}");
226
219
  if _api_key or _model_name {
227
220
  import from jac_coder.infra.config { llm as _current_llm }
@@ -292,7 +285,7 @@ impl chat(
292
285
  ctx_history.insert(0, {"role": "system", "content": agent_context});
293
286
  }
294
287
 
295
- # Inject available MCP tools into context (cached to avoid rebuilding every turn)
288
+ # Inject available MCP tools into context (rebuilt every turn)
296
289
  mcp_msg = _get_mcp_context_msg();
297
290
  if mcp_msg {
298
291
  ctx_history.insert(0, mcp_msg);
@@ -557,7 +550,7 @@ impl api_mcp_list() -> list {
557
550
 
558
551
 
559
552
  impl abort_session(session_id: str) -> None {
560
- session = _session_registry.get(session_id);
553
+ session = _find_session(session_id);
561
554
  if session {
562
555
  session.chat_history.append({
563
556
  "role": "assistant",
@@ -567,9 +560,14 @@ impl abort_session(session_id: str) -> None {
567
560
  }
568
561
 
569
562
 
570
- glob _mcp_ctx_cache: dict = {}; # {"msg": dict, "tool_names": set}
563
+ """Build MCP tools context message for the LLM system prompt.
571
564
 
572
- """Build MCP tools context message, cached by tool names."""
565
+ Rebuilt every turn no module-level cache. The previous in-process cache
566
+ (`_mcp_ctx_cache`) was keyed on `frozenset(tool_names)`, so descriptions or
567
+ inputSchemas could change on another pod without invalidating the cache,
568
+ causing stale system prompts. Rebuild cost is ~50us of string formatting,
569
+ dominated by the LLM call that follows.
570
+ """
573
571
  def _get_mcp_context_msg() -> dict | None {
574
572
  try {
575
573
  mcp_tools = mcp_get_tools();
@@ -579,12 +577,6 @@ def _get_mcp_context_msg() -> dict | None {
579
577
  if not mcp_tools {
580
578
  return None;
581
579
  }
582
- # Check if tools changed since last call
583
- current_names = frozenset(f"{t['server']}::{t['name']}" for t in mcp_tools);
584
- if _mcp_ctx_cache.get("tool_names") == current_names {
585
- return _mcp_ctx_cache.get("msg");
586
- }
587
- # Rebuild
588
580
  lines: list = [
589
581
  "Available MCP tools — call via mcp_call(server_name, tool_name, arguments_json):",
590
582
  "Check each tool's inputSchema for required arguments before calling."
@@ -600,10 +592,7 @@ def _get_mcp_context_msg() -> dict | None {
600
592
  }
601
593
  lines.append(f" - server={t['server']} tool={t['name']} {t['description']}{schema_hint}");
602
594
  }
603
- msg: dict = {"role": "system", "content": "\n".join(lines)};
604
- _mcp_ctx_cache["msg"] = msg;
605
- _mcp_ctx_cache["tool_names"] = current_names;
606
- return msg;
595
+ return {"role": "system", "content": "\n".join(lines)};
607
596
  }
608
597
 
609
598
 
@@ -13,7 +13,7 @@ import from jac_coder.util.tool_output { tool_end }
13
13
  import from jac_coder.runtime.permission { permission_engine }
14
14
  import from jac_coder.util.sandbox { set_sandbox_root, set_browser_exec }
15
15
  import from jac_coder.runtime.context { build_context, ContextConfig }
16
- import from jac_coder.core.walkers { new_session, ensure_main_agent, _consume_llm_stream }
16
+ import from jac_coder.core.walkers { new_session, ensure_main_agent, _consume_llm_stream, _find_session }
17
17
  import from jac_coder.infra.mcp_manager {
18
18
  mcp_add_server,
19
19
  mcp_disconnect_server,
@@ -43,11 +43,6 @@ import from jac_coder.runtime.events {
43
43
  }
44
44
 
45
45
 
46
- # Thread-safe session registry — graph root() is per-request in jac-cloud,
47
- # so background threads can't find sessions via [root()-->]. This dict
48
- # is module-level and accessible from any thread.
49
- glob _session_registry: dict = {};
50
-
51
46
  """Initialize jac-coder."""
52
47
  def initialize(mode: str = "web") -> None;
53
48
 
@@ -106,7 +106,17 @@ Respond to what the user is asking RIGHT NOW. Don't recap. Concise by default, d
106
106
 
107
107
  ## Delegation
108
108
  - Simple (1-2 files, questions, git): handle directly.
109
- - Multi-file or investigation: spawn_agent(task, mode="worker"|"explorer").
109
+ - Multi-file, investigation, or complex: spawn_agent().
110
+
111
+ SubAgent cannot see this conversation. Every task string must be fully self-contained.
112
+
113
+ Before spawning worker: use think + read_file + grep_search until you know the exact change needed. Task string must include: file path:line, function/node name, exact problem, what NOT to touch.
114
+ Good: `Fix delegation.impl.jac:112 — has_errs hardcoded False, extract from result.errors. Don't touch above line 108.`
115
+ Bad: `Fix the error tracking bug` / `Based on what you found, fix it`
116
+
117
+ Mode: `explorer` = root cause unknown → investigate first, then YOU synthesize findings and spawn `worker`. `worker` = exact file and line known → implement directly.
118
+
119
+ Never delegate understanding. Task strings must prove you already know what to change.
110
120
  """;
111
121
 
112
122
 
@@ -118,6 +128,7 @@ sem WorkerAgent.do_work = """
118
128
 
119
129
  Expert Jac coder executing a delegated task. Never edit `.jac/` (compiled output).
120
130
 
131
+
121
132
  ## Iteration Budget
122
133
  Build breadth-first — get all files written and app running before polishing. If a component fails after 2 fix attempts, write a minimal working version and move on. Never spend 10+ iterations on one file.
123
134
 
@@ -9,11 +9,10 @@ import from jac_coder.runtime.events { is_abort_requested }
9
9
 
10
10
  # MainAgent tools — full orchestrator set
11
11
  import from jac_coder.tool.meta.think { think }
12
- import from jac_coder.tool.meta.delegation { spawn_agent }
12
+ import from jac_coder.tool.meta.delegation { spawn_agent, set_iter_count }
13
13
  import from jac_coder.tool.meta.todo { update_todos }
14
14
  import from jac_coder.tool.read.load_jac_skill { load_jac_skill }
15
15
  import from jac_coder.tool.meta.question { ask_question }
16
- import from jac_coder.tool.write.scaffold { scaffold_project }
17
16
  import from jac_coder.tool.net.web { web_fetch, web_search }
18
17
  import from jac_coder.tool.run.guarded { run_command }
19
18
  import from jac_coder.tool.run.jac_tools { jac_check, jac_run }
@@ -163,7 +162,6 @@ node MainAgent {
163
162
  write_code,
164
163
  run_command,
165
164
  jac_run,
166
- scaffold_project,
167
165
  # Git — first-class version control
168
166
  git_status,
169
167
  git_diff,
@@ -200,8 +198,9 @@ node MainAgent {
200
198
  # SubAgent walkers — spawned on MainAgent node for task delegation
201
199
  # ---------------------------------------------------------------------------
202
200
 
203
- """on_iteration callback — checks abort flag between ReAct iterations."""
201
+ """on_iteration callback — checks abort flag and tracks real iteration count."""
204
202
  def _iteration_hook(ctx: IterationContext) -> IterationAction {
203
+ set_iter_count(ctx.iteration);
205
204
  if is_abort_requested() {
206
205
  return IterationAction.ABORT;
207
206
  }
@@ -217,6 +216,9 @@ walker WorkerAgent {
217
216
 
218
217
  def do_work(task_str: str) -> str by llm(
219
218
  tools=[
219
+ # Reason
220
+ think,
221
+ # Understand
220
222
  load_jac_skill,
221
223
  analyze_project,
222
224
  find_symbol,
@@ -224,26 +226,18 @@ walker WorkerAgent {
224
226
  list_files,
225
227
  grep_search,
226
228
  find_files,
229
+ # Act
227
230
  write_code,
228
231
  edit_code,
229
232
  run_command,
230
233
  jac_run,
231
- scaffold_project,
234
+ # Git — status only, workers don't commit
232
235
  git_status,
233
- git_diff,
234
- git_commit,
235
- git_log,
236
- web_fetch,
237
- web_search,
238
- browser_open,
239
- browser_do,
240
- browser_state,
241
- browser_close,
242
- browser_validate,
243
- mcp_call
236
+ # Validate
237
+ browser_validate
244
238
  ],
245
239
  on_iteration=_iteration_hook,
246
- max_react_iterations=60,
240
+ max_react_iterations=30,
247
241
  temperature=0.2,
248
242
  max_tokens=8192,
249
243
  stream=True,
@@ -46,10 +46,6 @@ impl new_session.create with Root entry {
46
46
  here ++> session;
47
47
  agent = ensure_main_agent();
48
48
  session ++> agent;
49
- # Persist the new session and the root→session edge so it survives server restarts
50
- save(here);
51
- save(session);
52
- commit();
53
49
  report {"session_id": session.id, "title": session.title, "status": "created"};
54
50
  }
55
51
 
@@ -103,8 +99,6 @@ impl close_session.close with Root entry {
103
99
  if matches {
104
100
  matches[0].status = "closed";
105
101
  matches[0].updated_at = datetime.now().isoformat();
106
- save(matches[0]);
107
- commit();
108
102
  report {"status": "closed", "id": matches[0].id};
109
103
  } else {
110
104
  report {"error": "Session not found"};
@@ -183,8 +177,6 @@ impl interact.enter_session with Session entry {
183
177
  here.chat_history.append({"role": "user", "content": self.message});
184
178
  here.updated_at = datetime.now().isoformat();
185
179
  self.chat_history = here.chat_history;
186
- save(here);
187
- commit();
188
180
 
189
181
  # Visit MainAgent
190
182
  visit [-->][?:MainAgent] else {
@@ -244,8 +236,6 @@ impl _persist_response(
244
236
  session.chat_history.append(record);
245
237
  session.last_agent = agent_mode;
246
238
  session.updated_at = datetime.now().isoformat();
247
- save(session);
248
- commit();
249
239
  }
250
240
 
251
241
 
@@ -415,8 +405,6 @@ impl _respond_and_persist(agent: MainAgent, ctx: interact) -> None {
415
405
  # Save mode hint for next turn's dynamic prompt assembly
416
406
  import from jac_coder.runtime.prompt { infer_mode_hint }
417
407
  session.last_mode_hint = infer_mode_hint(tools_used, files_modified);
418
- save(session);
419
- commit();
420
408
  }
421
409
 
422
410
  # If aborted, skip persistence — abort note already persisted by stop_jaccoder
@@ -0,0 +1,111 @@
1
+ """Cross-pod key-value cache backed by jac-scale's managed Redis.
2
+
3
+ Used for ephemeral state that must be consistent across horizontally-scaled
4
+ jac-coder pods (e.g. the MCP tool list cache, cross-pod abort signals). Falls
5
+ back to a no-op when Redis is not configured — that case applies to local CLI /
6
+ stdio mode and to dev environments where the kvstore URL isn't set. A no-op
7
+ fallback is correct (just slower, since each pod recomputes); a None return from
8
+ `kv_get` is treated by callers as a cache miss, and `get_kv()` returns a no-op
9
+ stub so B2 abort callers can invoke it unconditionally without None checks.
10
+
11
+ """
12
+
13
+ import sys;
14
+ import threading;
15
+
16
+ # Lazy singleton — created on first access. None means "Redis unavailable,
17
+ # operate in no-cache mode".
18
+ glob _kv: Any = None;
19
+ glob _kv_init_done: bool = False;
20
+ glob _kv_init_lock: Any = threading.Lock();
21
+
22
+
23
+ """Silent no-op fallback returned by get_kv() when Redis is unavailable.
24
+ Satisfies the .get() / .set_with_ttl() / .delete() interface so B2 abort
25
+ callers never need to check for None."""
26
+ obj _NoOpKv {
27
+ def set_with_ttl(key: str, value: dict, ttl: int) -> bool { return False; }
28
+ def get(key: str) -> (dict | None) { return None; }
29
+ def delete(key: str) -> int { return 0; }
30
+ }
31
+
32
+
33
+ """Initialize the kvstore handle once. Safe to call repeatedly."""
34
+ def _try_init() -> None {
35
+ global _kv, _kv_init_done;
36
+ if _kv_init_done {
37
+ return;
38
+ }
39
+ with _kv_init_lock {
40
+ if _kv_init_done {
41
+ return;
42
+ }
43
+ _kv_init_done = True;
44
+ try {
45
+ import from jac_scale.lib { kvstore }
46
+ _kv = kvstore(db_name="jac_coder", db_type="redis");
47
+ } except Exception as e {
48
+ # Redis not configured — common in CLI/stdio mode and local dev.
49
+ # Cross-pod cache is disabled; callers fall through to recompute.
50
+ sys.stderr.write(
51
+ f"[kv] Redis unavailable, cross-pod cache disabled: {e}\n"
52
+ );
53
+ _kv = None;
54
+ }
55
+ }
56
+ }
57
+
58
+
59
+ """Get a cached dict by key, or None if missing/unavailable."""
60
+ def kv_get(key: str) -> dict | None {
61
+ _try_init();
62
+ if _kv is None {
63
+ return None;
64
+ }
65
+ try {
66
+ return _kv.get(key);
67
+ } except Exception as e {
68
+ sys.stderr.write(f"[kv] get({key}) failed: {e}\n");
69
+ return None;
70
+ }
71
+ }
72
+
73
+
74
+ """Store a dict under key with TTL in seconds. No-op if Redis unavailable."""
75
+ def kv_set_ttl(key: str, value: dict, ttl: int) -> None {
76
+ _try_init();
77
+ if _kv is None {
78
+ return;
79
+ }
80
+ try {
81
+ _kv.set_with_ttl(key, value, ttl=ttl);
82
+ } except Exception as e {
83
+ sys.stderr.write(f"[kv] set_with_ttl({key}) failed: {e}\n");
84
+ }
85
+ }
86
+
87
+
88
+ """Delete a key. No-op if Redis unavailable or key missing."""
89
+ def kv_delete(key: str) -> None {
90
+ _try_init();
91
+ if _kv is None {
92
+ return;
93
+ }
94
+ try {
95
+ _kv.delete(key);
96
+ } except Exception as e {
97
+ sys.stderr.write(f"[kv] delete({key}) failed: {e}\n");
98
+ }
99
+ }
100
+
101
+
102
+ """Return the kvstore instance or a no-op stub — never returns None.
103
+ Used by B2 (cross-pod abort) in events.jac where callers invoke
104
+ .get() / .set_with_ttl() / .delete() directly without None-checking."""
105
+ def get_kv() -> Any {
106
+ _try_init();
107
+ if _kv is None {
108
+ return _NoOpKv();
109
+ }
110
+ return _kv;
111
+ }
@@ -17,6 +17,7 @@ import from mcp { ClientSession }
17
17
  import from mcp.client.stdio { stdio_client, StdioServerParameters }
18
18
  import from mcp.client.streamable_http { streamablehttp_client }
19
19
  import from mcp.client.sse { sse_client }
20
+ import from jac_coder.infra.kv { kv_get, kv_set_ttl, kv_delete }
20
21
 
21
22
  # Cached update info for jac-mcp (populated by background thread on startup).
22
23
  glob _jac_mcp_update_info: dict = {};
@@ -27,7 +28,7 @@ glob _mgr_thread: Any = None;
27
28
  glob _mgr_lock: Any = threading.Lock();
28
29
  glob _sessions: dict = {}; # name -> ClientSession (open inside _mgr_loop)
29
30
  glob _exit_stacks: dict = {}; # name -> AsyncExitStack (keeps transports alive)
30
- glob _tools_cache: dict = {}; # {"tools": [...], "ts": float} — cached tool list
31
+ glob _TOOLS_CACHE_KEY: str = "jc:mcp:tools";
31
32
  glob _TOOLS_CACHE_TTL: int = 30; # seconds before tool list is re-fetched
32
33
  glob _builtin_names: set = set(); # names registered via mcp_register_builtin
33
34
  glob _disconnected_names: set = set(); # names intentionally disconnected by user
@@ -84,8 +85,6 @@ def _load_configs() -> dict {
84
85
  def _save_configs(configs: dict) -> None {
85
86
  reg = _get_registry();
86
87
  reg.servers = configs;
87
- save(reg);
88
- commit();
89
88
  }
90
89
 
91
90
 
@@ -277,7 +276,7 @@ impl mcp_add_server(name: str, config: dict) -> dict {
277
276
  configs = _load_configs();
278
277
  configs[name] = config;
279
278
  _save_configs(configs);
280
- _tools_cache.clear(); # Invalidate cache — new server added
279
+ kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — new server added
281
280
  return {
282
281
  "status": "connected",
283
282
  "name": name,
@@ -299,7 +298,7 @@ impl mcp_disconnect_server(name: str) -> dict {
299
298
  }
300
299
  _disconnected_names.add(name);
301
300
  _submit(_close_connection(name));
302
- _tools_cache.clear();
301
+ kv_delete(_TOOLS_CACHE_KEY);
303
302
  return {"status": "disconnected", "name": name};
304
303
  }
305
304
 
@@ -317,7 +316,7 @@ impl mcp_reconnect_server(name: str) -> dict {
317
316
  # produces Unknown in the Jac type checker — the for pattern is used
318
317
  # consistently throughout this file).
319
318
  _submit(_close_connection(name));
320
- _tools_cache.clear();
319
+ kv_delete(_TOOLS_CACHE_KEY);
321
320
  for (n, config) in configs.items() {
322
321
  if n == name {
323
322
  try {
@@ -346,7 +345,7 @@ impl mcp_delete_server(name: str) -> dict {
346
345
  _submit(_close_connection(name));
347
346
  configs.pop(name);
348
347
  _save_configs(configs);
349
- _tools_cache.clear();
348
+ kv_delete(_TOOLS_CACHE_KEY);
350
349
  return {"status": "deleted", "name": name};
351
350
  }
352
351
 
@@ -397,6 +396,7 @@ impl mcp_register_builtin(name: str, config: dict) -> None {
397
396
  config["builtin"] = True;
398
397
  configs[name] = config;
399
398
  _save_configs(configs);
399
+ kv_delete(_TOOLS_CACHE_KEY); # Invalidate cross-pod cache — server set changed
400
400
  if name == "jac-mcp" {
401
401
  threading.Thread(target=_check_jac_mcp_version, daemon=True).start();
402
402
  }
@@ -405,9 +405,9 @@ impl mcp_register_builtin(name: str, config: dict) -> None {
405
405
 
406
406
  """Return a flat list of all tools from all registered servers (cached for 30s)."""
407
407
  impl mcp_get_tools() -> list {
408
- now = time.time();
409
- if _tools_cache and (now - _tools_cache.get("ts", 0)) < _TOOLS_CACHE_TTL {
410
- return _tools_cache["tools"];
408
+ cached = kv_get(_TOOLS_CACHE_KEY);
409
+ if cached is not None {
410
+ return cached.get("tools", []);
411
411
  }
412
412
 
413
413
  configs = _load_configs();
@@ -420,8 +420,7 @@ impl mcp_get_tools() -> list {
420
420
  }
421
421
  }
422
422
 
423
- _tools_cache["tools"] = all_tools;
424
- _tools_cache["ts"] = now;
423
+ kv_set_ttl(_TOOLS_CACHE_KEY, {"tools": all_tools}, ttl=_TOOLS_CACHE_TTL);
425
424
  return all_tools;
426
425
  }
427
426