granttap-mcp 0.8.70 → 0.8.71
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/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +36 -13
- package/apps/bridge/src/monitor/README.md +7 -3
- package/apps/bridge/src/monitor/handlers/receipts.ts +0 -2
- package/apps/bridge/src/monitor/handlers/task-create.ts +12 -10
- package/apps/bridge/src/monitor/handlers/user-message.ts +36 -32
- package/apps/bridge/src/monitor/message-delivery/README.md +3 -0
- package/apps/bridge/src/monitor/message-delivery/acceptance.ts +33 -0
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,15 @@ import { inspectRepository } from "../../catalog";
|
|
|
5
5
|
import { computerId } from "../../identity/computer";
|
|
6
6
|
import { matchesLocalCheckout } from "./checkout-identity";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Re-admit this computer's verified checkouts before an explicit Graph build.
|
|
10
|
+
*
|
|
11
|
+
* Only a repository whose Graph is genuinely unavailable is reported. A plain
|
|
12
|
+
* folder an agent ran in, a subfolder of a repository, a removed checkout and
|
|
13
|
+
* a former identity of a checkout another binding already describes are not
|
|
14
|
+
* repositories with a broken Graph; reporting them filled Mesh Health with
|
|
15
|
+
* errors nobody could act on.
|
|
16
|
+
*/
|
|
9
17
|
export async function refreshLocalGraphBindings(
|
|
10
18
|
snapshot: MeshSnapshot,
|
|
11
19
|
dependencies: {
|
|
@@ -18,19 +26,23 @@ export async function refreshLocalGraphBindings(
|
|
|
18
26
|
const candidates = (snapshot.bindings ?? []).filter((binding) =>
|
|
19
27
|
binding.endpointId === localEndpoint && binding.available && binding.localPathHint);
|
|
20
28
|
const failures: ProjectRepositoryGraph[] = [];
|
|
21
|
-
|
|
29
|
+
const checkouts = candidates.flatMap((binding) => {
|
|
30
|
+
const facts = readCheckout(dependencies.inspect, binding.localPathHint!);
|
|
31
|
+
return facts ? [{ binding, facts, real: realpathSync(facts.root) }] : [];
|
|
32
|
+
});
|
|
33
|
+
await Promise.all(checkouts.map(async ({ binding, facts, real }) => {
|
|
34
|
+
const pathIdentity = matchesLocalCheckout(binding.repositoryId, facts.root);
|
|
35
|
+
if (facts.canonicalRepositoryId !== binding.repositoryId && !pathIdentity) {
|
|
36
|
+
const described = checkouts.some((other) => other.real === real
|
|
37
|
+
&& (other.binding.repositoryId === facts.canonicalRepositoryId
|
|
38
|
+
|| matchesLocalCheckout(other.binding.repositoryId, facts.root)));
|
|
39
|
+
if (!described) failures.push({ projectId: snapshot.projectId, repositoryId: binding.repositoryId,
|
|
40
|
+
revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
|
|
41
|
+
analysisErrorCode: "REPOSITORY_IDENTITY_MISMATCH",
|
|
42
|
+
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
22
45
|
try {
|
|
23
|
-
const path = binding.localPathHint!;
|
|
24
|
-
const facts = dependencies.inspect(path);
|
|
25
|
-
const pathIdentity = matchesLocalCheckout(binding.repositoryId, facts.root);
|
|
26
|
-
if (!facts.worktree || realpathSync(facts.root) !== realpathSync(path)
|
|
27
|
-
|| (facts.canonicalRepositoryId !== binding.repositoryId && !pathIdentity)) {
|
|
28
|
-
failures.push({ projectId: snapshot.projectId, repositoryId: binding.repositoryId,
|
|
29
|
-
revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
|
|
30
|
-
analysisErrorCode: facts.worktree ? "REPOSITORY_IDENTITY_MISMATCH" : "REPOSITORY_IDENTITY_UNVERIFIED",
|
|
31
|
-
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false });
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
46
|
const admitted = await dependencies.sync(snapshot.project, {
|
|
35
47
|
summary: binding,
|
|
36
48
|
localRoot: facts.root,
|
|
@@ -45,6 +57,17 @@ export async function refreshLocalGraphBindings(
|
|
|
45
57
|
return failures;
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
/** A Git checkout rooted exactly at the bound path, or nothing a Graph can be built from. */
|
|
61
|
+
function readCheckout(inspect: typeof inspectRepository, path: string) {
|
|
62
|
+
try {
|
|
63
|
+
const facts = inspect(path);
|
|
64
|
+
if (!facts.worktree || realpathSync(facts.root) !== realpathSync(path)) return undefined;
|
|
65
|
+
return facts;
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
function admissionFailure(snapshot: MeshSnapshot, binding: NonNullable<MeshSnapshot["bindings"]>[number]): ProjectRepositoryGraph {
|
|
49
72
|
return { projectId: snapshot.projectId, repositoryId: binding.repositoryId,
|
|
50
73
|
revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
|
|
@@ -15,6 +15,10 @@ beside `mesh-publication`. This module uses the repository Apache License 2.0.
|
|
|
15
15
|
`message-delivery/index.ts` reserves the durable delivery lease, then
|
|
16
16
|
emits a non-terminal processing receipt only after workspace, attachment and
|
|
17
17
|
provider validation. Duplicate validated requests replay that receipt without
|
|
18
|
-
starting another provider run.
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
starting another provider run. A message is delivered once the agent has
|
|
19
|
+
taken it: `message-delivery/acceptance.ts` reports an immediate failure as that
|
|
20
|
+
failure and accepts a run still going after a short window, so the
|
|
21
|
+
eleven-minute lease only has to cover that window. The answer, or the reason a
|
|
22
|
+
long run stopped, reaches the chat when the run ends; unattended runs are
|
|
23
|
+
bounded at six hours. New-task startup events are status updates; the
|
|
24
|
+
correlated result is terminal. Delivery integration tests live in `tests/delivery`.
|
|
@@ -4,8 +4,6 @@ import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
|
4
4
|
import { pruneAttachments, takeAttachment } from "../../delivery/attachment-store";
|
|
5
5
|
import { sendSessionPayload } from "../../host/session-keys";
|
|
6
6
|
|
|
7
|
-
export const DELIVERY_TIMEOUT_MS = 10 * 60_000;
|
|
8
|
-
|
|
9
7
|
export async function sendDeliveryReceipt(
|
|
10
8
|
client: RelayClient,
|
|
11
9
|
messageId: string,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createCodexSession,
|
|
10
10
|
createCursorSession,
|
|
11
11
|
createGrokSession,
|
|
12
|
+
type ReplyResult,
|
|
12
13
|
} from "../../reply";
|
|
13
14
|
import { acceptInstanceEpoch } from "../../host/instance-epoch";
|
|
14
15
|
import { evaluateCreateTask } from "../../mesh/tasks/create-task";
|
|
@@ -16,7 +17,7 @@ import { recordDelegation } from "../../mesh/handoff/delegation-loop";
|
|
|
16
17
|
import { enqueuePinnedTask } from "../../mesh/tasks/queue";
|
|
17
18
|
import { sendSessionPayload } from "../../host/session-keys";
|
|
18
19
|
import { resolveMessageAttachments } from "./receipts";
|
|
19
|
-
import {
|
|
20
|
+
import { acceptWhenStarted, PHONE_RUN_LIMIT_MS } from "../message-delivery/acceptance";
|
|
20
21
|
|
|
21
22
|
export type TaskCreateAdmission =
|
|
22
23
|
| { status: "accepted" }
|
|
@@ -99,15 +100,16 @@ export async function handleTaskCreate(
|
|
|
99
100
|
cursor: createCursorSession,
|
|
100
101
|
grok: createGrokSession,
|
|
101
102
|
}[agent];
|
|
102
|
-
const
|
|
103
|
-
message.text, message.cwd,
|
|
103
|
+
const run = create(
|
|
104
|
+
message.text, message.cwd, PHONE_RUN_LIMIT_MS, resolved.attachments, message.model,
|
|
104
105
|
message.operationId,
|
|
105
106
|
);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
107
|
+
const settle = (result: ReplyResult, late: boolean) => result.ok
|
|
108
|
+
? say(result.text, result.sessionId, true)
|
|
109
|
+
: say(late ? `The ${displayName} task stopped: ${result.error}`
|
|
110
|
+
: `Could not create a ${displayName} task: ${result.error}`, undefined, true);
|
|
111
|
+
const early = await acceptWhenStarted(run, (result) => settle(result, true));
|
|
112
|
+
if (early === "started") return { status: "accepted" };
|
|
113
|
+
await settle(early, false);
|
|
114
|
+
return early.ok ? { status: "accepted" } : { status: "rejected", error: "Provider delivery failed." };
|
|
113
115
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createCursorSession,
|
|
11
11
|
createGrokSession,
|
|
12
12
|
deliverToSession,
|
|
13
|
+
type ReplyResult,
|
|
13
14
|
} from "../../reply";
|
|
14
15
|
import { noteDeliveredRun } from "../../mesh/delivery/run-digest";
|
|
15
16
|
import { evaluateCreateTask } from "../../mesh/tasks/create-task";
|
|
@@ -17,7 +18,7 @@ import { sendSessionPayload } from "../../host/session-keys";
|
|
|
17
18
|
import { scanSessions } from "../../sessions";
|
|
18
19
|
import { agentEventForUserMessage, sendDeliveryReceipt } from "./receipts";
|
|
19
20
|
import { resolveMessageAttachments } from "./receipts";
|
|
20
|
-
import {
|
|
21
|
+
import { acceptWhenStarted, PHONE_RUN_LIMIT_MS } from "../message-delivery/acceptance";
|
|
21
22
|
|
|
22
23
|
async function startNewAgentTask(input: {
|
|
23
24
|
client: RelayClient;
|
|
@@ -66,16 +67,17 @@ async function startNewAgentTask(input: {
|
|
|
66
67
|
cursor: createCursorSession,
|
|
67
68
|
grok: createGrokSession,
|
|
68
69
|
}[agent];
|
|
69
|
-
const
|
|
70
|
-
message.text, requestedCwd,
|
|
70
|
+
const run = create(
|
|
71
|
+
message.text, requestedCwd, PHONE_RUN_LIMIT_MS, resolved.attachments, message.model, undefined, message.effort,
|
|
71
72
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const settle = (result: ReplyResult, late: boolean) => result.ok
|
|
74
|
+
? say(result.text, result.sessionId, true)
|
|
75
|
+
: say(late ? `The ${displayName} task stopped: ${result.error}`
|
|
76
|
+
: `Could not create a ${displayName} task: ${result.error}`, undefined, true);
|
|
77
|
+
const early = await acceptWhenStarted(run, (result) => settle(result, true));
|
|
78
|
+
if (early === "started") return "accepted";
|
|
79
|
+
await settle(early, false);
|
|
80
|
+
return early.ok ? "accepted" : "rejected";
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export async function handleUserMessage(
|
|
@@ -125,32 +127,34 @@ export async function handleUserMessage(
|
|
|
125
127
|
// marks the person's bubble, and the next words in the chat are the answer.
|
|
126
128
|
await onProcessing?.();
|
|
127
129
|
const startedAt = Date.now();
|
|
128
|
-
const
|
|
130
|
+
const run = deliverToSession(target, message.text, PHONE_RUN_LIMIT_MS, attachments, {
|
|
129
131
|
preferredMcp: message.preferredMcp,
|
|
130
132
|
skill: message.skill,
|
|
131
133
|
model: message.model,
|
|
132
134
|
permissionMode: message.permissionMode,
|
|
133
135
|
effort: message.effort,
|
|
134
136
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
await say(result.text, result.sessionId ?? target.sessionId, true);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
137
|
+
const settle = async (result: ReplyResult, late: boolean) => {
|
|
138
|
+
// The run's turns are in the transcript, but a session holding this chat
|
|
139
|
+
// open never sees them; the journal is how it finds out on its next prompt,
|
|
140
|
+
// and the Task carries the same digest as progress.
|
|
141
|
+
const noted = noteDeliveredRun({
|
|
142
|
+
session: target,
|
|
143
|
+
prompt: message.text,
|
|
144
|
+
result,
|
|
145
|
+
startedAt,
|
|
146
|
+
endedAt: Date.now(),
|
|
147
|
+
});
|
|
148
|
+
if (noted?.event) {
|
|
149
|
+
await sendSessionPayload(client, noted.event, noted.event.taskId, "phone", { ttlMs: 24 * 60 * 60_000 })
|
|
150
|
+
.catch(() => {});
|
|
151
|
+
}
|
|
152
|
+
if (result.ok) await say(result.text, result.sessionId ?? target.sessionId, true);
|
|
153
|
+
else await say(late ? `The run stopped: ${result.error}` : `Could not deliver the message: ${result.error}`,
|
|
154
|
+
target.sessionId, true);
|
|
155
|
+
};
|
|
156
|
+
const early = await acceptWhenStarted(run, (result) => settle(result, true));
|
|
157
|
+
if (early === "started") return "accepted";
|
|
158
|
+
await settle(early, false);
|
|
159
|
+
return early.ok ? "accepted" : "rejected";
|
|
156
160
|
}
|
|
@@ -5,6 +5,9 @@ sequence for non-correlated phone messages. Validation happens before the first
|
|
|
5
5
|
processing receipt, and duplicate validated messages never start another run.
|
|
6
6
|
Terminal responses remain correlated to the original message id.
|
|
7
7
|
|
|
8
|
+
`acceptance.ts` decides when a phone message counts as delivered: when the agent
|
|
9
|
+
has taken it, not when its whole turn ends. Tests live in `tests/acceptance.test.ts`.
|
|
10
|
+
|
|
8
11
|
Integration tests: `tests/delivery/processing-receipt.test.ts`; attachment rejection
|
|
9
12
|
coverage: `tests/attachment/attachment-uploads.test.ts`. See the monitor README.
|
|
10
13
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a message from the phone counts as delivered.
|
|
3
|
+
*
|
|
4
|
+
* Delivery used to mean "the agent finished its whole turn within ten
|
|
5
|
+
* minutes": a longer turn was killed at the deadline and the person's bubble
|
|
6
|
+
* turned "Not delivered" although the agent had taken the message and was
|
|
7
|
+
* working on it. Delivered now means the agent took it. A run that fails at
|
|
8
|
+
* once (paused, transferred, a CLI that will not start) is still that
|
|
9
|
+
* failure; a run that is still going after a short window is delivered, and
|
|
10
|
+
* its answer — or the reason it stopped — reaches the chat when it ends.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Long enough for a provider CLI to refuse at once, short enough not to keep the phone waiting. */
|
|
14
|
+
export const ACCEPTANCE_WINDOW_MS = 15_000;
|
|
15
|
+
|
|
16
|
+
/** A safety bound for an unattended turn; the person stops a run with Stop, not with a deadline. */
|
|
17
|
+
export const PHONE_RUN_LIMIT_MS = 6 * 60 * 60_000;
|
|
18
|
+
|
|
19
|
+
export async function acceptWhenStarted<T>(
|
|
20
|
+
run: Promise<T>,
|
|
21
|
+
finish: (result: T) => Promise<unknown>,
|
|
22
|
+
windowMs = ACCEPTANCE_WINDOW_MS,
|
|
23
|
+
): Promise<T | "started"> {
|
|
24
|
+
let timer: NodeJS.Timeout | undefined;
|
|
25
|
+
const started = new Promise<"started">((resolve) => {
|
|
26
|
+
timer = setTimeout(() => resolve("started"), windowMs);
|
|
27
|
+
});
|
|
28
|
+
const first = await Promise.race([run, started]);
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
if (first !== "started") return first;
|
|
31
|
+
void run.then(finish).catch(() => {});
|
|
32
|
+
return "started";
|
|
33
|
+
}
|
package/cursor-plugin/README.md
CHANGED
|
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
|
|
|
10
10
|
Install the supported CLI on this computer, then run:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g granttap-mcp@0.8.
|
|
13
|
+
npm install -g granttap-mcp@0.8.71
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": [
|
|
6
6
|
"-e",
|
|
7
|
-
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.
|
|
7
|
+
"\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.71\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
|
|
|
7
7
|
const { existsSync } = require("node:fs");
|
|
8
8
|
const { delimiter, dirname, join } = require("node:path");
|
|
9
9
|
|
|
10
|
-
const PACKAGE = "granttap-mcp@0.8.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.71";
|
|
11
11
|
const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
|
|
12
12
|
const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
|
|
13
13
|
? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
|