snipara-companion 3.2.26 → 3.2.28

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  Release notes for `snipara-companion`, newest first.
4
4
 
5
+ ## New In 3.2.28
6
+
7
+ - Adds bounded Outcome Loop retrieval correlation to generated Codex, Claude,
8
+ Cursor, VS Code/Continue, and generic HTTP MCP references through
9
+ `SNIPARA_SESSION_ID` / `X-Snipara-Session-Id`.
10
+ - Prints the active correlation session during `init` and documents the
11
+ explicit `correlation_context.session_id` fallback for MCP clients that
12
+ cannot inject environment-backed headers.
13
+ - Keeps the correlation identifier telemetry-only and project-scoped; it does
14
+ not alter authentication or authorization.
15
+
16
+ ## New In 3.2.27
17
+
18
+ - Scopes `context-control drift` dirty Git detection to the ProjectContext
19
+ manifest, declared manifest sources, local Decision Requests, and
20
+ `.snipara/context-control/` artifacts. Dirty files outside that scope remain
21
+ visible but no longer force permanent `DRIFT_DETECTED` in shared checkouts.
22
+ - Clarifies the Context as Code V0 boundary: manifests are local declarative
23
+ metadata and do not refresh hosted context, reconcile manifest-vs-hosted
24
+ state, or mutate hosted memory. Hosted refresh/apply remains V1 work.
25
+ - Adds executable documentation smoke tests that run the README and full
26
+ reference `context-control` examples against the built local CLI, closing the
27
+ loop that previously allowed narrative examples to drift from command reality.
28
+
5
29
  ## New In 3.2.26
6
30
 
7
31
  - Fixes the published `context-control plan` examples so they use the real CLI
package/README.md CHANGED
@@ -49,6 +49,18 @@ Use this after `create-snipara` activation. `create-snipara` remains the
49
49
  canonical engine for first workspace setup; Companion owns the repeatable local
50
50
  continuity loop after that.
51
51
 
52
+ ## Retrieval And Outcome Correlation
53
+
54
+ `snipara-companion init` creates and prints a bounded workspace session ID.
55
+ Export it as `SNIPARA_SESSION_ID` before starting Codex, Claude, Cursor, VS Code,
56
+ Continue, or another HTTP MCP client. Generated configs forward it as
57
+ `X-Snipara-Session-Id`, while canonical execution events use the same Companion
58
+ session automatically. Clients without environment-backed headers can pass the
59
+ same value as `correlation_context.session_id` on retrieval tools.
60
+
61
+ The identifier is opaque, project-scoped telemetry. It does not grant access or
62
+ change authorization, and explicit per-call correlation remains authoritative.
63
+
52
64
  Example output excerpt:
53
65
 
54
66
  ```text
@@ -99,7 +111,8 @@ These commands are useful without hosted Snipara:
99
111
  `context-control` is the local trust layer for Project Intelligence state. It
100
112
  borrows Terraform's useful product grammar without copying Terraform: preview a
101
113
  bounded context mutation, inspect drift, then apply only the exact reviewed
102
- plan.
114
+ plan. V0 is intentionally local-only: it creates trust artifacts for review, not
115
+ hosted context mutations.
103
116
 
104
117
  ```bash
105
118
  npx -y snipara-companion context-control plan \
@@ -123,13 +136,15 @@ locally:
123
136
  "path": "docs/architecture.md",
124
137
  "authority": "canonical",
125
138
  "tier": "HOT",
126
- "tags": ["architecture"]
139
+ "required": true,
140
+ "description": "Architecture context that agents should treat as canonical."
127
141
  }
128
142
  ],
129
143
  "policies": [
130
144
  {
131
145
  "id": "review-context-changes",
132
- "description": "Human review required before changing canonical context",
146
+ "scope": "memory.canonical",
147
+ "requirement": "Human review required before changing canonical context.",
133
148
  "reviewRequired": true
134
149
  }
135
150
  ]
@@ -142,7 +157,12 @@ npx -y snipara-companion context-control plan --manifest snipara.project-context
142
157
  ```
143
158
 
144
159
  The manifest is declarative metadata only. Validation and local reconciliation
145
- do not upload documents, approve memory, or mutate hosted Snipara state.
160
+ do not upload documents, approve memory, refresh hosted context, or mutate
161
+ hosted Snipara state. `context-control drift` scopes dirty Git signals to the
162
+ manifest, manifest sources, local Decision Requests, and `.snipara/context-control/`
163
+ artifacts so unrelated checkout noise does not become permanent drift. A future
164
+ V1 hosted refresh/apply surface should compare manifest state against hosted
165
+ context before allowing real hosted mutations.
146
166
 
147
167
  ### Local Worker Registry
148
168
 
package/dist/index.js CHANGED
@@ -5641,6 +5641,7 @@ Claude Code should apply this workflow automatically for project-specific work;
5641
5641
 
5642
5642
  - Bound Snipara project: \`${projectSlug}\`
5643
5643
  - Hosted MCP endpoint: \`${buildHostedMcpEndpoint(projectSlug)}\`
5644
+ - Keep \`SNIPARA_SESSION_ID\` equal to the active Companion session. Generated MCP configs send it as \`X-Snipara-Session-Id\`; when a client cannot send that header, pass the same value as \`correlation_context.session_id\` on retrieval tools.
5644
5645
  - At the start of substantial work, validate the hosted MCP surface with a tool-oriented call, then use \`snipara_recall\` and a targeted \`snipara_context_query\` before falling back to local search.
5645
5646
  - Do not treat empty MCP resources/templates as an outage. If the tool surface looks incomplete, call \`snipara_help(list_all=true)\` and compare exact tool names.
5646
5647
  - Use \`snipara_context_query\` for docs, business context, architecture notes, runbooks, and source truth. Use \`snipara_get_chunk\` for exact cited sections when references are returned.
@@ -5660,6 +5661,7 @@ function buildSharedWorkflowInstructionBlock(projectSlug, client) {
5660
5661
  This workspace is bound to Snipara project \`${projectSlug}\` for ${formatClientName(client)}. Agents should use Snipara automatically for project-specific context, decisions, and workflow state.
5661
5662
 
5662
5663
  - Hosted MCP endpoint: \`${buildHostedMcpEndpoint(projectSlug)}\`
5664
+ - Keep \`SNIPARA_SESSION_ID\` equal to the active Companion session. Generated MCP configs send it as \`X-Snipara-Session-Id\`; when a client cannot send that header, pass the same value as \`correlation_context.session_id\` on retrieval tools.
5663
5665
  - At the start of substantial work, validate the hosted MCP surface with a tool-oriented call, then use \`snipara_recall\` and a targeted \`snipara_context_query\` before falling back to local search.
5664
5666
  - Do not treat empty MCP resources/templates as an outage. If the tool surface looks incomplete, call \`snipara_help(list_all=true)\` and compare exact tool names.
5665
5667
  - Use \`snipara_context_query\` for docs, business context, architecture notes, runbooks, and source truth. Use \`snipara_get_chunk\` for exact cited sections when references are returned.
@@ -5911,6 +5913,7 @@ function generateCodexConfigString(projectSlug) {
5911
5913
  type = "streamable_http"
5912
5914
  url = "https://api.snipara.com/mcp/${projectSlug}"
5913
5915
  bearer_token_env_var = "SNIPARA_API_KEY"
5916
+ env_http_headers = { "X-Snipara-Session-Id" = "SNIPARA_SESSION_ID" }
5914
5917
  `;
5915
5918
  }
5916
5919
  function generateGenericMcpReferenceString(client, projectSlug) {
@@ -5920,7 +5923,8 @@ function generateGenericMcpReferenceString(client, projectSlug) {
5920
5923
  name: "snipara",
5921
5924
  url: `https://api.snipara.com/mcp/${projectSlug}`,
5922
5925
  headers: {
5923
- "X-API-Key": "${SNIPARA_API_KEY}"
5926
+ "X-API-Key": "${SNIPARA_API_KEY}",
5927
+ "X-Snipara-Session-Id": "${SNIPARA_SESSION_ID}"
5924
5928
  }
5925
5929
  },
5926
5930
  null,
@@ -5935,7 +5939,8 @@ function generateGenericMcpReferenceString(client, projectSlug) {
5935
5939
  type: "http",
5936
5940
  url: `https://api.snipara.com/mcp/${projectSlug}`,
5937
5941
  headers: {
5938
- "X-API-Key": "${SNIPARA_API_KEY}"
5942
+ "X-API-Key": "${SNIPARA_API_KEY}",
5943
+ "X-Snipara-Session-Id": "${SNIPARA_SESSION_ID}"
5939
5944
  }
5940
5945
  }
5941
5946
  }
@@ -5995,6 +6000,7 @@ async function callSniparaTool(name: string, args: Record<string, unknown>): Pro
5995
6000
  headers: {
5996
6001
  "Content-Type": "application/json",
5997
6002
  "X-API-Key": SNIPARA_API_KEY,
6003
+ "X-Snipara-Session-Id": process.env.SNIPARA_SESSION_ID || "",
5998
6004
  },
5999
6005
  body: JSON.stringify({
6000
6006
  jsonrpc: "2.0",
@@ -6561,12 +6567,13 @@ async function initCommand(options) {
6561
6567
  existingProjectId: existingConfig.projectId,
6562
6568
  selectedProject
6563
6569
  });
6570
+ const sessionId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
6564
6571
  saveConfig(
6565
6572
  {
6566
6573
  apiKey,
6567
6574
  apiUrl: existingConfig.apiUrl,
6568
6575
  projectId: projectIdentifier,
6569
- sessionId: `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
6576
+ sessionId,
6570
6577
  client: selectedClient
6571
6578
  },
6572
6579
  { cwd: projectDir, scope: "workspace" }
@@ -6688,6 +6695,10 @@ async function initCommand(options) {
6688
6695
  console.log(`Companion config: ${configPaths.companion}`);
6689
6696
  console.log(`Selected project: ${formatProjectChoice(selectedProject)}`);
6690
6697
  console.log(`Selected client: ${formatClientName(selectedClient)}`);
6698
+ console.log(`Correlation session: ${sessionId}`);
6699
+ console.log(
6700
+ "Export SNIPARA_SESSION_ID with this value before starting the MCP client so served context and execution outcomes share one bounded session."
6701
+ );
6691
6702
  if (options.withHooks) {
6692
6703
  console.log("\nNext steps:");
6693
6704
  if (hookClient === "cursor") {
@@ -33089,6 +33100,35 @@ function parseDirtyFile2(line) {
33089
33100
  const renameParts = withoutStatus.split(" -> ");
33090
33101
  return renameParts[renameParts.length - 1]?.replace(/^"|"$/g, "");
33091
33102
  }
33103
+ function resolveGitDriftScopePaths(cwd) {
33104
+ const scopePaths = [
33105
+ PROJECT_CONTEXT_MANIFEST_DEFAULT_PATH,
33106
+ CONTEXT_CONTROL_RELATIVE_DIR,
33107
+ path30.join(".snipara", "decisions")
33108
+ ];
33109
+ const manifestPath = resolveManifestPath(cwd);
33110
+ if (!fs30.existsSync(manifestPath)) {
33111
+ return normalizeScopePaths(scopePaths, cwd);
33112
+ }
33113
+ const manifestRead = readJsonFileSafe(manifestPath);
33114
+ if (!manifestRead.value) {
33115
+ return normalizeScopePaths(scopePaths, cwd);
33116
+ }
33117
+ const validation = validateProjectContextManifest({ manifest: manifestRead.value });
33118
+ const manifestSourcePaths = validation.manifest?.sources.map((source2) => source2.path) ?? [];
33119
+ return normalizeScopePaths([...scopePaths, ...manifestSourcePaths], cwd);
33120
+ }
33121
+ function normalizeScopePaths(scopePaths, cwd) {
33122
+ return uniqueStrings20(
33123
+ scopePaths.map((scopePath) => normalizeProjectRelativePath(scopePath, cwd).replace(/\/+$/g, ""))
33124
+ );
33125
+ }
33126
+ function isPathInsideScope(filePath, scopePaths) {
33127
+ const normalized = filePath.replace(/\\/g, "/").replace(/\/+$/g, "");
33128
+ return scopePaths.some(
33129
+ (scopePath) => normalized === scopePath || normalized.startsWith(`${scopePath}/`)
33130
+ );
33131
+ }
33092
33132
  function slugify2(value) {
33093
33133
  const slug2 = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
33094
33134
  return slug2 || "context-control-state";
@@ -33380,25 +33420,39 @@ function readJsonFileSafe(filePath) {
33380
33420
  }
33381
33421
  function collectGitDriftSignals(cwd) {
33382
33422
  const revision = resolveGitBaseRevision(cwd);
33423
+ const scopePaths = resolveGitDriftScopePaths(cwd);
33424
+ const relevantDirtyFiles = revision.dirtyFiles.filter(
33425
+ (file) => isPathInsideScope(file, scopePaths)
33426
+ );
33383
33427
  const upstream = runGit3(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd);
33384
33428
  const upstreamCounts = upstream ? runGit3(["rev-list", "--left-right", "--count", `${upstream}...HEAD`], cwd) : void 0;
33385
33429
  const [behind = 0, ahead = 0] = upstreamCounts ? upstreamCounts.split(/\s+/g).map((part) => Number.parseInt(part, 10) || 0) : [];
33386
- const dirtySignal = revision.dirty ? {
33387
- id: "git-working-tree-dirty",
33430
+ const dirtySignal = revision.dirty ? relevantDirtyFiles.length > 0 ? {
33431
+ id: "git-working-tree-dirty-relevant",
33388
33432
  surface: "git",
33389
33433
  state: "DRIFT_DETECTED",
33390
- summary: "Working tree has local uncommitted changes.",
33391
- expected: "Clean working tree for release-grade context-control reconciliation.",
33392
- observed: revision.dirtyFiles.join(", "),
33393
- refs: revision.dirtyFiles,
33434
+ summary: "Working tree has uncommitted changes in context-control or ProjectContext scope.",
33435
+ expected: "Manifest, manifest sources, Decision Requests, and context-control artifacts are clean.",
33436
+ observed: relevantDirtyFiles.join(", "),
33437
+ refs: relevantDirtyFiles,
33394
33438
  severity: "watch",
33395
- reasonCodes: ["git_working_tree_dirty"]
33439
+ reasonCodes: ["git_working_tree_dirty_relevant"]
33440
+ } : {
33441
+ id: "git-working-tree-dirty-out-of-scope",
33442
+ surface: "git",
33443
+ state: "IN_SYNC",
33444
+ summary: "Working tree has uncommitted changes, but none are in context-control or ProjectContext scope.",
33445
+ expected: `Only scoped dirty paths affect context-control drift: ${scopePaths.join(", ")}.`,
33446
+ observed: `${revision.dirtyFiles.length} out-of-scope dirty file(s)`,
33447
+ refs: revision.dirtyFiles,
33448
+ severity: "info",
33449
+ reasonCodes: ["git_working_tree_dirty_out_of_scope"]
33396
33450
  } : {
33397
33451
  id: "git-working-tree-clean",
33398
33452
  surface: "git",
33399
33453
  state: "IN_SYNC",
33400
- summary: "Working tree is clean.",
33401
- refs: [],
33454
+ summary: "Working tree has no uncommitted changes.",
33455
+ refs: scopePaths,
33402
33456
  severity: "info",
33403
33457
  reasonCodes: ["git_working_tree_clean"]
33404
33458
  };
@@ -33645,6 +33699,8 @@ function buildLocalProjectDriftReport(options = {}) {
33645
33699
  ],
33646
33700
  caveats: [
33647
33701
  "Project Drift V0 is a read-only local report.",
33702
+ "Git dirty-file drift is scoped to the ProjectContext manifest, manifest sources, Decision Requests, and .snipara/context-control artifacts; use git status for full working-tree release readiness.",
33703
+ "Project Drift V0 validates manifest health locally but does not refresh hosted context state; hosted manifest-vs-context reconciliation is V1 work.",
33648
33704
  "UNKNOWN is never treated as IN_SYNC; verify missing or unreadable evidence before applying changes."
33649
33705
  ]
33650
33706
  });
@@ -339,13 +339,18 @@ snipara-companion workflow resume --include-session-context
339
339
  Git bases by default, writes only under `.snipara/context-control/`, and emits
340
340
  an apply receipt linked to the plan hash.
341
341
  - `context-control drift` is a read-only Project Drift V0 report. It checks Git
342
- working tree/upstream state, managed workflow state, pending Decision
343
- Requests, saved plans and receipts, and ProjectContext manifest health.
344
- `UNKNOWN` is never treated as `IN_SYNC`.
342
+ upstream state, scoped Git dirty files, managed workflow state, pending
343
+ Decision Requests, saved plans and receipts, and ProjectContext manifest
344
+ health. Dirty Git files only become drift when they touch the ProjectContext
345
+ manifest, manifest sources, local Decision Requests, or
346
+ `.snipara/context-control/`; unrelated checkout noise remains visible without
347
+ classifying the project as `DRIFT_DETECTED`. `UNKNOWN` is never treated as
348
+ `IN_SYNC`.
345
349
  - `context-control validate --manifest snipara.project-context.json` validates
346
350
  Context as Code V0. The manifest is JSON metadata declaring context sources,
347
- tiers, authority, tags, owners, freshness, and review policies. It does not
348
- upload content or mutate hosted Snipara state.
351
+ tiers, authority, freshness, and review policies. It does not upload content,
352
+ refresh hosted context, reconcile manifest-vs-hosted state, or mutate hosted
353
+ Snipara state; hosted refresh/apply belongs to the future V1 surface.
349
354
  - `lead-plan` turns local workflow state, Team Sync, file scope, context refs,
350
355
  proof gates, and acceptance criteria into an advisory Engineering Lead Plan.
351
356
  It emits worker recommendations and handoff contracts, keeps
@@ -816,6 +821,15 @@ Mistral generates MCP-first files (`MISTRAL.md`, Vibe config, Le Chat connector
816
821
  reference, and LangChain `ChatMistralAI.bindTools` snippets); Mistral request
817
822
  hooks are model request hooks, not local agent lifecycle hooks.
818
823
 
824
+ All generated HTTP/SSE references include a bounded correlation header sourced
825
+ from `SNIPARA_SESSION_ID`. Set it to the session printed by `init` before
826
+ starting Codex, Claude, Cursor, VS Code/Continue, GLM, or another generic MCP
827
+ host. The hosted server uses the header only for project-scoped retrieval
828
+ telemetry and ignores invalid values. If the host cannot inject environment
829
+ headers, pass the same value as `correlation_context.session_id` on
830
+ `snipara_context_query`, `snipara_recall`, `snipara_search`, `snipara_ask`, and
831
+ `snipara_get_chunk`.
832
+
819
833
  OpenClaw hooks remain separate:
820
834
 
821
835
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.2.26",
3
+ "version": "3.2.28",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {