context-rail 0.2.3__tar.gz → 0.2.4__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.
- {context_rail-0.2.3/src/context_rail.egg-info → context_rail-0.2.4}/PKG-INFO +1 -1
- {context_rail-0.2.3 → context_rail-0.2.4}/pyproject.toml +1 -1
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/cli.py +43 -7
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/db.py +38 -4
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/_imports.py +2 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_lifecycle.py +5 -3
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_orient.py +9 -5
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_plan_review.py +2 -4
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_projects.py +6 -6
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/skills/setup/SKILL.md +25 -15
- {context_rail-0.2.3 → context_rail-0.2.4/src/context_rail.egg-info}/PKG-INFO +1 -1
- {context_rail-0.2.3 → context_rail-0.2.4}/LICENSE +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/README.md +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/setup.cfg +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/__init__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/__main__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/__init__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_contracts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_drift.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_evidence_harvest.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_intent_match.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_pdca.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_rails.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_runs.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy/_runtime.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/autonomy.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm/__init__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm/_phase_drafts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm/_plan_drafts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm/drafts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm_artifacts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm_conductor.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm_core.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/brainstorm_synthesis.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/context_pack.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/db_schema/__init__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/db_schema/_gates.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/db_schema/_schema.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/db_schema.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/focus.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/gates.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/rules.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/__init__.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/_tool_surface.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_artifacts.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_autonomy.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_brainstorm.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_categories.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_configure.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_context.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_decisions.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_find.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_focus.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_gates.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_intent.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_notes.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_phase_brainstorm.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_planning.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_retro.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_risks.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_roadmap.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_rules.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_runtime.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server/tools_tasks.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/server.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/skills/daily/SKILL.md +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/skills/governance/SKILL.md +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/skills/orchestration/SKILL.md +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail/skills/wiki/SKILL.md +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail.egg-info/SOURCES.txt +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail.egg-info/dependency_links.txt +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail.egg-info/entry_points.txt +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail.egg-info/requires.txt +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/src/context_rail.egg-info/top_level.txt +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_agent_lanes.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_ambient_context.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_autonomy_drift.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_autonomy_record.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_brainstorm.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_brainstorm_tools.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_cli_agents.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_context_learning_loop.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_db.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_deps.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_evidence_harvest.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_evidence_validation.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_find.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_focus_tools.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_gates.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_governance_mode.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_governance_tiers.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_integration_lifecycle.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_intent_match.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_non_blocking_pdca.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_notes.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_orient.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_parallel_chat_safety.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_plan_review.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_project_create.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_retro.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_routing.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_runtime_check.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_runtime_integration.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_schema_invariants.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_smoke_v2.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_smoke_vibe.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_task_lifecycle.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_task_title_validation.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_tool_registry_integrity.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_tool_surface.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_usability_fixes.py +0 -0
- {context_rail-0.2.3 → context_rail-0.2.4}/tests/test_web.py +0 -0
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from .db import DB_DIR, DB_PATH, STATE_PATH, get_db, init_db, load_state, resolve_workspace_project
|
|
8
|
+
from .db import DB_DIR, DB_PATH, STATE_PATH, get_db, init_db, load_state, resolve_workspace_project, write_local_project, find_local_project
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
@@ -85,6 +85,17 @@ def ensure_agents_md(project_id: str = "", project_name: str = "") -> None:
|
|
|
85
85
|
agents_path.write_text(existing.rstrip() + "\n\n" + wrapped, encoding="utf-8")
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _ensure_gitignore_has_context_rail(cwd: str) -> None:
|
|
89
|
+
"""Add .context-rail to .gitignore if a .gitignore exists but doesn't mention it."""
|
|
90
|
+
gi_path = Path(cwd) / ".gitignore"
|
|
91
|
+
if not gi_path.is_file():
|
|
92
|
+
return
|
|
93
|
+
content = gi_path.read_text(encoding="utf-8")
|
|
94
|
+
if ".context-rail" in content:
|
|
95
|
+
return
|
|
96
|
+
gi_path.write_text(content.rstrip() + "\n\n.context-rail\n", encoding="utf-8")
|
|
97
|
+
|
|
98
|
+
|
|
88
99
|
def _active_project_for_agents_md() -> tuple[str, str]:
|
|
89
100
|
from pathlib import Path
|
|
90
101
|
project_id, _ = resolve_workspace_project(str(Path.cwd()))
|
|
@@ -342,19 +353,44 @@ def cmd_init() -> None:
|
|
|
342
353
|
print(" Skills: context-rail skills installed to .agents/skills/")
|
|
343
354
|
else:
|
|
344
355
|
from pathlib import Path
|
|
345
|
-
|
|
356
|
+
cwd = str(Path.cwd())
|
|
357
|
+
pid = ""
|
|
358
|
+
resolution = ""
|
|
359
|
+
|
|
360
|
+
# 1. Local .context-rail file
|
|
361
|
+
pid = find_local_project(cwd)
|
|
362
|
+
if pid:
|
|
363
|
+
resolution = ".context-rail file"
|
|
364
|
+
# 2. Workspace binding
|
|
365
|
+
if not pid:
|
|
366
|
+
pid, tier = resolve_workspace_project(cwd)
|
|
367
|
+
if pid:
|
|
368
|
+
resolution = f"workspace binding ({tier})"
|
|
369
|
+
# 3. Global state fallback
|
|
346
370
|
if not pid:
|
|
347
371
|
pid = load_state().get("active_project_id", "")
|
|
372
|
+
if pid:
|
|
373
|
+
resolution = "global state fallback"
|
|
374
|
+
|
|
348
375
|
if pid:
|
|
349
|
-
proj = get_db().execute(
|
|
350
|
-
|
|
351
|
-
|
|
376
|
+
proj = get_db().execute(
|
|
377
|
+
"SELECT name, deleted_at FROM projects WHERE id = ?", (pid,)
|
|
378
|
+
).fetchone()
|
|
379
|
+
if proj and not proj["deleted_at"]:
|
|
380
|
+
source = "" if resolution == ".context-rail file" else f" (source: {resolution})"
|
|
381
|
+
print(f"Active project: {proj['name']}{source}")
|
|
352
382
|
ensure_agents_md(pid, proj["name"])
|
|
383
|
+
write_local_project(cwd, pid)
|
|
384
|
+
_ensure_gitignore_has_context_rail(cwd)
|
|
385
|
+
elif proj and proj["deleted_at"]:
|
|
386
|
+
print(f"Active project: {proj['name']} (DELETED — run project_set_active() for a new project)")
|
|
387
|
+
ensure_agents_md()
|
|
353
388
|
else:
|
|
354
|
-
print(f"
|
|
389
|
+
print(f"Stale project ID '{pid}' found in {resolution}. Run project_set_active().")
|
|
355
390
|
ensure_agents_md()
|
|
356
391
|
else:
|
|
357
|
-
print(f"{projects} project(s) exist but none is active.
|
|
392
|
+
print(f"{projects} project(s) exist but none is active.")
|
|
393
|
+
print("Run project_set_active(project_id=\"...\", workdir=\".\") to bind this directory.")
|
|
358
394
|
ensure_agents_md()
|
|
359
395
|
ensure_skills()
|
|
360
396
|
print(" AGENTS.md: updated in current directory")
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import sqlite3
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
CONTEXT_RAIL_FILE = ".context-rail"
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
from contextlib import contextmanager
|
|
10
13
|
|
|
@@ -130,18 +133,46 @@ def set_workspace_binding(workdir: str, project_id: str, agent_id: str = "") ->
|
|
|
130
133
|
save_workspace_bindings(bindings)
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
def find_local_project(cwd: str) -> str:
|
|
137
|
+
"""Walk up from cwd looking for a .context-rail file containing a project_id.
|
|
138
|
+
|
|
139
|
+
Returns project_id string, or "" if none found.
|
|
140
|
+
"""
|
|
141
|
+
directory = Path(cwd).resolve()
|
|
142
|
+
while True:
|
|
143
|
+
cr_file = directory / CONTEXT_RAIL_FILE
|
|
144
|
+
if cr_file.is_file():
|
|
145
|
+
try:
|
|
146
|
+
return cr_file.read_text().strip()
|
|
147
|
+
except (OSError, UnicodeDecodeError):
|
|
148
|
+
pass
|
|
149
|
+
parent = directory.parent
|
|
150
|
+
if parent == directory:
|
|
151
|
+
break
|
|
152
|
+
directory = parent
|
|
153
|
+
return ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def write_local_project(workdir: str, project_id: str) -> None:
|
|
157
|
+
"""Write a .context-rail file in the given directory pointing to the project."""
|
|
158
|
+
cr_file = Path(workdir) / CONTEXT_RAIL_FILE
|
|
159
|
+
try:
|
|
160
|
+
cr_file.write_text(project_id + "\n")
|
|
161
|
+
except OSError:
|
|
162
|
+
pass # non-blocking — directory may not be writable
|
|
163
|
+
|
|
164
|
+
|
|
133
165
|
def resolve_pid_for_tool(workdir: str = "", agent_id: str = "") -> str:
|
|
134
166
|
"""Resolve the active project_id for a tool call.
|
|
135
167
|
|
|
136
168
|
Order of resolution:
|
|
137
169
|
|
|
170
|
+
0. Local .context-rail file in cwd (or ancestor) — auto-created by
|
|
171
|
+
project_create / project_set_active / ctx-rail init
|
|
138
172
|
1. Explicit workdir parameter on the call → workspace binding lookup
|
|
139
173
|
2. Default workdir = Path.cwd() (MCP subprocess inherits chat's cwd)
|
|
140
174
|
3. Workspace binding lookup with the effective workdir
|
|
141
|
-
4. Fall back to state.json ``active_project_id``
|
|
142
|
-
many tools (plan_phase, plan_goal, tools_risks, etc.) still read
|
|
143
|
-
state.json directly, so a project created via project_create()
|
|
144
|
-
(which writes state.json) must be visible here too.
|
|
175
|
+
4. Fall back to state.json ``active_project_id``
|
|
145
176
|
5. Empty string (caller decides what to do — usually fail with
|
|
146
177
|
"No active project")
|
|
147
178
|
|
|
@@ -152,6 +183,9 @@ def resolve_pid_for_tool(workdir: str = "", agent_id: str = "") -> str:
|
|
|
152
183
|
from pathlib import Path
|
|
153
184
|
workdir = str(Path.cwd())
|
|
154
185
|
if workdir:
|
|
186
|
+
local_pid = find_local_project(workdir)
|
|
187
|
+
if local_pid:
|
|
188
|
+
return local_pid
|
|
155
189
|
bound_pid, _tier = resolve_workspace_project(workdir, agent_id)
|
|
156
190
|
if bound_pid:
|
|
157
191
|
return bound_pid
|
|
@@ -41,6 +41,8 @@ _DELEGATES: dict[str, tuple] = {
|
|
|
41
41
|
"resolve_workspace_project": (_db, "resolve_workspace_project"),
|
|
42
42
|
"set_workspace_binding": (_db, "set_workspace_binding"),
|
|
43
43
|
"resolve_pid_for_tool": (_db, "resolve_pid_for_tool"),
|
|
44
|
+
"find_local_project": (_db, "find_local_project"),
|
|
45
|
+
"write_local_project": (_db, "write_local_project"),
|
|
44
46
|
"rows_to_list": (_db, "rows_to_list"),
|
|
45
47
|
"log_activity": (_db, "log_activity"),
|
|
46
48
|
"clock_in_agent_for_task": (_db, "clock_in_agent_for_task"),
|
|
@@ -33,9 +33,11 @@ async def session_start(agent_id: str = "", workdir: str = "") -> str:
|
|
|
33
33
|
workdir = str(Path.cwd())
|
|
34
34
|
|
|
35
35
|
if workdir:
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
36
|
+
pid = find_local_project(workdir)
|
|
37
|
+
if not pid:
|
|
38
|
+
bound_pid, tier = resolve_workspace_project(workdir, agent_id)
|
|
39
|
+
if bound_pid:
|
|
40
|
+
pid = bound_pid
|
|
39
41
|
|
|
40
42
|
if not pid:
|
|
41
43
|
state_pid = load_state().get("active_project_id")
|
|
@@ -44,7 +44,7 @@ async def orient(agent_id: str = "", workdir: str = "") -> str:
|
|
|
44
44
|
conn = get_db()
|
|
45
45
|
init_db(conn)
|
|
46
46
|
|
|
47
|
-
# --- Resolve project: workspace binding > workdir > cwd > state fallback ---
|
|
47
|
+
# --- Resolve project: .context-rail > workspace binding > workdir > cwd > state fallback ---
|
|
48
48
|
pid = ""
|
|
49
49
|
binding_tier = ""
|
|
50
50
|
|
|
@@ -56,10 +56,14 @@ async def orient(agent_id: str = "", workdir: str = "") -> str:
|
|
|
56
56
|
workdir = str(Path.cwd())
|
|
57
57
|
|
|
58
58
|
if workdir:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
pid = find_local_project(workdir)
|
|
60
|
+
if pid:
|
|
61
|
+
binding_tier = "local_file"
|
|
62
|
+
else:
|
|
63
|
+
bound_pid, tier = resolve_workspace_project(workdir, agent_id)
|
|
64
|
+
binding_tier = tier
|
|
65
|
+
if bound_pid:
|
|
66
|
+
pid = bound_pid
|
|
63
67
|
|
|
64
68
|
if not pid:
|
|
65
69
|
# Fallback: state.json active_project_id — keeps orient()
|
|
@@ -436,8 +436,7 @@ async def plan_contract_view(contract_id: str = "", workdir: str = "") -> str:
|
|
|
436
436
|
kind) or a verifiable artifact path/URL (manual_check). See
|
|
437
437
|
`default_evidence_rules()` in autonomy/_contracts.py.
|
|
438
438
|
"""
|
|
439
|
-
|
|
440
|
-
pid = state.get("active_project_id") if state else resolve_pid_for_tool(workdir)
|
|
439
|
+
pid = resolve_pid_for_tool(workdir) or load_state().get("active_project_id", "")
|
|
441
440
|
# Fall back to helper if state is empty (NCR from state.json removal)
|
|
442
441
|
if not pid:
|
|
443
442
|
pid = resolve_pid_for_tool(workdir)
|
|
@@ -530,8 +529,7 @@ async def contract_refresh(contract_id: str = "", workdir: str = "") -> str:
|
|
|
530
529
|
|
|
531
530
|
Returns the refreshed contract with before/after drift summary.
|
|
532
531
|
"""
|
|
533
|
-
|
|
534
|
-
pid = state.get("active_project_id") if state else resolve_pid_for_tool(workdir)
|
|
532
|
+
pid = resolve_pid_for_tool(workdir) or load_state().get("active_project_id", "")
|
|
535
533
|
if not pid:
|
|
536
534
|
pid = resolve_pid_for_tool(workdir)
|
|
537
535
|
if not pid:
|
|
@@ -68,6 +68,7 @@ async def project_login(project_id: str, agent_id: str = "", workdir: str = "")
|
|
|
68
68
|
binding_msg = f" Workspace {workdir} bound (agent {agent_id})."
|
|
69
69
|
else:
|
|
70
70
|
binding_msg = " (existing wildcard binding preserved; pass agent_id to bind specifically)"
|
|
71
|
+
write_local_project(workdir, project_id)
|
|
71
72
|
else:
|
|
72
73
|
binding_msg = " (pass workdir to bind workspace)"
|
|
73
74
|
|
|
@@ -109,6 +110,7 @@ async def project_create(name: str, description: str = "", budget: str = "medium
|
|
|
109
110
|
|
|
110
111
|
if workdir:
|
|
111
112
|
set_workspace_binding(workdir, pid)
|
|
113
|
+
write_local_project(workdir, pid)
|
|
112
114
|
|
|
113
115
|
# Update AGENTS.md
|
|
114
116
|
try:
|
|
@@ -221,16 +223,15 @@ async def project_list(project_id: str = "", include_deleted: bool = False) -> s
|
|
|
221
223
|
# ---------------------------------------------------------------------------
|
|
222
224
|
|
|
223
225
|
@mcp.tool()
|
|
224
|
-
async def project_budget(budget: str = "medium", project_id: str = "") -> str:
|
|
226
|
+
async def project_budget(budget: str = "medium", project_id: str = "", workdir: str = "", agent_id: str = "") -> str:
|
|
225
227
|
"""Set the project budget. Controls how deep auto-expand goes: small(3), medium(10), high(25), max.
|
|
226
228
|
|
|
227
229
|
Lower budgets save tokens during orient/focus. Use expand_all=True on focus() to bypass.
|
|
228
230
|
"""
|
|
229
231
|
conn = get_db()
|
|
230
232
|
init_db(conn)
|
|
231
|
-
state = load_state()
|
|
232
233
|
|
|
233
|
-
pid = project_id or
|
|
234
|
+
pid = project_id or resolve_pid_for_tool(workdir, agent_id) or load_state().get("active_project_id", "")
|
|
234
235
|
if not pid:
|
|
235
236
|
conn.close()
|
|
236
237
|
return json.dumps({"error": "No project"})
|
|
@@ -249,13 +250,12 @@ async def project_budget(budget: str = "medium", project_id: str = "") -> str:
|
|
|
249
250
|
|
|
250
251
|
|
|
251
252
|
@mcp.tool()
|
|
252
|
-
async def project_update(project_id: str = "", name: str = "", description: str = "", budget: str = "", status: str = "", userConfirm: bool = False) -> str:
|
|
253
|
+
async def project_update(project_id: str = "", name: str = "", description: str = "", budget: str = "", status: str = "", userConfirm: bool = False, workdir: str = "", agent_id: str = "") -> str:
|
|
253
254
|
"""Update a project. Uses active project if project_id is empty. To change status, set userConfirm=True."""
|
|
254
255
|
conn = get_db()
|
|
255
256
|
init_db(conn)
|
|
256
|
-
state = load_state()
|
|
257
257
|
|
|
258
|
-
pid = project_id or
|
|
258
|
+
pid = project_id or resolve_pid_for_tool(workdir, agent_id) or load_state().get("active_project_id", "")
|
|
259
259
|
if not pid:
|
|
260
260
|
conn.close()
|
|
261
261
|
return json.dumps({"error": "No project"})
|
|
@@ -45,36 +45,45 @@ Skills are auto-copied to `.agents/skills/context-rail/` in the current project.
|
|
|
45
45
|
```
|
|
46
46
|
1. project_create(name="My Project", description="What it does", workdir="/path/to/repo")
|
|
47
47
|
→ Creates project in database, auto-generates AGENTS.md
|
|
48
|
-
→
|
|
48
|
+
→ Creates .context-rail file + workspace binding in workdir
|
|
49
49
|
|
|
50
50
|
2. project_set_active(project_id="...", workdir="/path/to/repo")
|
|
51
|
-
→ Sets as active project in state.json
|
|
52
|
-
→ Creates
|
|
51
|
+
→ Sets as active project in state.json (global fallback)
|
|
52
|
+
→ Creates .context-rail file + workspace binding
|
|
53
|
+
→ After this, any command/tool in that directory auto-resolves
|
|
53
54
|
|
|
54
55
|
3. project_login(project_id, agent_id="...", workdir="/path/to/repo")
|
|
55
56
|
→ Alternative: log an agent into the project with workspace binding
|
|
56
57
|
→ Call this when orient() shows ⚠ unbound
|
|
58
|
+
|
|
59
|
+
4. ctx-rail init (run in project root)
|
|
60
|
+
→ Auto-detects project from .context-rail file, workspace binding, or state.json
|
|
61
|
+
→ Creates .context-rail file if a project resolves
|
|
62
|
+
→ Adds .context-rail to .gitignore (if .gitignore exists)
|
|
57
63
|
```
|
|
58
64
|
|
|
59
65
|
### Verifying setup
|
|
60
66
|
|
|
61
67
|
```
|
|
62
68
|
orient(workdir="/path/to/repo")
|
|
63
|
-
→ Should show: "Login: ✓ bound
|
|
64
|
-
→ If shows "⚠
|
|
69
|
+
→ Should show: "Login: ✓ bound via .context-rail"
|
|
70
|
+
→ If shows "⚠ unbound" → run ctx-rail init or project_set_active with workdir
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
##
|
|
73
|
+
## Project Resolution (how agents find the right project)
|
|
68
74
|
|
|
69
|
-
Context Rail resolves the active project from the agent's current working directory
|
|
75
|
+
Context Rail resolves the active project from the agent's current working directory
|
|
76
|
+
using a 3-tier approach:
|
|
70
77
|
|
|
71
|
-
| Method | How it works |
|
|
72
|
-
|
|
73
|
-
| `
|
|
74
|
-
|
|
|
75
|
-
|
|
|
78
|
+
| Tier | Method | How it works | Automatic? |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| 1 | `.context-rail` file | File in cwd (or parent) containing project_id | Created on `ctx-rail init`, `project_create`, `project_set_active` |
|
|
81
|
+
| 2 | Workspace binding | Global mapping in `workspace_bindings.json` | Created when workdir is passed to `project_set_active` |
|
|
82
|
+
| 3 | State fallback | `state.json`'s `active_project_id` | Legacy — only used when tiers 1 & 2 miss |
|
|
76
83
|
|
|
77
|
-
All
|
|
84
|
+
All tools use `resolve_pid_for_tool(workdir, agent_id)` which checks all three tiers
|
|
85
|
+
in order. After one `ctx-rail init`, you never need `project_set_active` again —
|
|
86
|
+
the `.context-rail` file handles it automatically.
|
|
78
87
|
|
|
79
88
|
## Loading Skills
|
|
80
89
|
|
|
@@ -208,8 +217,9 @@ project_login(project_id="...", agent_id="...", workdir="/path/to/repo")
|
|
|
208
217
|
|
|
209
218
|
### "No active project" on tools that should work
|
|
210
219
|
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
1. Run `ctx-rail init` in the project root to create `.context-rail`
|
|
221
|
+
2. Pass `workdir="/path/to/repo"` explicitly
|
|
222
|
+
3. Call `project_set_active(project_id="...", workdir="/path/to/repo")` to bind the directory
|
|
213
223
|
2. Call `project_set_active(workdir="/path/to/repo")` first
|
|
214
224
|
3. Report the tool name — it may still need the workspace binding fix
|
|
215
225
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|