Skip to content

Commit 259bee3

Browse files
authored
feat: implement scope organization runtime for RFC #1345 (#1401)
* feat(scope): add durable organization and bindings * feat(migration): register existing runtime scopes * feat(artifact): publish exact revisions across scopes * refactor(scope): replace project reporting with selections * feat(integrations): resolve server-owned scopes * test(scope): verify selection and plugin contracts * docs(scope): explain server-owned organization * refactor(scope): remove legacy runtime migration * fix(scope): retry bindings without savepoints * fix(scope): reject unregistered runtime partitions * fix(scope): serialize hierarchy mutations * fix(codex): guard operations by scope mode * fix(publication): reject incomplete memory copies * fix(ci): escape plugin shell variable safely * fix(runtime): preserve referenced scope recall * fix(publication): make delivery atomic * docs: use registered scope identifiers
1 parent 4e0f78a commit 259bee3

337 files changed

Lines changed: 14550 additions & 26603 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ POWERCONTEXT_SERVER_AUTH_ENABLED=false
2727
# POWERCONTEXT_SERVER_AUTH_TOKEN=replace-me
2828

2929
# Dashboard -------------------------------------------------------------------
30-
# Every Coding Agent below uses this same Scope ID.
30+
# The Dashboard discovers Scopes from the Server. The Server creates a default Scope on first startup.
3131
POWERCONTEXT_SERVER_DASHBOARD_ENABLED=true
32-
POWERCONTEXT_SERVER_DASHBOARD_SCOPES='[{"scope_id":"project:quickstart","display_name":"Quick Start"}]'
3332

3433
# Logging, metrics, and tracing -----------------------------------------------
3534
POWERCONTEXT_SERVER_LOGGING_LEVEL=INFO
@@ -134,13 +133,14 @@ POWERCONTEXT_CLIENT_TIMEOUT=10
134133
# POWERCONTEXT_CLIENT_API_TOKEN=replace-me
135134

136135
# Coding Agent integrations ---------------------------------------------------
137-
# Keep these values equal to the Dashboard scope above. Load .env before starting the selected Agent.
138-
POWERCONTEXT_CODEX_SCOPE_ID=project:quickstart
139-
POWERCONTEXT_CLAUDE_SCOPE_ID=project:quickstart
140-
POWERCONTEXT_DSH_SCOPE_ID=project:quickstart
141-
POWERCONTEXT_OPENCODE_SCOPE_ID=project:quickstart
142-
POWERCONTEXT_PI_SCOPE_ID=project:quickstart
143-
POWERCONTEXT_LANGGRAPH_SCOPE_ID=project:quickstart
136+
# Load .env before starting the selected Agent. Codex binds each Session through the Scope service.
137+
# Set an integration's SCOPE_ID only to select an existing Scope explicitly.
138+
# POWERCONTEXT_CODEX_SCOPE_ID=scp_existing
139+
# POWERCONTEXT_CLAUDE_SCOPE_ID=scp_existing
140+
# POWERCONTEXT_DSH_SCOPE_ID=scp_existing
141+
# POWERCONTEXT_OPENCODE_SCOPE_ID=scp_existing
142+
# POWERCONTEXT_PI_SCOPE_ID=scp_existing
143+
# POWERCONTEXT_LANGGRAPH_SCOPE_ID=scp_existing
144144

145145
# Local Server endpoints used by integrations.
146146
POWERCONTEXT_CLAUDE_SERVER_URL=http://127.0.0.1:8000

.github/workflows/build-artifacts.yml

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -254,36 +254,29 @@ jobs:
254254
255255
root = Path(os.environ["PLUGIN_ROOT"])
256256
python_executable = f"python{os.environ['PYTHON_VERSION']}"
257+
plugin_root = "$" + "{PLUGIN_ROOT}"
257258
hooks_path = root / "hooks/hooks.json"
258259
hooks = json.loads(hooks_path.read_text())
259-
recall_hook = hooks["hooks"]["UserPromptSubmit"][0]["hooks"][0]
260-
expected = (
261-
'uv run --locked --quiet --project "${PLUGIN_ROOT}" '
262-
'python "${PLUGIN_ROOT}/hooks/recall.py"'
263-
)
264-
if recall_hook["command"] != expected:
265-
raise SystemExit("unexpected Codex recall command")
266-
recall_hook["command"] = (
267-
'PYTHONPATH="${PLUGIN_ROOT}/vendor" '
268-
+ python_executable
269-
+ ' "${PLUGIN_ROOT}/hooks/recall.py"'
270-
)
260+
hook_scripts = {
261+
"SessionStart": "session_binding.py",
262+
"PreToolUse": "bind_tools.py",
263+
"UserPromptSubmit": "recall.py",
264+
}
265+
for event, script in hook_scripts.items():
266+
hook = hooks["hooks"][event][0]["hooks"][0]
267+
expected = (
268+
f'uv run --frozen --quiet --project "{plugin_root}" '
269+
f'python "{plugin_root}/hooks/{script}"'
270+
)
271+
if hook["command"] != expected:
272+
raise SystemExit(f"unexpected Codex {event} command")
273+
hook["command"] = (
274+
'PYTHONPATH="${PLUGIN_ROOT}/vendor" '
275+
+ python_executable
276+
+ f' "{plugin_root}/hooks/{script}"'
277+
)
271278
hooks_path.write_text(json.dumps(hooks, indent=2) + "\n")
272279
273-
skill_path = root / "skills/project-context/SKILL.md"
274-
skill = skill_path.read_text()
275-
expected_skill = (
276-
'uv run --locked --quiet --project "$PLUGIN_ROOT" python '
277-
'"$PLUGIN_ROOT/scripts/project_scope.py" --cwd "$PWD"'
278-
)
279-
replacement_skill = (
280-
f'PYTHONPATH="$PLUGIN_ROOT/vendor" {python_executable} '
281-
'"$PLUGIN_ROOT/scripts/project_scope.py" --cwd "$PWD"'
282-
)
283-
if skill.count(expected_skill) != 1:
284-
raise SystemExit("unexpected project-context Skill command")
285-
skill_path.write_text(skill.replace(expected_skill, replacement_skill))
286-
287280
readme_path = root / "README.md"
288281
readme = readme_path.read_text()
289282
expected_readme = (
@@ -390,7 +383,7 @@ jobs:
390383
(
391384
cd "$plugin_root"
392385
PYTHONPATH="$plugin_root/vendor" \
393-
"python${PYTHON_VERSION}" scripts/project_scope.py --cwd "$GITHUB_WORKSPACE"
386+
"python${PYTHON_VERSION}" -m compileall -q hooks scripts
394387
)
395388
396389
- name: Create compressed artifact

.licenserc.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ header:
4343
- '**/LICENSE'
4444
- 'NOTICE'
4545
- '**/LICENSE.oceanbase-design'
46-
- '**/uv.lock'
46+
- '**/*.lock'
47+
- '**/*-lock.yaml'
48+
- '**/*.lockb'
4749
- 'benchmark/locomo/dataset/**'
4850
- 'benchmark/locomo/results/**'
4951
- 'tox.ini'

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ js-test: ## Install, build, and test the DeepSeek Harness plugin.
117117
@pnpm --dir integrations/dsh/plugins/powercontext test
118118
@pnpm --dir integrations/dsh/plugins/powercontext build
119119
@git diff --exit-code -- \
120-
integrations/dsh/plugins/powercontext/openapi/powercontext.yaml \
121120
integrations/dsh/plugins/powercontext/src/operations.generated.ts \
122121
integrations/dsh/plugins/powercontext/lib
123122
@pnpm --dir integrations/dsh/plugins/powercontext test

docs/en/development/server-web-ui.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,23 @@ versioned prefixes.
7272

7373
## Understand Dashboard data
7474

75-
The browser authenticates against `/dashboard/scopes`, then requests `/v1/stats` with the selected `scope_id` and a
76-
`30d` period. The Server reads one scoped snapshot and returns inventory, model usage, and recall statistics.
75+
The browser authenticates against `/dashboard/scopes`, builds the shared Scope selector, then posts the selected
76+
`ScopeSelection` and period to `/v1/stats`. The selector exposes three observation views: `all`, one root's `subtree`,
77+
or one `exact` Scope. A Parent relation organizes the selector; it does not make parent data visible to a child.
7778

7879
| Dashboard value | Source |
7980
| --- | --- |
80-
| Sources | Current scoped Source journal position |
81-
| Memory entries | Entries in the current Memory Artifact |
82-
| Artifacts | Current Artifact heads grouped by family |
83-
| Pending review | Current Candidate heads grouped by family and status |
81+
| Sources | Selected Scopes' Source journal positions |
82+
| Memory entries | Entries in the selected Scopes' Memory Artifacts |
83+
| Artifacts | Selected Scopes' Artifact heads grouped by family |
84+
| Pending review | Selected Scopes' Candidate heads grouped by family and status |
8485
| Skill origin | Immutable lineage for managed Skills; registration for external Skills |
8586
| Model usage | Persisted daily generation and embedding usage |
8687
| Recall hits, token reduction, and savings trend | Persisted daily recall measurements for the configured estimator |
8788

88-
The Runtime performs these reads in one database transaction and calculates totals, pending Sources, family counts,
89-
daily buckets, and token reduction on the Server. The browser presents `ready_preparations` as recall hits and plots the
89+
The Runtime resolves the selection to exact Scope IDs, aggregates totals, pending Sources, family counts, daily buckets,
90+
and token reduction on the Server, and returns both the selection and resolved IDs. The browser presents
91+
`ready_preparations` as recall hits and plots the
9092
signed daily `token_reduction` as the savings trend. Each heatmap cell combines those two fields for its date. Its fixed
9193
bands are no hit, hit without a positive reduction, 1–255, 256–1023, and 1024 or more estimated tokens reduced. The
9294
fixed thresholds keep sparse activity and outliers from changing the meaning of every other cell.
@@ -109,17 +111,17 @@ or rendering contract only after a second page needs the same behavior.
109111

110112
## Add the Handoff Report page
111113

112-
When Handoff Report is enabled, the Server hosts the scope Handoff page at `/handoff-reports` without requiring the scoped-statistics Dashboard or its configured scope list. The optional Dashboard remains at `/` when separately enabled. The pages share only `base.html`, the header and footer, `auth.js`, theme state, and locale state; their statistics and report calculations remain independent.
114+
When Handoff Report is enabled, the Server hosts a read-only report page at `/handoff-reports`; the Dashboard remains
115+
optional. Both pages load Scopes from `/dashboard/scopes` and use `scope-selection.js` to expose the same `all`,
116+
`subtree`, and `exact` views.
113117

114-
The Handoff Report page obtains exact `scope_id` values with committed Handoffs from `POST /v1/handoff-reports/scopes/list-known` and uses them in a searchable scope combobox. Selecting a scope sends its required `scope_id` to `POST /v1/handoff-reports/get`; neither the Project catalog nor `project_id` participates in report selection. The page presents the exact current Handoff snapshot at full width.
118+
The page posts the selected `ScopeSelection` to `/v1/handoff-reports/get`. The Server resolves it to exact Scope IDs
119+
and projects each Scope's descriptor and latest exact Handoff. A Scope without a committed Handoff remains visible as
120+
`no_handoff`. Parent does not infer Context sharing, and the report does not edit Handoff state.
115121

116-
The current snapshot displays objective, current state, disposition, next action, and known omissions as one Handoff document. One Edit action opens all five fields, and one Save Revision action prepares and commits the complete document as a new immutable Handoff Revision. Scope switching and background refresh pause while the editor is open. Receiver-side decisions are not part of this page; existing continuity records remain available in the read-only Continuity timeline. Apart from the explicit revision write, the browser formats returned `summary`, `coverage`, Workstream state, and digests without recalculating report semantics.
117-
118-
When known-scope discovery succeeds but no scope has a committed Handoff, the page replaces report controls with a clearly labeled, data-free template preview. Retry enumerates Handoff heads again; the first committed scope replaces the preview. The preview neither creates a Handoff nor requests fabricated report data.
119-
120-
The page requests the current day in UTC by default and provides current-day, ISO-week, calendar-month, and custom date-range inputs. The custom end date is inclusive in the UI and is converted to the exclusive start of the next day for the API. The current scope application normalizes this input but supplies no Activity events, reports `activity_coverage=not_configured`, and returns no period comparison. Handoff status comes from the current exact selection and must not be presented as a historical period-end state.
121-
122-
The overview request may disable evidence checks for lower latency. A Markdown download makes a separate request with `format=markdown`, `download=true`, and evidence checks enabled by default. The browser never reconstructs Markdown from rendered DOM or canonical JSON. Both background refresh and browser download currently require a stored bearer token even when Server authentication is disabled; initial and manual report loads still work without one. Disabling Handoff Report removes the `/handoff-reports` page and its API while leaving the original Dashboard route, scope selection, and statistics request unchanged.
122+
JSON is the browser projection. Markdown download repeats the same selection with `format=markdown` and
123+
`download=true`; the browser does not reconstruct Markdown from the rendered DOM. Disabling Handoff Report removes
124+
the page and report API without changing Dashboard selection or statistics behavior.
123125

124126
## Preserve the security boundary
125127

docs/en/docs/explanation/core-concepts.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ Agent turn. Every value belongs to a scope.
1111

1212
## Scope is the isolation boundary
1313

14-
Every content operation uses a `scope_id`. The scope selects an isolated Source journal, Memory lifecycle, Candidate
15-
inbox, Handoff history, and related runtime state. Integrations can derive a scope from a repository or path, while
16-
public interfaces also allow callers to supply one explicitly.
14+
Every content operation uses a `scope_id`. The Scope selects an isolated Source journal, Memory lifecycle, Candidate
15+
inbox, Handoff history, and related runtime state. Scope IDs are opaque Server identifiers. Integrations resolve an
16+
explicit Scope, a durable binding, or the Server default; repository, path, session, and Agent identities are binding
17+
inputs rather than Scope IDs.
1718

18-
A scope ID selects data. It does not prove user identity, grant tool access, or authorize execution.
19+
A Scope ID selects data. It does not prove user identity, grant tool access, or authorize execution.
1920

2021
## Sources preserve evidence
2122

@@ -89,9 +90,8 @@ inspected boundary and returns a temporary Prepared Handoff. Committing a Handof
8990
the user wants a milestone. The receiver resolves the Handoff and records an Acknowledgement; a Task Outcome preserves
9091
the final status and checks as Source evidence.
9192

92-
The [Handoff Report](../how-to/use-handoff-report.md) projects current Handoff Revisions for inspection and export. The
93-
current scope report does not yet include Activity events or period comparison, and it does not rewrite Memory or the
94-
underlying Handoff history.
93+
The [Handoff Report](../how-to/use-handoff-report.md) projects the latest Handoff Revision in each selected Scope for
94+
inspection and export. It is read-only and does not rewrite Memory or the underlying Handoff history.
9595

9696
## Interfaces expose different parts of the same Server
9797

docs/en/docs/how-to/configure-claude-code.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ PowerContext Server data.
5858

5959
For each user prompt, the Hook:
6060

61-
1. derives the same project scope as the Codex integration;
61+
1. resolves the current Scope from explicit, session, workspace, and default bindings;
6262
2. calls `POST /v1/context/prepare` at most once;
6363
3. strictly validates `powercontext.prepared-context.v1` and injects it unchanged through `additionalContext`;
6464
4. independently captures the prompt as ordinary Content Source evidence.
@@ -73,20 +73,19 @@ Skill.
7373
Scope resolution uses this order:
7474

7575
1. `POWERCONTEXT_CLAUDE_SCOPE_ID`, when explicitly set;
76-
2. the Git-private Workstream binding shared with Codex;
77-
3. the normalized `remote.origin.url` of the Git top-level directory;
78-
4. a `local:sha256:<digest>` identifier derived from the resolved project directory.
76+
2. a durable session binding stored by PowerContext;
77+
3. a durable workspace binding stored by PowerContext;
78+
4. the Server's default Scope.
7979

80-
Claude Code and Codex therefore resolve the exact same scope in a checkout that has a Workstream binding. Without a
81-
binding they still share the normalized remote scope. Bind a known Workstream with the bundled resolver:
80+
Bind a known Scope to the checkout with the bundled resolver:
8281

8382
```bash
84-
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/project_scope.py" \
85-
--cwd "$PWD" --bind-workstream "WORKSTREAM_SCOPE_ID"
83+
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/workspace_scope.py" \
84+
--cwd "$PWD" --bind-scope "SCOPE_ID"
8685
```
8786

88-
The local fallback is stable for one resolved directory, but it is not intended to join unrelated checkouts. Set an
89-
explicit scope only when that separation or sharing is deliberate.
87+
The resolver hashes the workspace path only as an external binding key. It never generates a Scope ID from a Git
88+
remote or directory. Set an explicit Scope only when that separation or sharing is deliberate.
9089

9190
## Use explicit Memory and Handoff operations
9291

@@ -122,8 +121,8 @@ export POWERCONTEXT_CLAUDE_CAPTURE_PROMPTS=false
122121
claude
123122
```
124123

125-
Use `POWERCONTEXT_CLAUDE_SCOPE_ID` only when the Memory scope must intentionally differ from both the Git remote and
126-
local project path.
124+
Use `POWERCONTEXT_CLAUDE_SCOPE_ID` only when the current work must intentionally override durable bindings and the
125+
Server default.
127126

128127
`POWERCONTEXT_CLAUDE_FLUSH_ON_CAPTURE=true` makes the Hook wait for Source processing and is intended for tests, not
129128
normal interactive use.

docs/en/docs/how-to/configure-codex.md

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,20 @@ handoff this work
3636
```
3737

3838
The `project-context` Skill treats that imperative as explicit authorization to create one durable Handoff milestone.
39-
If the catalog contains multiple Workstreams, Codex first opens a native picker; one Workstream is selected
40-
automatically when it is the only candidate. Codex binds the selected Workstream to the checkout, inspects the current
41-
conversation and repository, assembles the objective, branch and worktree state, changed files, observed checks,
42-
blockers, omissions, and next action, then calls `handoff_current_work` followed by `commit_handoff`. After a successful
43-
commit, Codex reports the selected Workstream and exact Handoff Revision; the user does not need to fill in the Handoff
44-
content or confirm the commit again.
39+
Codex inspects the current conversation and repository, assembles the objective, branch and worktree state, changed
40+
files, observed checks, blockers, omissions, and next action, then calls `handoff_current_work` followed by
41+
`commit_handoff` in the current Session Scope. After a successful commit, Codex reports the exact Handoff Revision; the
42+
user does not need to fill in the Handoff content or confirm the commit again.
4543

4644
`交接`, `交接当前工作`, and `commit a handoff` use the same behavior. To inspect the proposed content without writing,
4745
ask to `preview the handoff without committing`; the Skill renders the proposed fields in chat and calls no write
4846
tool. Discussing Handoff design or asking how it works does not authorize a write.
4947

50-
Codex resolves scope in this order: an explicit `POWERCONTEXT_CODEX_SCOPE_ID`, a Workstream scope persistently bound
51-
to the current Git workspace, the normalized Git remote, and finally the project path. Later Codex sessions in the
52-
same workspace reuse that scope.
53-
54-
The picker returns the Workstream's human-facing `work_id` and authoritative `scope_id`. The `project-context` Skill
55-
passes that exact scope to the resolver's `--bind-workstream` operation and verifies the result. The binding lives in
56-
`powercontext/codex-workspace.json` below the Git-private directory, outside the worktree and commits. A one-line
57-
Handoff then continues the selected Workstream's Artifact lifecycle and creates the next Revision. If the MCP client
58-
does not support native elicitation, the tool returns structured choices instead; the integration must still obtain an
59-
explicit selection and must not choose silently.
48+
At Session start, Codex resolves Scope in this order: an explicit `POWERCONTEXT_CODEX_SCOPE_ID`, an existing Session
49+
binding, a host-managed workspace binding, and the Server's default Scope. The selected Scope is fixed to the Session.
50+
Repository and directory identities are lookup inputs only; they never generate a Scope ID. The prompt hook uses the
51+
binding for recall and capture, while `PreToolUse` injects it into data-plane tools so Agent input cannot redirect a
52+
read or write. The host must create or bind a different Scope when the Session changes work boundaries.
6053

6154
The Hook calls `POST /v1/context/prepare` once before Codex analyzes the prompt. It requests an 8000-byte total budget,
6255
strictly validates `powercontext.prepared-context.v1`, and injects the returned content unchanged. The Runtime labels

docs/en/docs/how-to/configure-dsh.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ The plugin has two paths to the same Server:
3030
- before each model step it asks the Runtime to prepare one final, bounded context value, then independently captures the user's prompt as Source evidence;
3131
- named `pc_*` tools call the public HTTP API to remember, search, revise, retire, and audit Memory.
3232

33-
Memory scope comes from the normalized Git remote when one is available, or from the session workspace path otherwise. Set `POWERCONTEXT_DSH_SCOPE_ID` when the session has no workspace cwd, or when the scope must be independent of both. The plugin does not fall back to the Harness process directory.
33+
The plugin resolves one Server-owned Scope in this order: `POWERCONTEXT_DSH_SCOPE_ID`, a durable binding for the
34+
session workspace, then the Server default. The workspace path is hashed only as an external binding key. A missing
35+
workspace therefore uses the Server default instead of the Harness process directory.
3436

3537
The plugin calls `POST /v1/context/prepare` once before the model analyzes the prompt. Explicit `remember_memory` calls do not require a model.
3638

0 commit comments

Comments
 (0)