Skip to content

Shared Agent Context

Shared Agent Context (SAC) is the shipped local-first entry point for a piece of work: a file-backed workspace, a bounded Facts / Work / Know-how (FWK) view, and a reviewed proposal path that promotes a completed session into wiki, memory, or skills. It stores workspace-relative references, not copies of those owners, and it never becomes a second wiki, Flow tracker, or session store.

The domain lives in src/sac/. Operators use keryx workspace. Editor agents use MCP sac.* on local stdio only. A live keryx shell turn can read FWK through workspace_overview / workspace_read.

What is still experimental

The learned candidate policy is disabled by default (enabled: false, killSwitch: true) and only synthetic fixtures exist. Phase 6b re-ingestion of real receipts/outcomes is not shipped. Core registry, FWK reads, propose/review, owner writers, and the access-receipt ledger are shipped (phases 0–5 and 6a, on main since v0.2.32, present in 0.2.35).

Contracts and schemas: docs/requirements/shared-agent-context/. This page describes current behavior. Where it disagrees with a future/planned sentence in an older requirements revision or in satellite RP-01…RP-12 packages, this page and src/sac/ win.

The FWK model

SAC never blurs these three kinds:

  • Facts — evidence-linked, task-local, freshness-bound statements. A Fact never silently becomes long-term knowledge.
  • Work — a read-only projection of an existing Flow (done, next, blocked, verification evidence). SAC never creates a second tracker. Unbound work is explicit when no Flow is linked.
  • Know-how — accepted wiki, memory, or skill items. Raw transcripts and hidden reasoning are not Know-how. Graph is not a knowledge owner.

Create a workspace and register resources

Work from a project cwd that already has .metaproject/. Identity is the local OS user (user:local-<uid>). There is no --actor flag; adapters pass request: undefined. There is no session↔workspace auto-bind: every later call takes an explicit workspaceId.

Discover the full surface with keryx workspace --help. keryx commands intentionally omits this verb (Phase 1 local-CLI only).

keryx workspace --help
keryx workspace create --title "Payments retry" --component ./src/payments
keryx workspace list
keryx workspace add-resource <workspace-id> --kind evidence --uri ./src/payments/retry.ts
keryx workspace add-resource <workspace-id> --kind flow --uri ./.metaproject/flows/<flow>/flow.json
keryx workspace show <workspace-id>

--component and --uri must be workspace-relative (./…). Absolute, network, and escaping paths are rejected. add-resource takes --kind, --uri, and optional --revision — not a positional typed-ref.

Read a bounded overview, then the detail

keryx workspace overview <workspace-id> --max-items 20 --max-tokens 2000
keryx workspace read <workspace-id> <item-id>
keryx workspace overview <workspace-id> --explain   # human text on stderr; JSON stays on stdout

Defaults: overview maxItems=32 / maxTokens=4096; read maxItems=1 / maxTokens=4096. If a mandatory item cannot fit the budget the operation returns typed context_overflow and no access receipt. Optional omissions are listed. Domain denials (freshness: "denied") are ordinary JSON, not a tool/CLI crash.

Each allowed or denied progressive access appends a metadata-only receipt to .metaproject/context-operations/access-receipts.jsonl (hash-chained; src/sac/receipt-integrity.ts). Receipts do not store retrieved content, prompts, or hidden reasoning. FWK results are derived response objects — there is no persisted fwk-receipt.json.

Propose knowledge on wrap-up, and review it

Propose only from a completed keryx session (≥ archived messages). Kinds: decision, wiki-update, memory-entry, follow-up, contract-change, risk. --note is a one-line sidecar beside the proposal; it is not evidence and is outside the frozen schema (known unbound field).

keryx workspace propose <workspace-id> --kind wiki-update \
  --session <session-id> \
  --note "Retry uses capped exponential backoff"
keryx workspace review <workspace-id> <proposal-id> --decision accepted \
  --reason "owner accepted" \
  --idempotency-key <key>

Review decisions: accepted | rejected | dismissed. Accept is possible only through the harness composition (createHarnessProposalLifecycleService): it wires real owner writers. The local factory is fail-closed and cannot self-accept. --decision accepted additionally requires --confirm-token, minted only by keryx workspace confirm-review <workspace-id> <proposal-id> run yourself in a real, approval-gated shell. No MCP tool or agent-native tool mints one. That is friction, not proof. The approval prompt is triggered by the command's text, so a differently spelled command avoids it. The token store is a plain file, so a shell-capable agent can forge it. An agent outside keryx supervision is not gated at all. See TM-03 for the full list. Accepting a wiki-update or memory-entry proposal also returns a DedupHint (duplicates/conflicts against already-accepted entries, computed after the decision, never gating it) and, when the hint is non-empty, an optional model-judge annotation — both informational only; nothing reads .verdict to decide accept/reject. sac.review (MCP) and keryx workspace review (CLI) return the identical shape.

Kind Owner Lands under
wiki-update wiki .metaproject/wiki/decisions/
memory-entry memory .metaproject/memory/ (same guarded seam as keryx memory new)
decision, follow-up, contract-change, risk skill .metaproject/project-skills/sac/<proposal-id>/

A failed owner write becomes stale, never accepted. The same --idempotency-key replays the original terminal event. Flow wrap-up as a propose source is not wired — only source: "session".

When the proposal trips the security scanner

confirm-review reads the proposal's security gate before it mints anything. On a clean proposal it mints and says nothing further. When the gate is needs-approval it prints what the scan found and in which evidence, and then refuses:

keryx workspace confirm-review <workspace-id> <proposal-id>
# → security findings in this proposal's evidence:
#   … findings and the evidence refs they came from …
# Read the evidence, then re-run with --acknowledge-security to state that you have.

keryx workspace confirm-review <workspace-id> <proposal-id> --acknowledge-security

The flag is the record that a human read the findings, and the minted token carries that fact forward: review --decision accepted refuses a token that does not. A clean proposal claims no acknowledgement even if the flag is passed, and a proposal whose gate cannot be read is refused outright rather than assumed to have passed — an unreadable gate is not a passing one.

The history is worth stating, because two releases got this wrong in opposite directions. Before 0.2.75 no flag existed and the acceptance path passed no acknowledgement at all, so a needs-approval proposal could not be accepted by any route — a dead end, but a visible one. 0.2.74 replaced that by passing the acknowledgement unconditionally on every call, which made the refusal unfirable while its error text still promised "explicit human acknowledgement": a silent bypass, the worse of the two. 0.2.75 made the acknowledgement real.

The same acknowledgement is available inside keryx shell. The /review modal's Detail tab lists a proposal's actions as [a] accept, [s] accept with the security acknowledgement, and [d] decline — each armed by its key and then confirmed with [y]. [s] mints through the very same confirm-review --acknowledge-security path, and it is never an automatic fallback for [a]: with no acknowledgement handler wired, [s] reports itself unavailable instead of downgrading to a plain accept.

Collaboration is read-only on the shipped adapters:

keryx workspace collaboration <workspace-id>

CollaborationService.record has no production CLI/MCP caller. Collaboration and proposal lifecycle both append activity.jsonl with incompatible event shapes — a mixed file can make collaboration overview fail.

Catch up on what accumulated during unattended runs

keryx workspace catch-up [--workspace <workspace-id>] [--json]

A pull-based, cwd-scoped digest: pending proposals, blocked runs, unbound-candidate wrap-ups, and sessions of genuinely unknown fate — always four separate sections, never merged. It also includes a fifth, --include-lifecycle-flags section (shown by default) — every workspace, memory entry, and wiki decision page whose recorded module no longer resolves in the code graph, reusing the graph-diff signal that already drives orphaned-wiki-page pruning. Report-only: it never archives a workspace, edits a memory entry, or removes a wiki page on its own; a workspace can carry both a pending proposal and a lifecycle flag at once.

Agent surfaces (MCP and shell)

MCP tools (src/mcp/tools.ts), local stdio / in-process only. HTTP returns { code: "sac_transport_denied" } before workspace discovery:

Tool Mutating
sac.overview no
sac.read no
sac.collaboration no
sac.propose yes
sac.review yes

Harness tools on a local keryx shell agent turn. They are not on keryx serve / chat-only mode. Only the parent turn has them — a dispatched subagent never gets workspace resolve/create/propose access.

Tool Risk Same service as
workspace_overview read keryx workspace overview
workspace_read read keryx workspace read
workspace_list read keryx workspace list
workspace_show read keryx workspace show
workspace_create mutating keryx workspace create

CLI and MCP reads share normalizeFwkResult. Never-shipped names (workspace.fwk, workspace.get, workspace.proposal create --from-flow) are not commands.

On-disk layout

.metaproject/workspaces/<workspace-id>/
  workspace.json
  proposals/<id>.json
  proposals/<id>.<hash>.{decision,approval,write-intent,write-result}.json
  activity.jsonl
  session-evidence/<sessionId>.md

.metaproject/context-operations/
  access-receipts.jsonl
  access-receipts.checkpoint.json

workspace.json is the only SAC primary record. Knowledge bodies stay in wiki / memory / project-skills.

Advanced: phase-6 runtime opt-in policy (off by default)

By default SAC uses a deterministic baseline policy. An optional learned candidate sits behind resolvePolicySelection. It activates only when an explicit pinned config and a complete integrity chain succeed; any failure is fail-closed back to the baseline.

Config path: .metaproject/context-operations/policy-experiment/config.json.

{
  "enabled": false,
  "killSwitch": true,
  "candidateArtifactRef": "./fixtures/sac-policy-experiment/artifacts/candidate.json",
  "candidateArtifactDigest": "<sha256>",
  "candidateVersion": "<immutable-version>",
  "baselineArtifactRef": "./fixtures/sac-policy-experiment/artifacts/deterministic-baseline.json",
  "baselineArtifactDigest": "<sha256>",
  "baselineVersion": "<immutable-version>",
  "corpusRef": "./fixtures/sac-policy-experiment/corpus.json",
  "corpusDigest": "<sha256>",
  "corpusVersion": "<immutable-version>",
  "evaluationReportRef": "./fixtures/sac-policy-experiment/evaluation-report.json",
  "evaluationDigest": "<sha256>"
}

Rules the guard enforces:

  • Off by default. The candidate is selected only when enabled is true and killSwitch is false; a missing config keeps the baseline.
  • Explicit pins. Every artifact needs a workspace-relative ref, a matching sha256 digest and an immutable version; latest/main/head-style versions are rejected.
  • Fixed-order integrity chain. Baseline → candidate → corpus → evaluation report → deterministic activation. Any parse error, digest mismatch, schema failure or activation mismatch falls back to the baseline.
  • Rollback. Rolling back forces enabled: false and killSwitch: true.

Before flipping enabled: true, verify the chain (validates pins even while disabled; exits non-zero when integrityReady is false):

keryx workspace policy-readiness

This path does not change any public CLI or MCP schema and never enables the candidate implicitly. The operator process for real (non-synthetic) artifacts is in the Phase 6b operator playbook; runtime re-ingestion of raw receipts/outcomes is still planned — see phase-6-real-opt-in-readiness.md.

Autonomous binding on an action-intent turn (Slate v2)

An action-intent turn in keryx shell/TUI/harness run — or an explicit /goal <text> [--workspace <id>] [--auto [N]] (harness run --goal ... [--workspace <id>] non-interactively) — now resolves-or-creates its workspace binding automatically, by the same tool-calling judgment ask_user/spawn_subagent already use (no new similarity/embedding engine), and re-evaluates that binding mid-session if the topic shifts. On completion it dispatches its own wrap-up proposal without a manual propose call. This is still source: "session" under the hood, not a new propose source — see the Flow-wrap-up note above. Review/accept is unaffected: still strictly human, still gated by confirm-review.

Not shipped (do not treat as current)

  • Propose from a Flow wrap-up snapshot.
  • SAC over MCP HTTP or keryx serve.
  • Public collaboration writer / member / archive APIs.
  • Phase 6b real-data re-ingestion.
  • Satellite RP-01…RP-12 capabilities.

Source reads of workspace files require POSIX openat + O_NOFOLLOW (macOS / Linux). There is no Win32 fallback for that path.