granttap-mcp 0.6.10 → 0.7.0

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 (56) hide show
  1. package/README.md +52 -3
  2. package/SECURITY.md +20 -0
  3. package/apps/bridge/src/bin/claude-hook.ts +3 -13
  4. package/apps/bridge/src/bin/codex-hook.ts +3 -0
  5. package/apps/bridge/src/bin/codex-policy-hook.ts +2 -1
  6. package/apps/bridge/src/bin/cursor-after-shell.ts +5 -1
  7. package/apps/bridge/src/bin/cursor-hook.ts +5 -0
  8. package/apps/bridge/src/bin/cursor-mcp-hook.ts +5 -0
  9. package/apps/bridge/src/bin/mesh-connect.ts +27 -0
  10. package/apps/bridge/src/bin/setup.ts +1 -0
  11. package/apps/bridge/src/config/runtime.ts +34 -1
  12. package/apps/bridge/src/install.ts +9 -1
  13. package/apps/bridge/src/mesh/README.md +25 -0
  14. package/apps/bridge/src/mesh/attention.ts +19 -0
  15. package/apps/bridge/src/mesh/capsule.ts +86 -0
  16. package/apps/bridge/src/mesh/catalog.ts +105 -0
  17. package/apps/bridge/src/mesh/endpoint.ts +179 -0
  18. package/apps/bridge/src/mesh/handoff.ts +37 -0
  19. package/apps/bridge/src/mesh/identity.ts +39 -0
  20. package/apps/bridge/src/mesh/local.ts +18 -0
  21. package/apps/bridge/src/mesh/runtime.ts +278 -0
  22. package/apps/bridge/src/mesh/store-support.ts +23 -0
  23. package/apps/bridge/src/mesh/store.ts +297 -0
  24. package/apps/bridge/src/mesh/worktree.ts +38 -0
  25. package/apps/bridge/src/monitor.ts +68 -9
  26. package/apps/bridge/src/reply/README.md +4 -0
  27. package/apps/bridge/src/reply/provider-headless.ts +96 -0
  28. package/apps/bridge/src/reply.ts +35 -2
  29. package/apps/bridge/src/session-keys.ts +23 -1
  30. package/apps/bridge/src/sessions/common.ts +3 -5
  31. package/apps/bridge/src/sessions/grok.ts +179 -0
  32. package/apps/bridge/src/sessions.ts +11 -13
  33. package/apps/mcp/src/bin/status.ts +3 -1
  34. package/apps/mcp/src/create-server.ts +3 -1
  35. package/apps/mcp/src/mcp-tools/README.md +11 -0
  36. package/apps/mcp/src/mcp-tools/interaction.ts +102 -9
  37. package/apps/mcp/src/mcp-tools/mesh-actions.ts +224 -0
  38. package/apps/mcp/src/mcp-tools/mesh-relay.ts +66 -0
  39. package/apps/mcp/src/mcp-tools/mesh-resource.ts +31 -0
  40. package/apps/mcp/src/mcp-tools/mesh-tools.ts +103 -0
  41. package/apps/mcp/src/mesh-server.ts +13 -0
  42. package/apps/mcp/src/mesh-stdio.ts +5 -0
  43. package/apps/mcp/src/oauth/consent-page.ts +5 -2
  44. package/apps/mcp/src/provider-status.ts +19 -4
  45. package/bin/granttap-mcp.mjs +8 -1
  46. package/package.json +4 -5
  47. package/packages/core/relay-client.ts +3 -2
  48. package/packages/protocol/messages/README.md +7 -0
  49. package/packages/protocol/messages/interaction.ts +2 -1
  50. package/packages/protocol/messages/mesh-endpoint.ts +68 -0
  51. package/packages/protocol/messages/mesh.ts +247 -0
  52. package/packages/protocol/messages/primitives.ts +3 -0
  53. package/packages/protocol/messages/sessions.ts +28 -1
  54. package/packages/protocol/schema.ts +15 -1
  55. package/apps/bridge/src/sessions/copilot-capabilities.ts +0 -127
  56. package/apps/bridge/src/sessions/copilot.ts +0 -283
package/README.md CHANGED
@@ -34,11 +34,47 @@ end-to-end encrypted.
34
34
 
35
35
  - Primary: Claude Code and Codex.
36
36
  - Beta: Cursor.
37
- - Experimental where available: GitHub Copilot CLI and Grok Build.
37
+ - Experimental where available: Grok Build.
38
38
 
39
39
  GrantTap reports the depth each provider actually exposes. Visibility does not
40
40
  imply deterministic remote blocking or full mobile continuation.
41
41
 
42
+ ## Project Mesh and task handoff
43
+
44
+ Project Mesh adds stable Project and Task identity above provider-native
45
+ sessions. A Task can retain its `taskId` across multiple executions, agents,
46
+ and computers while each native session remains intact.
47
+
48
+ Agents can read the compact `granttap://mesh/current` MCP resource and publish
49
+ bounded progress, dependency, question, answer, claim, conflict, and completion
50
+ events through the existing `notify` tool. Full transcripts and hidden
51
+ reasoning are never mesh payloads. Claims have TTLs; a colliding claim is
52
+ rejected before it is recorded so agents can choose different work or contact
53
+ the owner before escalating to Needs You.
54
+
55
+ The first executable handoff path is Claude Code ↔ Codex across linked
56
+ computers. GrantTap builds a bounded Task Capsule from explicit task/git facts,
57
+ requires local phone authorization, creates a separate target branch/worktree,
58
+ starts the target execution, and returns a receipt bound to the exact capsule.
59
+ Cursor and Grok Build use the same provider-neutral schema and discovery model;
60
+ unsupported remote-start paths fail closed instead of claiming parity.
61
+
62
+ ### Grok Bot as a scoped Mesh participant
63
+
64
+ Grok Bot is a persistent agent, not a coding-agent integration. The iPhone
65
+ issues a one-time encrypted Mesh Invite scoped to the Projects you select, and
66
+ the invite is redeemed on the trusted CLI:
67
+
68
+ ```bash
69
+ granttap mesh connect <one-time-invite>
70
+ ```
71
+
72
+ Grok Bot then runs `granttap internal mesh-mcp`, a separate scoped MCP server
73
+ that exposes only the twelve task-scoped Mesh operations. It cannot create
74
+ invites, change the relay, expand Project scope, or reach `setup`; revoking the
75
+ endpoint from the iPhone stops new Mesh operations immediately while local Task
76
+ history stays on the device.
77
+
42
78
  ## Install
43
79
 
44
80
  ```bash
@@ -58,6 +94,7 @@ granttap setup
58
94
  granttap status [--json]
59
95
  granttap connect [--relay <wss-url>]
60
96
  granttap reset [--yes]
97
+ granttap mesh connect <one-time-invite>
61
98
  ```
62
99
 
63
100
  `connect` reuses a valid pairing. If none exists, it creates a one-time QR.
@@ -89,6 +126,9 @@ MCP `connect` accepts no custom routing, replacement, or key-rotation input.
89
126
  Setup is CLI-only because it changes provider configuration and must not be
90
127
  available to a model through prompt injection.
91
128
 
129
+ `notify` may alternatively carry one bounded task-scoped Mesh event. This does
130
+ not add a fifth MCP tool or grant any global setup capability.
131
+
92
132
  Provider-native approvals and mobile continuation require the matching local
93
133
  adapter. MCP registration alone is never reported as proof that an integration
94
134
  is ready.
@@ -141,10 +181,19 @@ npm install
141
181
  npm run typecheck
142
182
  npm test
143
183
  npm run package:allowlist
184
+ npm run test:coverage # macOS only — see below
144
185
  ```
145
186
 
187
+ `npm test` runs everywhere. Both suites run with an isolated `HOME`, so a
188
+ developer's own Claude/Codex/Cursor data can never inflate a local result: the
189
+ numbers on this machine and in CI are the same. The coverage contract is
190
+ measured on macOS because the background helper, the Cursor OAuth service, and
191
+ the installer only execute there, so a Linux percentage understates real
192
+ runtime coverage. CI runs the cross-platform suite on Linux and the coverage
193
+ gate on macOS, and `npm publish` enforces the same gate through
194
+ `prepublishOnly`.
195
+
146
196
  Do not publish from a dirty checkout or before the package allowlist, tests,
147
197
  typecheck, and release checks pass.
148
198
 
149
- GrantTap is not affiliated with Anthropic, OpenAI, Apple, Microsoft, Anysphere,
150
- GitHub, or xAI.
199
+ GrantTap is not affiliated with Anthropic, OpenAI, Apple, Anysphere, or xAI.
package/SECURITY.md CHANGED
@@ -27,6 +27,9 @@ GrantTap does not send them to the relay.
27
27
  transfer key. The relay receives the mailbox ID and ciphertext, not the key.
28
28
  - Each attached task has an independent 256-bit task key. One task key cannot
29
29
  decrypt another task.
30
+ - Project Mesh snapshots have independent project keys. Cross-computer events
31
+ remain under their Task key, and a linked destination receives either key
32
+ only through an explicit phone-mediated route.
30
33
  - Task keys travel only inside the authenticated device channel and are stored
31
34
  in device-protected storage.
32
35
  - Native phone/watch payloads remain authenticated ciphertext across the
@@ -46,6 +49,14 @@ whose independent keys were granted to it. The protection is against relay,
46
49
  database/network compromise, other pairings, and disclosure of a different
47
50
  task key—not against an endpoint already authorized for that task.
48
51
 
52
+ Task Capsules are strict, bounded schemas containing goal, explicit status,
53
+ repository/commit facts, changed-file names, tests, dependencies, claims,
54
+ remaining work, and explicit decisions. Unknown fields are rejected; hidden
55
+ reasoning and transcript replication are outside the contract. Handoff
56
+ acceptance includes a SHA-256 receipt over a canonical capsule and the source,
57
+ target, Task identity, and acceptance time. Stale claims expire, replayed event
58
+ IDs are ignored, and destination routing is explicit.
59
+
49
60
  ## Runtime controls
50
61
 
51
62
  Provider hooks enforce policy on the computer. Global provider deny always
@@ -57,5 +68,14 @@ background services. The public MCP server can only connect an existing or new
57
68
  phone pairing, notify, and ask bounded questions; it cannot reconfigure the
58
69
  machine or select a custom relay.
59
70
 
71
+ A Grok Bot endpoint is a separate scoped identity. Its one-time invite is
72
+ created only on the iPhone, stored on the relay as ciphertext, redeemed only by
73
+ the trusted `granttap mesh connect` CLI, and written to disk with `0600`
74
+ permissions. Its MCP server exposes only task-scoped Mesh operations bound to
75
+ one credential: every call re-checks endpoint, credential, policy revision,
76
+ expiry, actor, Project scope, and operation, and a revoked or disabled endpoint
77
+ fails closed. Invite creation, actor enablement, Project scope, and revocation
78
+ stay in the iPhone UI and the trusted CLI, never in a model-callable tool.
79
+
60
80
  Relay vulnerabilities should be reported through the
61
81
  [GrantTap relay advisory form](https://github.com/sergii-ziborov/granttap-relay/security/advisories/new).
@@ -18,6 +18,7 @@ import {
18
18
  autoAcceptLevelFor,
19
19
  blockedSessionCapability,
20
20
  isGatingSkipped,
21
+ isProviderEnabled,
21
22
  loadConfig,
22
23
  machineConfigPath,
23
24
  shouldAutoAcceptTool,
@@ -48,6 +49,8 @@ async function main(): Promise<void> {
48
49
  return;
49
50
  }
50
51
 
52
+ if (!isProviderEnabled("claude")) return;
53
+
51
54
  const blocked = blockedSessionCapability(
52
55
  input.session_id,
53
56
  input.tool_name,
@@ -69,19 +72,6 @@ async function main(): Promise<void> {
69
72
  // Claude already granted this chat unconditional tool access. GrantTap's
70
73
  // explicit MCP/skill/CLI blocks above still win, but phone approval must not
71
74
  // re-prompt a call Claude is intentionally running in bypass mode.
72
- // TEMPORARY DIAGNOSTIC: record only permission-related metadata, never the
73
- // tool input or any prompt text. Remove once the bypass field name is known.
74
- try {
75
- const { appendFileSync } = await import("node:fs");
76
- appendFileSync("/tmp/granttap-hook-probe.log", JSON.stringify({
77
- keys: Object.keys(input),
78
- permission_mode: (input as Record<string, unknown>).permission_mode,
79
- permissionMode: (input as Record<string, unknown>).permissionMode,
80
- tool_name: (input as Record<string, unknown>).tool_name,
81
- session_id: String((input as Record<string, unknown>).session_id ?? "").slice(0, 8),
82
- }) + "\n");
83
- } catch { /* diagnostics must never block a tool call */ }
84
-
85
75
  if (input.permission_mode === "bypassPermissions") return;
86
76
 
87
77
  // Gating paused, or this session is exempt → abstain (empty output = Claude
@@ -18,6 +18,7 @@ import {
18
18
  autoAcceptLevelFor,
19
19
  blockedSessionCapability,
20
20
  isGatingSkipped,
21
+ isProviderEnabled,
21
22
  loadConfig,
22
23
  machineConfigPath,
23
24
  shouldAutoAcceptTool,
@@ -49,6 +50,8 @@ async function main(): Promise<void> {
49
50
  return;
50
51
  }
51
52
 
53
+ if (!isProviderEnabled("codex")) return;
54
+
52
55
  const blocked = blockedSessionCapability(
53
56
  input.session_id,
54
57
  input.tool_name,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  /** Deny-only Codex PreToolUse policy for exact per-chat capability switches. */
3
3
  import type { HookInput } from "../adapters";
4
- import { blockedSessionCapability } from "../config";
4
+ import { blockedSessionCapability, isProviderEnabled } from "../config";
5
5
 
6
6
  async function readStdin(): Promise<string> {
7
7
  const chunks: Buffer[] = [];
@@ -10,6 +10,7 @@ async function readStdin(): Promise<string> {
10
10
  }
11
11
 
12
12
  async function main(): Promise<void> {
13
+ if (!isProviderEnabled("codex")) return;
13
14
  let input: HookInput;
14
15
  try {
15
16
  input = JSON.parse(await readStdin()) as HookInput;
@@ -7,7 +7,7 @@ import {
7
7
  resolvedFromOutcome,
8
8
  sendApprovalResolved,
9
9
  } from "../approval-state";
10
- import { loadConfig, machineConfigPath } from "../config";
10
+ import { isProviderEnabled, loadConfig, machineConfigPath } from "../config";
11
11
  import { cursorRootSessionId } from "../sessions/cursor";
12
12
 
13
13
  const SHELL_TOOLS = new Set([
@@ -32,6 +32,10 @@ async function readStdin(): Promise<string> {
32
32
  }
33
33
 
34
34
  async function main(): Promise<void> {
35
+ if (!isProviderEnabled("cursor")) {
36
+ process.stdout.write("{}\n");
37
+ return;
38
+ }
35
39
  const raw = await readStdin().catch(() => "");
36
40
  let input: {
37
41
  conversation_id?: string;
@@ -10,6 +10,7 @@ import {
10
10
  autoAcceptLevelFor,
11
11
  blockedSessionCapability,
12
12
  isGatingSkipped,
13
+ isProviderEnabled,
13
14
  loadConfig,
14
15
  machineConfigPath,
15
16
  shouldAutoAcceptCursorShell,
@@ -38,6 +39,10 @@ async function main(): Promise<void> {
38
39
  nativeAsk("GrantTap could not read this shell call; use Cursor approval.");
39
40
  return;
40
41
  }
42
+ if (!isProviderEnabled("cursor")) {
43
+ nativeAsk("GrantTap monitoring for Cursor is disabled; use Cursor approval.");
44
+ return;
45
+ }
41
46
  const rawSessionId = input.conversation_id ?? input.session_id;
42
47
  const sessionId = cursorRootSessionId(rawSessionId) ?? rawSessionId;
43
48
  const blocked = blockedSessionCapability(
@@ -7,6 +7,7 @@ import { isUnanswered, requestApproval } from "../approval";
7
7
  import {
8
8
  blockedSessionMcpServer,
9
9
  isGatingSkipped,
10
+ isProviderEnabled,
10
11
  loadConfig,
11
12
  machineConfigPath,
12
13
  shouldAutoAcceptTool,
@@ -40,6 +41,10 @@ async function main(): Promise<void> {
40
41
  nativeAsk("GrantTap could not correlate this MCP call; use Cursor approval.");
41
42
  return;
42
43
  }
44
+ if (!isProviderEnabled("cursor")) {
45
+ nativeAsk("GrantTap monitoring for Cursor is disabled; use Cursor approval.");
46
+ return;
47
+ }
43
48
  const rawSessionId = cursorConversationId(input);
44
49
  const sessionId = cursorRootSessionId(rawSessionId) ?? rawSessionId;
45
50
  const server = resolveCursorMcpServer(input);
@@ -0,0 +1,27 @@
1
+ import { connectGrokBotInvite } from "../mesh/endpoint";
2
+
3
+ function inviteArgument(args: string[]): string {
4
+ if (args.length === 1 && !["--help", "-h"].includes(args[0]!)) return args[0]!;
5
+ process.stderr.write("Usage: granttap mesh connect <one-time-invite>\n");
6
+ process.exit(1);
7
+ }
8
+
9
+ const invite = inviteArgument(process.argv.slice(2));
10
+ void connectGrokBotInvite(invite).then((bundle) => {
11
+ process.stdout.write([
12
+ "GrantTap Project Mesh",
13
+ "",
14
+ `Grok Bot endpoint Connected (${bundle.endpoint.displayName})`,
15
+ `Actors ${bundle.actors.length}`,
16
+ `Allowed projects ${bundle.credential.projectIds.length}`,
17
+ "",
18
+ "Configure Grok Bot to run: granttap internal mesh-mcp",
19
+ "The scoped MCP server cannot create invites, change relay, or expand Project access.",
20
+ "",
21
+ ].join("\n"));
22
+ }).catch((error: unknown) => {
23
+ process.stderr.write(`[granttap] Grok Bot connection failed: ${
24
+ error instanceof Error ? error.message : String(error)
25
+ }\n`);
26
+ process.exitCode = 1;
27
+ });
@@ -79,6 +79,7 @@ async function main(): Promise<void> {
79
79
  ? codexHook.status === "manual" ? "Needs attention" : "Needs hook trust"
80
80
  : "Not installed"}`,
81
81
  `Cursor ${before.cursor.installed ? `Beta · ${cursorReady ? "Authorize in Cursor" : "Needs repair"}` : "Not installed"}`,
82
+ `Grok Build ${installed.has("grok") ? "Ready" : "Not installed"}`,
82
83
  "",
83
84
  paired && installed.has("codex")
84
85
  ? `Next: ${CODEX_TRUST_INSTRUCTION}`
@@ -1,5 +1,9 @@
1
1
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import type { AgentAccess } from "../../../../packages/protocol/schema";
2
+ import type {
3
+ AgentAccess,
4
+ CodingAgent,
5
+ ProviderRuntimeSettings,
6
+ } from "../../../../packages/protocol/schema";
3
7
  import {
4
8
  classifyAction,
5
9
  isAutoAcceptLevel,
@@ -19,6 +23,8 @@ export type RuntimeConfig = {
19
23
  sessionMcpDisabled: Record<string, string[]>;
20
24
  sessionSkillsDisabled: Record<string, string[]>;
21
25
  sessionShellDisabled: string[];
26
+ providerSettings: ProviderRuntimeSettings;
27
+ meshEnabled: boolean;
22
28
  };
23
29
 
24
30
  const DEFAULT_RUNTIME: RuntimeConfig = {
@@ -31,8 +37,23 @@ const DEFAULT_RUNTIME: RuntimeConfig = {
31
37
  sessionMcpDisabled: {},
32
38
  sessionSkillsDisabled: {},
33
39
  sessionShellDisabled: [],
40
+ providerSettings: { claude: true, codex: true, cursor: true, grok: true },
41
+ meshEnabled: true,
34
42
  };
35
43
 
44
+ function parseProviderSettings(raw: unknown): ProviderRuntimeSettings {
45
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
46
+ return { ...DEFAULT_RUNTIME.providerSettings };
47
+ }
48
+ const value = raw as Record<string, unknown>;
49
+ return {
50
+ claude: value.claude !== false,
51
+ codex: value.codex !== false,
52
+ cursor: value.cursor !== false,
53
+ grok: value.grok !== false,
54
+ };
55
+ }
56
+
36
57
  export function boundedIdentifier(raw: unknown, maxLength: number): string | null {
37
58
  if (typeof raw !== "string") return null;
38
59
  const value = raw.trim();
@@ -96,6 +117,8 @@ export function loadRuntimeConfig(): RuntimeConfig {
96
117
  .filter((sessionId): sessionId is string => sessionId != null),
97
118
  )]
98
119
  : [],
120
+ providerSettings: parseProviderSettings(raw.providerSettings),
121
+ meshEnabled: raw.meshEnabled !== false,
99
122
  };
100
123
  } catch {
101
124
  return {
@@ -105,10 +128,20 @@ export function loadRuntimeConfig(): RuntimeConfig {
105
128
  sessionMcpDisabled: {},
106
129
  sessionSkillsDisabled: {},
107
130
  sessionShellDisabled: [],
131
+ providerSettings: { ...DEFAULT_RUNTIME.providerSettings },
132
+ meshEnabled: true,
108
133
  };
109
134
  }
110
135
  }
111
136
 
137
+ export function isProviderEnabled(provider: CodingAgent): boolean {
138
+ return loadRuntimeConfig().providerSettings[provider];
139
+ }
140
+
141
+ export function isMeshEnabled(): boolean {
142
+ return loadRuntimeConfig().meshEnabled;
143
+ }
144
+
112
145
  export function saveRuntimeConfig(cfg: Partial<RuntimeConfig>): void {
113
146
  mkdirSync(configDir(), { recursive: true });
114
147
  const merged: RuntimeConfig = { ...loadRuntimeConfig(), ...cfg };
@@ -22,6 +22,7 @@ import { homedir } from "node:os";
22
22
  import { delimiter, dirname, join } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { spawnSync } from "node:child_process";
25
+ import type { CodingAgent } from "../../../packages/protocol/schema";
25
26
  import { configDir } from "./config";
26
27
 
27
28
  const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
@@ -54,7 +55,7 @@ export const CODEX_TRUST_INSTRUCTION =
54
55
  "Open /hooks in Codex, review and trust both GrantTap hooks, then restart Codex.";
55
56
 
56
57
  export type AgentIntegrationStatus = {
57
- agent: "codex" | "claude";
58
+ agent: CodingAgent;
58
59
  installed: boolean;
59
60
  hookConfigured: boolean;
60
61
  };
@@ -242,9 +243,16 @@ export function inspectCursorIntegration(): CursorIntegrationStatus {
242
243
  export function inspectAgentIntegrations(): AgentIntegrationStatus[] {
243
244
  const codex = process.env.GRANTTAP_CODEX_BIN ?? process.env.NODVOX_CODEX_BIN ?? "codex";
244
245
  const claude = process.env.GRANTTAP_CLAUDE_BIN ?? process.env.NODVOX_CLAUDE_BIN ?? "claude";
246
+ const cursor = process.env.GRANTTAP_CURSOR_AGENT_BIN ?? "cursor-agent";
247
+ const grok = process.env.GRANTTAP_GROK_BIN ?? "grok";
248
+ const cursorStatus = inspectCursorIntegration();
245
249
  return [
246
250
  { agent: "codex", installed: executableAvailable(codex), hookConfigured: codexHookConfigured() },
247
251
  { agent: "claude", installed: executableAvailable(claude), hookConfigured: claudeHookConfigured() },
252
+ { agent: "cursor", installed: executableAvailable(cursor), hookConfigured: cursorStatus.hookConfigured },
253
+ // Grok Build's headless session contract is direct; GrantTap does not
254
+ // claim an approval hook exists when none has been installed.
255
+ { agent: "grok", installed: executableAvailable(grok), hookConfigured: false },
248
256
  ];
249
257
  }
250
258
 
@@ -0,0 +1,25 @@
1
+ # Project Mesh runtime
2
+
3
+ This module links discovered native sessions to stable Project and Task
4
+ identities, persists bounded structured events, tracks expiring resource
5
+ claims and dependencies, and executes authorized task handoffs.
6
+
7
+ `runtime.ts` is the public runtime entry point. `catalog.ts` derives repository
8
+ identity without replacing provider-native session discovery. `store.ts`
9
+ deduplicates replayed events and expires stale claims. `capsule.ts` reads only
10
+ explicit git and task facts; transcripts and hidden reasoning are never copied.
11
+ `worktree.ts` creates a separate target branch/worktree before Claude Code or
12
+ Codex is started. The provider abstraction already admits Cursor and Grok Build
13
+ without changing the core contract, while their unsupported phase-one start
14
+ paths fail closed.
15
+
16
+ The read-only MCP resource `granttap://mesh/current` exposes compact local state
17
+ to an agent. Structured events use the existing `notify` tool so the public MCP
18
+ tool allowlist remains exactly `connect`, `notify`, `ask_yes_no`, and `ask`.
19
+ Setup and global provider configuration remain CLI-only.
20
+
21
+ Every event is schema-bounded and sent under an independent task key; snapshots
22
+ use an independent project key. Resource claims are advisory, expire without a
23
+ heartbeat, and a colliding claim is rejected before it is recorded. The agent
24
+ may choose other work or ask the current owner; the conflict reaches the human
25
+ only after explicit `needsUser` escalation.
@@ -0,0 +1,19 @@
1
+ import type { MeshEventType } from "../../../../packages/protocol/schema";
2
+
3
+ type AttentionFacts = {
4
+ category?: unknown;
5
+ resolved?: unknown;
6
+ needsUser?: unknown;
7
+ failed?: unknown;
8
+ };
9
+
10
+ /** Only deterministic, product-approved reasons may enter the human inbox. */
11
+ export function classifyHumanAttention(type: MeshEventType, facts: AttentionFacts): boolean {
12
+ if (type === "AGENT_QUESTION") {
13
+ return ["product", "business", "security", "destructive"].includes(String(facts.category));
14
+ }
15
+ if (type === "CONFLICT") return facts.resolved !== true && facts.needsUser === true;
16
+ if (type === "TASK_BLOCKED") return facts.needsUser === true;
17
+ if (type === "HANDOFF_REJECTED") return facts.failed === true;
18
+ return false;
19
+ }
@@ -0,0 +1,86 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import {
4
+ TaskCapsule,
5
+ type MeshHandoffPrepare,
6
+ type SessionInfo,
7
+ type TaskCapsule as CapsuleValue,
8
+ } from "../../../../packages/protocol/schema";
9
+ import type { MeshStore } from "./store";
10
+
11
+ function git(cwd: string, args: string[]): string | undefined {
12
+ try {
13
+ return execFileSync("git", ["-C", cwd, ...args], {
14
+ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3_000,
15
+ maxBuffer: 512 * 1_024,
16
+ }).trim() || undefined;
17
+ } catch {
18
+ return undefined;
19
+ }
20
+ }
21
+
22
+ function gitRaw(cwd: string, args: string[]): string | undefined {
23
+ try {
24
+ return execFileSync("git", ["-C", cwd, ...args], {
25
+ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3_000,
26
+ maxBuffer: 512 * 1_024,
27
+ }) || undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ function changedFiles(cwd: string): string[] {
34
+ const status = gitRaw(cwd, ["status", "--porcelain=v1", "-z"]);
35
+ if (!status) return [];
36
+ const files = status.split("\0").flatMap((row) => {
37
+ const value = row.length > 3 ? row.slice(3).trim() : "";
38
+ return value ? [value.split(" -> ").at(-1)!] : [];
39
+ });
40
+ return [...new Set(files)].slice(0, 64);
41
+ }
42
+
43
+ function dirtyDiffHash(cwd: string): string | undefined {
44
+ const diff = git(cwd, ["diff", "--binary", "HEAD"]);
45
+ return diff ? createHash("sha256").update(diff).digest("hex") : undefined;
46
+ }
47
+
48
+ export function buildTaskCapsule(
49
+ store: MeshStore,
50
+ session: SessionInfo,
51
+ request: MeshHandoffPrepare,
52
+ computerId: string,
53
+ ): CapsuleValue | undefined {
54
+ const cwd = session.worktree ?? session.cwd;
55
+ const snapshot = store.snapshot(request.projectId);
56
+ const task = snapshot?.tasks.find((item) => item.taskId === request.taskId);
57
+ const project = snapshot?.project;
58
+ if (!cwd || !task || !project || session.taskId !== request.taskId) return undefined;
59
+ const head = git(cwd, ["rev-parse", "HEAD"]);
60
+ if (!head) return undefined;
61
+ const capsule = {
62
+ taskId: request.taskId,
63
+ goal: task.goal,
64
+ currentStatus: session.summary ?? session.title ?? task.title,
65
+ sourceProvider: session.agent,
66
+ sourceComputer: computerId,
67
+ targetProvider: request.targetProvider,
68
+ targetActorId: request.targetActorId,
69
+ targetComputer: request.targetComputer,
70
+ repository: project.canonicalRepositoryId,
71
+ baseSha: head,
72
+ branch: session.branch ?? git(cwd, ["branch", "--show-current"]),
73
+ latestCommit: head,
74
+ dirtyDiffHash: dirtyDiffHash(cwd),
75
+ filesChanged: changedFiles(cwd),
76
+ dependencies: snapshot.dependencies
77
+ .filter((item) => item.taskId === request.taskId).map((item) => item.dependsOnTaskId).slice(0, 32),
78
+ resourceClaims: snapshot.claims
79
+ .filter((item) => item.taskId === request.taskId).map((item) => item.resource).slice(0, 64),
80
+ remainingWork: ["Continue the task from the current committed state."],
81
+ importantDecisions: [],
82
+ createdAt: Date.now(),
83
+ };
84
+ const parsed = TaskCapsule.safeParse(capsule);
85
+ return parsed.success ? parsed.data : undefined;
86
+ }
@@ -0,0 +1,105 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import type {
4
+ MeshProvider,
5
+ MeshTask,
6
+ Project,
7
+ SessionInfo,
8
+ TaskState,
9
+ } from "../../../../packages/protocol/schema";
10
+ import { canonicalRepositoryIdentity, projectIdentity, taskIdentity } from "./identity";
11
+ import type { MeshStore } from "./store";
12
+
13
+ export type RepositoryFacts = {
14
+ root: string;
15
+ canonicalRepositoryId: string;
16
+ baseRemote?: string;
17
+ worktree?: string;
18
+ };
19
+
20
+ const repositoryCache = new Map<string, RepositoryFacts>();
21
+
22
+ function git(cwd: string, args: string[]): string | undefined {
23
+ try {
24
+ return execFileSync("git", ["-C", cwd, ...args], {
25
+ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2_000,
26
+ }).trim() || undefined;
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function inspectRepository(cwd: string): RepositoryFacts {
33
+ const cached = repositoryCache.get(cwd);
34
+ if (cached) return cached;
35
+ const root = git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd;
36
+ const baseRemote = git(root, ["remote", "get-url", "origin"]);
37
+ const facts = {
38
+ root,
39
+ baseRemote,
40
+ canonicalRepositoryId: canonicalRepositoryIdentity(baseRemote, root),
41
+ worktree: git(root, ["rev-parse", "--show-toplevel"]),
42
+ };
43
+ repositoryCache.set(cwd, facts);
44
+ return facts;
45
+ }
46
+
47
+ function provider(value: string): MeshProvider | undefined {
48
+ return ["claude", "codex", "cursor", "grok"].includes(value)
49
+ ? value as MeshProvider
50
+ : undefined;
51
+ }
52
+
53
+ function taskState(session: SessionInfo): TaskState {
54
+ if (session.state === "waiting") return "needs_user";
55
+ if (session.state === "working") return "working";
56
+ return "planned";
57
+ }
58
+
59
+ export function linkSessionsToProjects(
60
+ store: MeshStore,
61
+ sessions: SessionInfo[],
62
+ computerId: string,
63
+ inspect: (cwd: string) => RepositoryFacts = inspectRepository,
64
+ ): SessionInfo[] {
65
+ return sessions.map((session) => {
66
+ const agent = provider(session.agent);
67
+ const cwd = session.cwd?.trim();
68
+ if (!agent || !cwd) return session;
69
+ const repository = inspect(cwd);
70
+ const projectId = projectIdentity(repository.canonicalRepositoryId);
71
+ const project: Project = {
72
+ projectId,
73
+ name: basename(repository.root) || "Project",
74
+ repositoryRoot: repository.root,
75
+ canonicalRepositoryId: repository.canonicalRepositoryId,
76
+ baseRemote: repository.baseRemote,
77
+ createdAt: session.startedAt,
78
+ };
79
+ store.upsertProject(project);
80
+ const knownTask = store.taskForExecution(computerId, agent, session.sessionId);
81
+ const taskId = knownTask ?? taskIdentity(projectId, agent, session.sessionId);
82
+ const task: MeshTask = {
83
+ taskId,
84
+ projectId,
85
+ title: (session.title ?? session.summary ?? `${agent} task`).slice(0, 160),
86
+ goal: (session.summary ?? session.title ?? "Continue this coding task").slice(0, 1_000),
87
+ state: taskState(session),
88
+ ownerSessionId: session.sessionId,
89
+ createdAt: session.startedAt,
90
+ updatedAt: session.lastActivityAt,
91
+ };
92
+ store.upsertTask(task);
93
+ store.linkExecution({
94
+ taskId,
95
+ sessionId: session.sessionId,
96
+ provider: agent,
97
+ computerId,
98
+ workspace: cwd,
99
+ branch: session.branch,
100
+ worktree: repository.worktree,
101
+ startedAt: session.startedAt,
102
+ });
103
+ return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
104
+ });
105
+ }