feat(plugin): a directory says which project it belongs to, git repo or not (#4085)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 47s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m1s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 47s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m1s
CI & Build / Build & push image (push) Skipped
All six hooks scoped their requests one way: `git remote get-url origin`,
resolved server-side through the repo bindings. That key does not exist
outside a git repo, so a session in a plain directory was unscoped in every
hook at once — no project context, no prior-art scoping, no project rules —
and silently, because a missing remote is indistinguishable from a remote
nobody bound.
A `.scribe` file is the second key, read by the shared scribe_scope_query so a
directory scopes the same way everywhere:
{"instance": "https://scribe.example.com", "project_id": 2, "project": "…"}
`instance` is why the file is not just a number: an id is a different project
on every Scribe, so a marker that travels — a copied directory, a shared
machine, a repo someone else clones — would otherwise scope the session to the
wrong project without a word. Compared host-only, and a mismatch drops the id:
no project beats the wrong project. A bare integer is accepted too, since it
is what a person writes by hand. The marker beats a git remote — someone put
the file there on purpose — which is also how a directory overrides its
binding.
Two things it found on the way:
* An explicit project_id that did not resolve rendered NO message at all —
the branch hung off `if project_id` as an `elif`, so a caller holding a
pointer it believed in got a context that silently omitted the project it
had asked for. Now reported.
* The refusal reason was a global set inside a function every caller reads
through `$( )`. The assignment died with the subshell, leaving the caller
to read an unset variable under `set -u` — which aborts the hook and costs
the whole session's SessionStart context, to fetch a warning about a file.
It comes back through stdout with the id instead, and a test pins it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -64,8 +64,11 @@ async def session_context():
|
||||
resolves it to the bound project (see services/repo_bindings); an
|
||||
unbound repo yields a "bind this repo" hint instead. This is how the
|
||||
active project is determined — the plugin never pins a project id.
|
||||
project_id (optional int) — explicit override, mainly for manual/ad-hoc
|
||||
curl testing; takes precedence over `repo` when set.
|
||||
project_id (optional int) — the project named directly, and the only
|
||||
key a caller outside a git repo has (#4085): the plugin's hooks
|
||||
send it when a `.scribe` marker file names a project. Takes
|
||||
precedence over `repo`. Access-checked like any other read — an id
|
||||
this account cannot read loads no project rather than failing.
|
||||
"""
|
||||
project_id, _repo, unbound_repo = await _project_scope()
|
||||
result = await plugin_ctx_svc.build_session_context(
|
||||
|
||||
@@ -2171,7 +2171,10 @@ async def build_session_context(
|
||||
Args:
|
||||
user_id: the operator.
|
||||
project_id: the resolved active project (0 = none). The endpoint
|
||||
resolves this from the working repo's remote, not from config.
|
||||
resolves this from the working repo's remote, or from a `.scribe`
|
||||
marker file naming the project directly — the only key a session
|
||||
outside a git repo has (#4085). A non-zero id that does not
|
||||
resolve is reported rather than silently dropped.
|
||||
unbound_repo: when the hook sent a repo remote that maps to no project,
|
||||
its normalized key — triggers a one-line "bind this repo" hint so
|
||||
the binding is self-healing.
|
||||
@@ -2241,18 +2244,34 @@ async def build_session_context(
|
||||
f"`get_design_system({design['id']})` → "
|
||||
f"`resolved_guidance`.",
|
||||
]
|
||||
elif unbound_repo:
|
||||
lines += [
|
||||
"",
|
||||
"## Repository not yet bound",
|
||||
f"This repo (`{unbound_repo}`) isn't mapped to a Scribe project, so "
|
||||
"no project context was loaded. Bind it once with "
|
||||
f'`bind_repo(repo_url="{unbound_repo}", project_id=<id>)` '
|
||||
"(call `list_projects` to find the id) and future sessions here will "
|
||||
"auto-load that project's context.",
|
||||
]
|
||||
else:
|
||||
lines += ["", "No Scribe project is bound to this working directory."]
|
||||
# Nothing loaded — say which nothing (#4085). This used to hang off the
|
||||
# `if project_id:` above as an `elif`, which meant an id that was SENT and
|
||||
# did not resolve produced no message at all: the outer branch was taken,
|
||||
# the inner one was not, and the caller got a context that simply omitted
|
||||
# the project it had asked for. That is the one case worth being loudest
|
||||
# about, because the caller is holding a pointer it believes in.
|
||||
if project_dict is None:
|
||||
if project_id:
|
||||
lines += [
|
||||
"",
|
||||
f"## Project {project_id} could not be loaded",
|
||||
f"This session asked for project {project_id}, but this account "
|
||||
"cannot read it — the id may belong to a different Scribe "
|
||||
"instance, or the project may have been deleted. "
|
||||
"`list_projects` shows what is readable here.",
|
||||
]
|
||||
elif unbound_repo:
|
||||
lines += [
|
||||
"",
|
||||
"## Repository not yet bound",
|
||||
f"This repo (`{unbound_repo}`) isn't mapped to a Scribe project, so "
|
||||
"no project context was loaded. Bind it once with "
|
||||
f'`bind_repo(repo_url="{unbound_repo}", project_id=<id>)` '
|
||||
"(call `list_projects` to find the id) and future sessions here will "
|
||||
"auto-load that project's context.",
|
||||
]
|
||||
else:
|
||||
lines += ["", "No Scribe project is bound to this working directory."]
|
||||
|
||||
context = "\n".join(line for line in lines if line is not None)
|
||||
if len(context) > _MAX_CHARS:
|
||||
|
||||
Reference in New Issue
Block a user