granttap-mcp 0.8.20 → 0.8.22

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.
Files changed (45) hide show
  1. package/README.md +23 -1
  2. package/apps/bridge/src/bin/runtime/monitor.ts +9 -0
  3. package/apps/bridge/src/capabilities/README.md +5 -0
  4. package/apps/bridge/src/cortex/integration.ts +10 -7
  5. package/apps/bridge/src/engine/protocol/engine-memory-protocol.ts +3 -0
  6. package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +10 -0
  7. package/apps/bridge/src/engine/protocol/transport/result-parser.ts +50 -0
  8. package/apps/bridge/src/engine/runtime/engine-memory.ts +56 -34
  9. package/apps/bridge/src/engine/runtime/engine-projects.ts +17 -23
  10. package/apps/bridge/src/engine/runtime/repository-graph/fit.ts +86 -0
  11. package/apps/bridge/src/mesh/catalog/project/observations.ts +7 -0
  12. package/apps/bridge/src/mesh/context/env.ts +32 -2
  13. package/apps/bridge/src/mesh/context/project.ts +13 -2
  14. package/apps/bridge/src/mesh/knowledge/README.md +14 -0
  15. package/apps/bridge/src/mesh/knowledge/active.ts +64 -0
  16. package/apps/bridge/src/mesh/knowledge/write.ts +45 -0
  17. package/apps/bridge/src/mesh/runtime/catalog/session-catalog.ts +23 -0
  18. package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +22 -10
  19. package/apps/bridge/src/mesh/runtime/index.ts +19 -7
  20. package/apps/bridge/src/mesh/snapshot/scoped-view.ts +5 -1
  21. package/apps/bridge/src/mesh/store/index.ts +10 -1
  22. package/apps/bridge/src/monitor/controller-knowledge/index.ts +51 -0
  23. package/apps/bridge/src/monitor/handlers/inbound.ts +37 -12
  24. package/apps/bridge/src/monitor/support/runtime.ts +1 -1
  25. package/apps/bridge/src/pairing/controller-invites.ts +92 -0
  26. package/apps/bridge/src/reply/README.md +6 -0
  27. package/apps/bridge/src/reply/process/index.ts +20 -3
  28. package/apps/mcp/src/connection-center/widget.html +1 -1
  29. package/cursor-plugin/.cursor-plugin/plugin.json +1 -1
  30. package/cursor-plugin/README.md +5 -5
  31. package/cursor-plugin/mcp.json +1 -1
  32. package/cursor-plugin/stdio-bootstrap.cjs +1 -1
  33. package/docs/cursor-authorize.md +2 -2
  34. package/docs/images/iphone-health-code-towers.png +0 -0
  35. package/docs/images/iphone-linked-projects.png +0 -0
  36. package/docs/images/iphone-weavatrix-graph.png +0 -0
  37. package/package.json +2 -2
  38. package/packages/core/relay-client.ts +21 -0
  39. package/packages/protocol/messages/README.md +6 -0
  40. package/packages/protocol/messages/device/controller-pairing.ts +32 -0
  41. package/packages/protocol/messages/mesh/graph/code-map.ts +44 -0
  42. package/packages/protocol/messages/mesh/knowledge/index.ts +1 -0
  43. package/packages/protocol/messages/mesh/knowledge/write.ts +27 -0
  44. package/packages/protocol/messages/mesh/snapshot.ts +8 -0
  45. package/packages/protocol/schema.ts +8 -0
package/README.md CHANGED
@@ -80,12 +80,34 @@ separate from their path identity. Missing content evidence remains unknown.
80
80
 
81
81
  <p align="center">
82
82
  <img src="docs/images/iphone-projects-shared.png" width="200" alt="Projects: one owned by this phone, one shared by another">
83
+ <img src="docs/images/iphone-linked-projects.png" width="200" alt="Two linked Projects with separate access scopes">
83
84
  <img src="docs/images/iphone-project-mesh.png" width="200" alt="A Project: Governance, members and computers, Mesh status, repositories, Tasks">
84
85
  <img src="docs/images/iphone-task-route.png" width="200" alt="A Task: executions, Runtime history, and resource claims">
85
86
  <img src="docs/images/iphone-handoff.png" width="200" alt="Task handoff: destination, push, and readiness checks">
86
87
  </p>
87
88
 
88
- <p align="center"><em>Projects, one of them shared by another phone · a Project with its Governance, members, computers, repositories, and Tasks · a Task with executions, Runtime history, and claims · a handoff with its readiness checks</em></p>
89
+ <p align="center"><em>Shared Projects · linked repository bindings with separate Project access · Project management · Task execution history · handoff readiness</em></p>
90
+
91
+ <p align="center">
92
+ <img src="docs/images/iphone-weavatrix-graph.png" width="200" alt="Full-screen architecture graph with sample evidence">
93
+ <img src="docs/images/iphone-health-code-towers.png" width="200" alt="Full-screen Health code towers with sample evidence">
94
+ </p>
95
+
96
+ <p align="center"><em>These are deterministic simulator fixtures, not reports from a live customer repository.</em></p>
97
+
98
+ An authenticated controller can request a separate, one-time device credential
99
+ for another iPhone or iPad from each Live linked computer. The phone packages
100
+ the resulting links in an encrypted, single-use QR. This adds the new phone to
101
+ the same device network; it does not grant Project membership. Explicit human
102
+ decisions for a bound Project Task can be recorded through Engine Memory, and
103
+ a same-scope correction supersedes an earlier decision while retaining history.
104
+ The computer returns `recorded` only after the Engine confirms storage.
105
+
106
+ The capability catalog distinguishes local observations from installed and
107
+ initialized state. It does not yet transfer and apply complete MCP or skill
108
+ bundles to every host. Usage figures do not enforce spending caps. Project
109
+ Environment secrets injected into an agent process remain readable by that
110
+ process; a use-only credential broker is not part of this runtime yet.
89
111
 
90
112
  For Claude Code, Codex, and Cursor, the provider hook runs inside the agent's
91
113
  own session and sees the exact call. GrantTap attributes every `notify` to the
@@ -3,6 +3,8 @@ import { join } from "node:path";
3
3
  import { RelayClient } from "../../../../../packages/core/relay-client";
4
4
  import { configDir, loadConfig, machineConfigPath } from "../../config";
5
5
  import { EngineSupervisor } from "../../engine/runtime/engine-supervisor";
6
+ import { meshSnapshots } from "../../mesh/runtime";
7
+ import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
6
8
  import { startSessionMonitor } from "../../monitor";
7
9
  import { controllerPeerGate } from "../../pairing/controllers";
8
10
 
@@ -38,6 +40,13 @@ process.on("SIGINT", stop);
38
40
  process.on("SIGTERM", stop);
39
41
 
40
42
  void engine.ensureAvailable().then((health) => {
43
+ if (health.state === "healthy") {
44
+ // A new Engine binary starts with its own registry. Re-admit existing,
45
+ // locally verified Git bindings so linked repositories have a graph again.
46
+ void recoverLocalGraphBindings(meshSnapshots())
47
+ .then(() => monitor.publish())
48
+ .catch(() => undefined);
49
+ }
41
50
  if (health.state !== "disabled" && health.state !== "healthy") {
42
51
  const reason = health.reason ? `: ${health.reason}` : "";
43
52
  process.stderr.write(
@@ -2,4 +2,9 @@
2
2
 
3
3
  This module discovers provider-configured MCP servers and available skills. Metadata probing is isolated because it may start an MCP transport; local descriptors and skill discovery remain read-only.
4
4
 
5
+ Skill digests cover the bounded bundle, including scripts and references.
6
+ Catalog observations and Project requests do not install a skill or native MCP
7
+ configuration on another host. Availability requires a separate native apply
8
+ and independently observed initialization; that rollout is still unfinished.
9
+
5
10
  License: this module is distributed under the MIT License in the repository-root `LICENSE` file.
@@ -14,6 +14,7 @@ import type { ScopedMeshView } from "../mesh/snapshot/scoped-view";
14
14
  import { loadRuntimeConfig } from "../config/runtime";
15
15
  import { computerId } from "../mesh/identity/computer";
16
16
  import { defaultCortexConfig } from "./config";
17
+ import { activeProjectKnowledge, activeScopedKnowledge, unrecordedKnowledgeEvents } from "../mesh/knowledge/active";
17
18
 
18
19
  type CortexDependencies = {
19
20
  provenance?: typeof projectEngineProvenance;
@@ -109,15 +110,16 @@ export function cortexSnapshotEvidence(
109
110
  `repository.${graph.repositoryId}`, "weavatrix.repository", JSON.stringify(graph),
110
111
  "high", "verified", "graph", graph.revision,
111
112
  ));
112
- const knowledge = (snapshot.knowledge ?? []).filter((record) =>
113
+ const knowledge = activeProjectKnowledge(snapshot.projectId, snapshot.knowledge ?? []).filter((record) =>
113
114
  record.taskId === taskId || record.visibility === "project").slice(0, 16);
114
115
  for (const record of knowledge) evidence.push(item(
115
116
  `knowledge.${record.recordId}`, "weavatrix.memory", JSON.stringify(record),
116
117
  "high", "unverified", "memory", `memory:${record.streamVersion}`,
117
118
  ));
118
- const recorded = new Set(knowledge.map((record) => record.sourceRef));
119
- for (const event of snapshot.events.filter((value) => value.taskId === taskId
120
- && !recorded.has(value.eventId)).slice(-32)) {
119
+ for (const event of unrecordedKnowledgeEvents(
120
+ snapshot.events.filter((value) => value.taskId === taskId),
121
+ snapshot.knowledge ?? [], snapshot.supersededKnowledgeRecordIds,
122
+ ).slice(-32)) {
121
123
  evidence.push(item(
122
124
  `event.${event.eventId}`, "mesh.event", JSON.stringify(event),
123
125
  "normal", "unverified", "memory", `event:${event.createdAt}`,
@@ -145,13 +147,14 @@ export function cortexScopedEvidence(view: ScopedMeshView): EngineContextEvidenc
145
147
  `repository.${graph.repositoryId}`, "weavatrix.repository", JSON.stringify(graph),
146
148
  "high", "verified", "graph", graph.revision,
147
149
  ));
148
- const knowledge = (view.knowledge ?? []).slice(0, 16);
150
+ const knowledge = activeScopedKnowledge(view.project.projectId, view.knowledge ?? [], taskId).slice(0, 16);
149
151
  for (const record of knowledge) evidence.push(item(
150
152
  `knowledge.${record.recordId}`, "weavatrix.memory", JSON.stringify(record),
151
153
  "high", "unverified", "memory", `memory:${record.streamVersion}`,
152
154
  ));
153
- const recorded = new Set(knowledge.map((record) => record.sourceRef));
154
- for (const event of view.events.filter((value) => !recorded.has(value.eventId)).slice(-32)) evidence.push(item(
155
+ for (const event of unrecordedKnowledgeEvents(
156
+ view.events, view.knowledge ?? [], view.supersededKnowledgeRecordIds,
157
+ ).slice(-32)) evidence.push(item(
155
158
  `event.${event.eventId}`, "mesh.event", JSON.stringify(event),
156
159
  "normal", "unverified", "memory", `event:${event.createdAt}`,
157
160
  ));
@@ -12,6 +12,7 @@ export type KnowledgeRecordInput = {
12
12
  visibility: KnowledgeVisibility;
13
13
  repository_id?: string | null;
14
14
  commit_sha?: string | null;
15
+ supersedes_record_id?: string | null;
15
16
  recorded_at: number;
16
17
  };
17
18
 
@@ -28,6 +29,7 @@ export type MemoryEngineOperation =
28
29
  | { operation: "memory.history"; input: {
29
30
  project_id: string; task_id?: string | null; visibility?: KnowledgeVisibility | null;
30
31
  before_version?: number | null; limit?: number | null;
32
+ include_superseded?: boolean;
31
33
  } };
32
34
  export type MemoryEngineResult =
33
35
  | { operation: "memory.recorded"; record_id: string; stream_version: number }
@@ -62,6 +64,7 @@ export function parseMemoryResult(result: Record<string, unknown>, invalid: () =
62
64
  if (entry.repository_id != null) bounded(entry.repository_id, 512, invalid);
63
65
  if (entry.commit_sha != null && (typeof entry.commit_sha !== "string"
64
66
  || !/^[0-9a-f]{7,64}$/i.test(entry.commit_sha))) invalid();
67
+ if (entry.supersedes_record_id != null) bounded(entry.supersedes_record_id, 128, invalid);
65
68
  integer(entry.recorded_at, invalid);
66
69
  integer(entry.stream_version, invalid);
67
70
  }
@@ -45,6 +45,16 @@ export type EngineRepositoryGraph = {
45
45
  status: "SUPPORTED" | "CANDIDATE" | "CONTRADICTED" | "INSUFFICIENT_EVIDENCE";
46
46
  evidence: string[]; contradictions: string[]; unknowns: string[];
47
47
  }>;
48
+ code_map?: {
49
+ files: Array<{ path: string; language: string | null; line_count: number | null;
50
+ symbols: Array<{ id: string; label: string; kind: string;
51
+ start_line: number; line_count: number }> }>;
52
+ externals?: Array<{ id: string; label: string; kind: string }>;
53
+ roads: Array<{ source: string; target: string; relation: string }>;
54
+ total_files: number;
55
+ total_externals?: number;
56
+ truncated: boolean;
57
+ } | null;
48
58
  nodes: Array<{ id: string; kind: string; label: string }>;
49
59
  relations: Array<{ source: string; target: string; relation: string; evidence_count?: number }>;
50
60
  total_nodes: number;
@@ -148,6 +148,7 @@ function parseRepositoryGraph(value: unknown): void {
148
148
  }
149
149
  }
150
150
  }
151
+ if (graph.code_map != null) parseCodeMap(graph.code_map);
151
152
  if (!Array.isArray(graph.nodes) || graph.nodes.length > 256
152
153
  || !Array.isArray(graph.relations) || graph.relations.length > 512
153
154
  || !Number.isSafeInteger(graph.total_nodes) || Number(graph.total_nodes) < 0
@@ -170,6 +171,55 @@ function parseRepositoryGraph(value: unknown): void {
170
171
  }
171
172
  }
172
173
 
174
+ function parseCodeMap(value: unknown): void {
175
+ const map = requireObject(value, "repository code map");
176
+ if (!Array.isArray(map.files) || map.files.length > 650
177
+ || (map.externals != null && (!Array.isArray(map.externals) || map.externals.length > 24))
178
+ || !Array.isArray(map.roads) || map.roads.length > 1_200
179
+ || !Number.isSafeInteger(map.total_files) || Number(map.total_files) < map.files.length
180
+ || (map.total_externals != null && !Number.isSafeInteger(map.total_externals))
181
+ || Number(map.total_externals ?? 0) < (Array.isArray(map.externals) ? map.externals.length : 0)
182
+ || typeof map.truncated !== "boolean") invalidResult();
183
+ const paths = new Set<string>();
184
+ for (const value of map.files) {
185
+ const file = requireObject(value, "repository code file");
186
+ requireBoundedString(file.path, "code path", 512);
187
+ const path = file.path as string;
188
+ if (path.startsWith("/") || path.split("/").includes("..") || paths.has(path)) invalidResult();
189
+ paths.add(path);
190
+ requireOptionalString(file.language, "code language", 64);
191
+ if (file.line_count != null
192
+ && (!Number.isSafeInteger(file.line_count) || Number(file.line_count) < 1)) invalidResult();
193
+ if (!Array.isArray(file.symbols) || file.symbols.length > 72) invalidResult();
194
+ for (const value of file.symbols) {
195
+ const symbol = requireObject(value, "code symbol");
196
+ requireBoundedString(symbol.id, "code symbol id", 512);
197
+ requireBoundedString(symbol.label, "code symbol label", 160);
198
+ requireBoundedString(symbol.kind, "code symbol kind", 64);
199
+ if (!Number.isSafeInteger(symbol.start_line) || Number(symbol.start_line) < 1
200
+ || !Number.isSafeInteger(symbol.line_count) || Number(symbol.line_count) < 1) invalidResult();
201
+ }
202
+ }
203
+ for (const value of (map.externals ?? []) as unknown[]) {
204
+ const external = requireObject(value, "repository external resource");
205
+ requireBoundedString(external.id, "external id", 512);
206
+ requireBoundedString(external.label, "external label", 160);
207
+ requireBoundedString(external.kind, "external kind", 64);
208
+ const id = external.id as string;
209
+ if (!id.startsWith("ext:") || id.length < 5 || paths.has(id)) invalidResult();
210
+ paths.add(id);
211
+ }
212
+ for (const value of map.roads) {
213
+ const road = requireObject(value, "code road");
214
+ requireBoundedString(road.source, "road source", 512);
215
+ requireBoundedString(road.target, "road target", 512);
216
+ const source = road.source as string;
217
+ const target = road.target as string;
218
+ requireBoundedString(road.relation, "road relation", 64);
219
+ if (!paths.has(source) || !paths.has(target)) invalidResult();
220
+ }
221
+ }
222
+
173
223
  function parseBackbone(value: unknown): void {
174
224
  const backbone = requireObject(value, "project backbone");
175
225
  requireBoundedString(backbone.project_id, "project_id", 128);
@@ -7,10 +7,11 @@ import { EngineClient } from "./engine-client";
7
7
  import { engineFeatureEnabled, type EngineClientLike } from "./engine-supervisor";
8
8
 
9
9
  type Options = { env?: NodeJS.ProcessEnv; client?: EngineClientLike };
10
+ type KnowledgeSourceWindow = Pick<MeshSnapshot, "projectId" | "events">;
10
11
  let sharedClient: EngineClient | undefined;
11
12
  const synced = new Set<string>();
12
13
  const pendingSyncs = new Map<string, Promise<void>>();
13
- const nextIndex = new Map<string, number>();
14
+ const queuedSyncs = new Map<string, { snapshot: KnowledgeSourceWindow; options: Options }>();
14
15
 
15
16
  function client(options: Options): EngineClientLike {
16
17
  return options.client ?? (sharedClient ??= new EngineClient({
@@ -34,23 +35,36 @@ export async function projectKnowledge(
34
35
  ): Promise<ProjectKnowledgeRecord[] | undefined> {
35
36
  if (!engineFeatureEnabled(options.env ?? process.env)) return undefined;
36
37
  try {
37
- const result = await client(options).request({
38
- operation: "memory.history",
39
- input: { project_id: projectId, task_id: taskId,
40
- visibility: taskId ? undefined : "project", limit: 32 },
41
- }, { timeoutMs: 250 });
42
- if (result.operation !== "memory.history" || result.page.project_id !== projectId) return undefined;
43
- return result.page.entries.filter((entry) => taskId
44
- ? entry.task_id === taskId || entry.visibility === "project"
45
- : entry.visibility === "project").map((entry) => ({
46
- projectId: entry.project_id, taskId: entry.task_id,
47
- recordId: entry.record_id, category: entry.category,
48
- content: entry.content, source: entry.source,
49
- sourceRef: entry.source_ref, visibility: entry.visibility,
50
- repositoryId: entry.repository_id ?? undefined,
51
- commitSha: entry.commit_sha ?? undefined,
52
- recordedAt: entry.recorded_at, streamVersion: entry.stream_version,
53
- }));
38
+ const records: ProjectKnowledgeRecord[] = [];
39
+ let beforeVersion: number | undefined;
40
+ // The local Mesh cache holds at most 256 records. Read that same bounded
41
+ // window, including corrections, before deciding which entries are active.
42
+ for (let page = 0; page < 4; page += 1) {
43
+ const result = await client(options).request({
44
+ operation: "memory.history",
45
+ input: { project_id: projectId, task_id: taskId,
46
+ visibility: taskId ? undefined : "project", limit: 64,
47
+ include_superseded: true, before_version: beforeVersion },
48
+ }, { timeoutMs: 250 });
49
+ if (result.operation !== "memory.history" || result.page.project_id !== projectId) return undefined;
50
+ records.push(...result.page.entries.filter((entry) => taskId
51
+ ? entry.task_id === taskId || entry.visibility === "project"
52
+ : entry.visibility === "project").map((entry) => ({
53
+ projectId: entry.project_id, taskId: entry.task_id,
54
+ recordId: entry.record_id, category: entry.category,
55
+ content: entry.content, source: entry.source,
56
+ sourceRef: entry.source_ref, visibility: entry.visibility,
57
+ repositoryId: entry.repository_id ?? undefined,
58
+ commitSha: entry.commit_sha ?? undefined,
59
+ supersedesRecordId: entry.supersedes_record_id ?? undefined,
60
+ recordedAt: entry.recorded_at, streamVersion: entry.stream_version,
61
+ })));
62
+ const next = result.page.next_before_version;
63
+ if (!result.page.incomplete || next == null
64
+ || beforeVersion != null && next >= beforeVersion) break;
65
+ beforeVersion = next;
66
+ }
67
+ return records;
54
68
  } catch { return undefined; }
55
69
  }
56
70
 
@@ -86,31 +100,39 @@ export function recordsFromEvent(event: MeshEvent): KnowledgeRecordInput[] {
86
100
  return [];
87
101
  }
88
102
 
89
- /** Backfill the bounded event window once per process, then append only new IDs. */
90
- export async function syncProjectKnowledge(snapshot: MeshSnapshot, options: Options = {}): Promise<void> {
103
+ /** Backfill retained structured events; a failed Engine write stops this pass. */
104
+ export async function syncProjectKnowledge(snapshot: KnowledgeSourceWindow, options: Options = {}): Promise<void> {
91
105
  if (!engineFeatureEnabled(options.env ?? process.env)) return;
92
106
  const candidates = snapshot.events.flatMap(recordsFromEvent)
93
- .filter((entry) => !synced.has(`${entry.project_id}\0${entry.record_id}`));
107
+ .filter((entry) => entry.project_id === snapshot.projectId
108
+ && !synced.has(`${entry.project_id}\0${entry.record_id}`))
109
+ .slice(-512);
94
110
  if (candidates.length === 0) return;
95
- const start = (nextIndex.get(snapshot.projectId) ?? 0) % candidates.length;
96
- const count = Math.min(candidates.length, 16);
97
- nextIndex.set(snapshot.projectId, start + count);
98
- for (let offset = 0; offset < count; offset += 1) {
99
- const entry = candidates[(start + offset) % candidates.length]!;
100
- const key = `${entry.project_id}\0${entry.record_id}`;
101
- if (await recordProjectKnowledge(entry, options)) {
102
- synced.add(key);
103
- if (synced.size > 4_096) synced.delete(synced.values().next().value!);
111
+ for (const [index, entry] of candidates.entries()) {
112
+ if (index > 0 && index % 16 === 0) {
113
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
104
114
  }
115
+ const key = `${entry.project_id}\0${entry.record_id}`;
116
+ if (!await recordProjectKnowledge(entry, options)) break;
117
+ synced.add(key);
118
+ if (synced.size > 4_096) synced.delete(synced.values().next().value!);
105
119
  }
106
120
  }
107
121
 
108
122
  /** Publish never waits for the memory backfill; one bounded writer runs per Project. */
109
- export function queueProjectKnowledgeSync(snapshot: MeshSnapshot): void {
110
- if (pendingSyncs.has(snapshot.projectId)) return;
111
- const pending = syncProjectKnowledge(snapshot);
123
+ export function queueProjectKnowledgeSync(snapshot: KnowledgeSourceWindow, options: Options = {}): void {
124
+ if (pendingSyncs.has(snapshot.projectId)) {
125
+ queuedSyncs.set(snapshot.projectId, { snapshot, options });
126
+ return;
127
+ }
128
+ const pending = syncProjectKnowledge(snapshot, options);
112
129
  pendingSyncs.set(snapshot.projectId, pending);
113
130
  void pending.catch(() => undefined).finally(() => {
114
- if (pendingSyncs.get(snapshot.projectId) === pending) pendingSyncs.delete(snapshot.projectId);
131
+ if (pendingSyncs.get(snapshot.projectId) !== pending) return;
132
+ pendingSyncs.delete(snapshot.projectId);
133
+ const next = queuedSyncs.get(snapshot.projectId);
134
+ if (!next) return;
135
+ queuedSyncs.delete(snapshot.projectId);
136
+ queueProjectKnowledgeSync(next.snapshot, next.options);
115
137
  });
116
138
  }
@@ -15,6 +15,7 @@ import type {
15
15
  EngineContextEvidence,
16
16
  } from "../protocol/engine-protocol";
17
17
  import { RepositoryGraphJobs } from "./repository-graph-jobs";
18
+ import { fitRepositoryGraph } from "./repository-graph/fit";
18
19
 
19
20
  export type LocalProjectBinding = {
20
21
  summary: ProjectBindingSummary;
@@ -26,8 +27,8 @@ export type LocalProjectBinding = {
26
27
 
27
28
  let sharedClient: EngineClient | undefined;
28
29
  const pendingBindingSyncs = new Map<string, Promise<boolean>>();
29
- const MAX_REPOSITORY_GRAPH_WIRE_BYTES = 128 * 1_024;
30
- const MAX_SINGLE_GRAPH_WIRE_BYTES = 48 * 1_024;
30
+ const MAX_REPOSITORY_GRAPH_WIRE_BYTES = 256 * 1_024;
31
+ const MAX_SINGLE_GRAPH_WIRE_BYTES = 192 * 1_024;
31
32
  const repositoryGraphJobs = new RepositoryGraphJobs<ProjectRepositoryGraph>();
32
33
 
33
34
  export async function syncProjectBinding(
@@ -211,6 +212,20 @@ async function analyzeRepositoryGraph(
211
212
  analysisId: graph.analysis_id ?? undefined,
212
213
  analysisStatus: graph.analysis_status,
213
214
  architectureHypotheses: graph.architecture_hypotheses,
215
+ codeMap: graph.code_map ? {
216
+ files: graph.code_map.files.map((file) => ({
217
+ path: file.path, language: file.language, lineCount: file.line_count,
218
+ symbols: file.symbols.map((symbol) => ({
219
+ id: symbol.id, label: symbol.label, kind: symbol.kind,
220
+ startLine: symbol.start_line, lineCount: symbol.line_count,
221
+ })),
222
+ })),
223
+ externals: graph.code_map.externals ?? [],
224
+ roads: graph.code_map.roads,
225
+ totalFiles: graph.code_map.total_files,
226
+ totalExternals: graph.code_map.total_externals ?? 0,
227
+ truncated: graph.code_map.truncated,
228
+ } : undefined,
214
229
  nodes: graph.nodes,
215
230
  relations: graph.relations.map((edge) => ({
216
231
  source: edge.source, target: edge.target, relation: edge.relation,
@@ -249,27 +264,6 @@ function boundRepositoryGraphs(graphs: ProjectRepositoryGraph[]): ProjectReposit
249
264
  return result;
250
265
  }
251
266
 
252
- function fitRepositoryGraph(
253
- graph: ProjectRepositoryGraph,
254
- maxBytes: number,
255
- ): ProjectRepositoryGraph | undefined {
256
- let nodes = graph.nodes;
257
- let relations = graph.relations;
258
- let bounded = graph;
259
- while (Buffer.byteLength(JSON.stringify(bounded), "utf8") > maxBytes
260
- && (nodes.length > 8 || relations.length > 8)) {
261
- if (relations.length >= nodes.length && relations.length > 8) {
262
- relations = relations.slice(0, Math.max(8, Math.floor(relations.length / 2)));
263
- } else if (nodes.length > 8) {
264
- nodes = nodes.slice(0, Math.max(8, Math.floor(nodes.length / 2)));
265
- const ids = new Set(nodes.map((node) => node.id));
266
- relations = relations.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
267
- }
268
- bounded = { ...graph, nodes, relations, truncated: true };
269
- }
270
- return Buffer.byteLength(JSON.stringify(bounded), "utf8") <= maxBytes ? bounded : undefined;
271
- }
272
-
273
267
  function defaultClient(): EngineClient {
274
268
  sharedClient ??= new EngineClient({ socketPath: join(configDir(), "engine.sock") });
275
269
  return sharedClient;
@@ -0,0 +1,86 @@
1
+ import type { ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
2
+
3
+ type CodeMap = NonNullable<ProjectRepositoryGraph["codeMap"]>;
4
+ type Relation = ProjectRepositoryGraph["relations"][number];
5
+
6
+ /** Keep a navigable code city and the strongest architecture evidence in the snapshot envelope. */
7
+ export function fitRepositoryGraph(
8
+ graph: ProjectRepositoryGraph,
9
+ maxBytes: number,
10
+ ): ProjectRepositoryGraph | undefined {
11
+ let bounded = graph;
12
+ const bytes = () => Buffer.byteLength(JSON.stringify(bounded), "utf8");
13
+ if (bytes() <= maxBytes) return bounded;
14
+ if (bounded.relations.length > 128) {
15
+ bounded = { ...bounded, relations: strongestRelations(bounded.relations, 128), truncated: true };
16
+ }
17
+ while (bytes() > maxBytes) {
18
+ const map = bounded.codeMap;
19
+ if (map?.files.some((file) => file.symbols.length > 0)) {
20
+ bounded = { ...bounded, codeMap: trimSymbols(map) };
21
+ } else if (bounded.relations.length > 8) {
22
+ bounded = { ...bounded, relations: strongestRelations(bounded.relations,
23
+ Math.ceil(bounded.relations.length / 2)), truncated: true };
24
+ } else if (bounded.nodes.length > 8) {
25
+ const nodes = bounded.nodes.slice(0, Math.ceil(bounded.nodes.length / 2));
26
+ const ids = new Set(nodes.map((node) => node.id));
27
+ bounded = { ...bounded, nodes, relations: bounded.relations.filter((edge) =>
28
+ ids.has(edge.source) && ids.has(edge.target)), truncated: true };
29
+ } else if (map && map.roads.length > 16) {
30
+ bounded = { ...bounded, codeMap: { ...map, truncated: true,
31
+ roads: map.roads.slice(0, Math.ceil(map.roads.length / 2)) } };
32
+ } else if (map && map.files.length > 8) {
33
+ bounded = { ...bounded, codeMap: trimFiles(map) };
34
+ } else if (map) {
35
+ bounded = { ...bounded, codeMap: undefined };
36
+ } else {
37
+ return undefined;
38
+ }
39
+ }
40
+ return bounded;
41
+ }
42
+
43
+ function strongestRelations(relations: Relation[], limit: number): Relation[] {
44
+ return relations.map((edge, index) => ({ edge, index }))
45
+ .sort((left, right) => (right.edge.evidenceCount ?? 0) - (left.edge.evidenceCount ?? 0)
46
+ || left.index - right.index)
47
+ .slice(0, limit).sort((left, right) => left.index - right.index)
48
+ .map(({ edge }) => edge);
49
+ }
50
+
51
+ function roadDegree(map: CodeMap): Map<string, number> {
52
+ const degree = new Map<string, number>();
53
+ for (const road of map.roads) {
54
+ degree.set(road.source, (degree.get(road.source) ?? 0) + 1);
55
+ degree.set(road.target, (degree.get(road.target) ?? 0) + 1);
56
+ }
57
+ return degree;
58
+ }
59
+
60
+ function rankedFiles(map: CodeMap): CodeMap["files"] {
61
+ const degree = roadDegree(map);
62
+ return [...map.files].sort((left, right) =>
63
+ (degree.get(right.path) ?? 0) - (degree.get(left.path) ?? 0)
64
+ || (right.lineCount ?? 0) - (left.lineCount ?? 0)
65
+ || left.path.localeCompare(right.path));
66
+ }
67
+
68
+ function trimSymbols(map: CodeMap): CodeMap {
69
+ const ranked = rankedFiles(map).filter((file) => file.symbols.length > 0);
70
+ const keep = new Set(ranked.slice(0, Math.floor(ranked.length / 2)).map((file) => file.path));
71
+ return { ...map, truncated: true, files: map.files.map((file) => {
72
+ if (!keep.has(file.path)) return { ...file, symbols: [] };
73
+ if (ranked.length === 1) return { ...file, symbols: file.symbols.slice(0,
74
+ Math.floor(file.symbols.length / 2)) };
75
+ return file;
76
+ }) };
77
+ }
78
+
79
+ function trimFiles(map: CodeMap): CodeMap {
80
+ const files = rankedFiles(map).slice(0, Math.ceil(map.files.length / 2))
81
+ .sort((left, right) => left.path.localeCompare(right.path));
82
+ const paths = new Set(files.map((file) => file.path));
83
+ for (const external of map.externals ?? []) paths.add(external.id);
84
+ return { ...map, files, truncated: true,
85
+ roads: map.roads.filter((road) => paths.has(road.source) && paths.has(road.target)) };
86
+ }
@@ -55,6 +55,13 @@ function mcpObservation(
55
55
  if (candidates.length === 0) return { state: "not_found" };
56
56
  const digests = new Set(candidates.map((server) => server.configDigest).filter(Boolean));
57
57
  const artifactDigest = digests.size === 1 ? [...digests][0] : undefined;
58
+ if (request.artifactDigest) {
59
+ if (candidates.some((server) => server.configDigest
60
+ && server.configDigest !== request.artifactDigest)) {
61
+ return { state: "version_conflict", artifactDigest };
62
+ }
63
+ if (candidates.some((server) => !server.configDigest)) return { state: "unsupported" };
64
+ }
58
65
  if (!request.artifactDigest) {
59
66
  if (digests.size > 1) return { state: "version_conflict" };
60
67
  if (digests.size === 1 && candidates.some((server) => !server.configDigest)) {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import type { ProjectEnvironment, ProjectEnvVar } from "../../../../../packages/protocol/schema";
4
4
  import { ProjectEnvironment as EnvironmentSchema } from "../../../../../packages/protocol/schema";
@@ -10,6 +10,7 @@ type StoreFile = { environments: ProjectEnvironment[] };
10
10
  const PROTECTED_KEYS = new Set([
11
11
  "HOME", "PATH", "SHELL", "USER", "LOGNAME", "TMPDIR", "NODE_OPTIONS",
12
12
  ]);
13
+ const MANAGED_HEADER = "# GrantTap Project environment. Secrets stay on the Mesh, not here.";
13
14
 
14
15
  function protectedRuntimeKey(key: string): boolean {
15
16
  return PROTECTED_KEYS.has(key)
@@ -76,6 +77,7 @@ export function rememberEnvironment(
76
77
  ): ProjectEnvironment | undefined {
77
78
  const others = loadAll().filter((item) => item.projectId !== projectId);
78
79
  if (!environment) {
80
+ if (repositoryRoot) removeManagedRepoEnv(repositoryRoot);
79
81
  saveAll(others);
80
82
  return undefined;
81
83
  }
@@ -83,6 +85,8 @@ export function rememberEnvironment(
83
85
  const stored = mergeEnvironment({ ...environment, projectId }, loadEnvironment(projectId));
84
86
  if (stored.shareNonSecretsWithRepo && repositoryRoot) {
85
87
  writeRepoEnv(repositoryRoot, stored.variables);
88
+ } else if (repositoryRoot) {
89
+ removeManagedRepoEnv(repositoryRoot);
86
90
  }
87
91
  saveAll([...others, stored]);
88
92
  return stored;
@@ -120,13 +124,39 @@ export function writeRepoEnv(root: string, variables: ProjectEnvVar[]): void {
120
124
  .map((item) => `${item.key}=${escapeEnv(item.value ?? "")}`);
121
125
  const path = repoEnvPath(root);
122
126
  mkdirSync(dirname(path), { recursive: true });
127
+ if (!lstatSync(dirname(path)).isDirectory()) throw new Error("repository export directory is unsafe");
128
+ const stat = lstatSync(path, { throwIfNoEntry: false });
129
+ if (stat) {
130
+ if (!stat.isFile() || !readFileSync(path, "utf8").startsWith(`${MANAGED_HEADER}\n`)) {
131
+ throw new Error("repository environment file is not managed by GrantTap");
132
+ }
133
+ }
123
134
  writeFileSync(
124
135
  path,
125
- `${["# GrantTap Project environment. Secrets stay on the Mesh, not here.", ...lines].join("\n")}\n`,
136
+ `${[MANAGED_HEADER, ...lines].join("\n")}\n`,
126
137
  { mode: 0o644 },
127
138
  );
128
139
  }
129
140
 
141
+ /** Revoking a share removes a managed export and reports unsafe leftovers. */
142
+ export function removeManagedRepoEnv(root: string): boolean {
143
+ const path = repoEnvPath(root);
144
+ try {
145
+ if (!lstatSync(dirname(path)).isDirectory()) {
146
+ throw new Error("repository environment directory is unsafe");
147
+ }
148
+ if (!lstatSync(path).isFile()
149
+ || !readFileSync(path, "utf8").startsWith(`${MANAGED_HEADER}\n`)) {
150
+ throw new Error("repository environment file is not managed by GrantTap");
151
+ }
152
+ unlinkSync(path);
153
+ return true;
154
+ } catch (error) {
155
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
156
+ throw error;
157
+ }
158
+ }
159
+
130
160
  function escapeEnv(value: string): string {
131
161
  if (!/[\s#"\\]/.test(value)) return value;
132
162
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
@@ -5,6 +5,9 @@
5
5
  import type { ScopedMeshView } from "../snapshot/scoped-view";
6
6
  import { compileMeshContext } from "./packet";
7
7
  import { projectKnowledge } from "../../engine/runtime/engine-memory";
8
+ import {
9
+ activeScopedKnowledge, supersededScopedKnowledgeIds, unrecordedKnowledgeEvents,
10
+ } from "../knowledge/active";
8
11
 
9
12
  export type ProjectContextMode = "compact" | "full" | "legacy";
10
13
 
@@ -139,8 +142,16 @@ export async function renderProjectContext(
139
142
  const local = await projectKnowledge(view.project.projectId, view.execution.taskId);
140
143
  const records = new Map((view.knowledge ?? []).map((item) => [item.recordId, item]));
141
144
  for (const item of local ?? []) records.set(item.recordId, item);
142
- const scoped = { ...view, knowledge: [...records.values()]
143
- .sort((a, b) => b.recordedAt - a.recordedAt).slice(0, 32) };
145
+ const all = [...records.values()];
146
+ const supersededKnowledgeRecordIds = [...new Set([
147
+ ...(view.supersededKnowledgeRecordIds ?? []),
148
+ ...supersededScopedKnowledgeIds(view.project.projectId, all, view.execution.taskId),
149
+ ])];
150
+ const scoped = { ...view,
151
+ knowledge: activeScopedKnowledge(view.project.projectId, all, view.execution.taskId)
152
+ .sort((a, b) => b.recordedAt - a.recordedAt).slice(0, 32),
153
+ supersededKnowledgeRecordIds,
154
+ events: unrecordedKnowledgeEvents(view.events, all, supersededKnowledgeRecordIds) };
144
155
  const contextPacket = await compileMeshContext(scoped);
145
156
  if (mode === "compact") return { ...renderCompactProjectContext(scoped), contextPacket };
146
157
  if (mode === "legacy") {
@@ -0,0 +1,14 @@
1
+ # Mesh knowledge projection
2
+
3
+ `active.ts` filters a Project's bounded phone projection using recorded
4
+ supersession links. The Engine journal remains the durable source and retains
5
+ the complete audit history. A correction only hides an earlier record when both
6
+ records have the same Project, visibility, category, and repository scope.
7
+
8
+ `write.ts` accepts an explicit human decision for an existing Task on a locally
9
+ bound Project, records it through Engine Memory, and returns a `recorded` receipt
10
+ only after the Engine confirms. A correction carries the old record ID; the
11
+ Engine validates its scope. The phone may replay the same ID after a lost reply.
12
+ This is not a migration of old chats into verified knowledge.
13
+
14
+ MIT License.