granttap-mcp 0.8.105 → 0.8.107
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/engine/README.md +3 -2
- package/apps/bridge/src/engine/distribution/artifacts.ts +2 -2
- package/apps/bridge/src/engine/runtime/engine-supervisor.ts +2 -3
- package/apps/bridge/src/engine/runtime/launch/README.md +18 -0
- package/apps/bridge/src/engine/runtime/launch/index.ts +21 -0
- package/apps/bridge/src/mesh/restrictions/check.ts +3 -2
- package/apps/bridge/src/mesh/restrictions/cli.ts +6 -2
- package/apps/bridge/src/mesh/restrictions/evaluate.ts +1 -1
- package/apps/bridge/src/mesh/restrictions/guidance/README.md +27 -0
- package/apps/bridge/src/mesh/restrictions/guidance/index.ts +45 -0
- package/apps/bridge/src/mesh/restrictions/store.ts +3 -0
- package/apps/bridge/src/monitor/handlers/user-message.ts +4 -1
- package/apps/bridge/src/reply/index.ts +3 -2
- package/apps/bridge/src/reply/payload/routing.ts +3 -1
- 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
- package/packages/protocol/messages/project-policy.ts +2 -39
- package/packages/protocol/messages/restrictions/index.ts +51 -0
|
@@ -29,7 +29,8 @@ Explicit Graph builds also invoke the Engine's `graph.analyze_project` for two
|
|
|
29
29
|
to eight admitted local repositories. The projection labels these shared static
|
|
30
30
|
contracts and keeps their runtime destination unverified. They do not become
|
|
31
31
|
verified Backbone dependencies or merge Project grants. The Apple Silicon
|
|
32
|
-
artifact pins Engine 0.1.
|
|
32
|
+
artifact pins Engine 0.1.5 with Weavatrix 2.17.5, injective package import identities
|
|
33
|
+
and per-file function extent reuse during metrics analysis; unsupported Engine operations
|
|
33
34
|
leave this analysis unavailable. See `runtime/project-relationships/README.md`.
|
|
34
35
|
|
|
35
36
|
Claude, Codex, and Cursor hooks can evaluate the engine's local Project
|
|
@@ -62,7 +63,7 @@ on macOS, Linux and Windows rather than relying on a LaunchAgent environment.
|
|
|
62
63
|
Explicit disablement and a complete explicit executable/checksum pair win over
|
|
63
64
|
the saved declaration. Partial environment overrides are never mixed with a
|
|
64
65
|
different file's checksum. The supervisor still verifies the executable's bytes
|
|
65
|
-
before launch. Windows clients derive a local named pipe from the normalized
|
|
66
|
+
before launch. Its child process receives a minimum 32 MiB Rust worker stack: the default stack aborts the native parser on long, valid Rust syntax trees. Larger explicit reserves are retained. The parent process environment, Engine enablement and project policy settings are unchanged. This is a worker stack reserve, not a scan timeout or a guarantee that arbitrary inputs cannot exhaust resources. Windows clients derive a local named pipe from the normalized
|
|
66
67
|
configuration path; remote shares never become Engine endpoints.
|
|
67
68
|
The paired monitor installs the pinned managed Engine in the background when
|
|
68
69
|
needed. Catalog delivery does not await that download. Failed preparation retries
|
|
@@ -8,8 +8,8 @@ export type EngineArtifact = {
|
|
|
8
8
|
|
|
9
9
|
/** Release hashes are part of the reviewed MCP, never supplied by a remote manifest. */
|
|
10
10
|
export const ENGINE_ARTIFACTS: readonly EngineArtifact[] = [
|
|
11
|
-
{ version: "0.1.
|
|
12
|
-
sha256: "
|
|
11
|
+
{ version: "0.1.5", platform: "darwin", arch: "arm64",
|
|
12
|
+
sha256: "bd16babf11fd3d7e65b9713797b977deba1670ba41230a0e6607ce938b847455", bytes: 12202080 },
|
|
13
13
|
{ version: "0.1.2", platform: "darwin", arch: "x64",
|
|
14
14
|
sha256: "b3235a15936a77ec323d5ecbc54a0bc19994fe6e5a36044da964b68524b4c1dd", bytes: 13944984 },
|
|
15
15
|
{ version: "0.1.2", platform: "linux", arch: "x64",
|
|
@@ -8,7 +8,7 @@ import { configDir } from "../../config/runtime/paths";
|
|
|
8
8
|
import { EngineClient } from "./engine-client";
|
|
9
9
|
import { engineHealth, type EngineHealth } from "./engine-health";
|
|
10
10
|
import { EngineProtocolError } from "../protocol/engine-protocol";
|
|
11
|
-
import {
|
|
11
|
+
import { engineLaunchEnvironment } from "./launch";
|
|
12
12
|
|
|
13
13
|
export type VerifiedEngineBinary = { path: string; sha256: string };
|
|
14
14
|
|
|
@@ -133,8 +133,7 @@ function configuredBinary(env: NodeJS.ProcessEnv): VerifiedEngineBinary | undefi
|
|
|
133
133
|
|
|
134
134
|
function launchEngine(path: string, socketPath: string, env: NodeJS.ProcessEnv): ChildProcess {
|
|
135
135
|
const child = spawn(path, [], {
|
|
136
|
-
env:
|
|
137
|
-
...(process.platform === "win32" ? { GRANTTAP_ENGINE_STATE_DIR: join(configDir(), "engine", "state") } : {}) },
|
|
136
|
+
env: engineLaunchEnvironment(env, socketPath),
|
|
138
137
|
stdio: "ignore",
|
|
139
138
|
shell: false,
|
|
140
139
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Engine process launch
|
|
2
|
+
|
|
3
|
+
`index.ts` prepares the verified Engine child's environment. Rust parser workers
|
|
4
|
+
receive a minimum 32 MiB stack reserve; larger explicit reserves remain intact.
|
|
5
|
+
This prevents the reproduced daemon abort on long Rust expression trees. It does
|
|
6
|
+
not change the parent environment, enablement, policy, or provider settings.
|
|
7
|
+
Windows keeps its private Engine state directory and local endpoint mapping.
|
|
8
|
+
|
|
9
|
+
The native regression analyzes a real synthetic Rust checkout through
|
|
10
|
+
`EngineSupervisor` and confirms Graph data plus a subsequent control ping.
|
|
11
|
+
Environment tests cover disablement, invalid and larger reserves, parent
|
|
12
|
+
immutability and Windows state placement. Set `GRANTTAP_ENGINE_TEST_BINARY` to
|
|
13
|
+
an exact verified native binary to run the native case.
|
|
14
|
+
|
|
15
|
+
This is a fixed worker reserve, not a guarantee against arbitrary resource
|
|
16
|
+
exhaustion. [Engine bridge](../../README.md) owns the public runtime boundary.
|
|
17
|
+
|
|
18
|
+
License: GrantTap Commercial Source License; see the repository-root `LICENSE`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { configDir } from "../../../config/runtime/paths";
|
|
3
|
+
import { engineSocketEndpoint } from "../../startup";
|
|
4
|
+
|
|
5
|
+
const MINIMUM_PARSER_STACK_BYTES = 32 * 1_024 * 1_024;
|
|
6
|
+
|
|
7
|
+
/** Native syntax parsing runs on Rust worker threads, outside the JS stack. */
|
|
8
|
+
export function engineLaunchEnvironment(
|
|
9
|
+
env: NodeJS.ProcessEnv, socketPath: string, platform: NodeJS.Platform = process.platform,
|
|
10
|
+
): NodeJS.ProcessEnv {
|
|
11
|
+
const requestedStack = Number(env.RUST_MIN_STACK);
|
|
12
|
+
return {
|
|
13
|
+
...env,
|
|
14
|
+
GRANTTAP_ENGINE_SOCKET: engineSocketEndpoint(socketPath),
|
|
15
|
+
// Rust's default worker stack aborts the daemon on ordinary long ASTs.
|
|
16
|
+
// Scope this reserve to the verified Engine child; keep larger explicit reserves.
|
|
17
|
+
RUST_MIN_STACK: String(Number.isSafeInteger(requestedStack)
|
|
18
|
+
? Math.max(MINIMUM_PARSER_STACK_BYTES, requestedStack) : MINIMUM_PARSER_STACK_BYTES),
|
|
19
|
+
...(platform === "win32" ? { GRANTTAP_ENGINE_STATE_DIR: join(configDir(), "engine", "state") } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import type { ProjectRestrictionSet } from "../../../../../packages/protocol/schema";
|
|
4
|
+
import { hasGuidance } from "./guidance";
|
|
4
5
|
import { evaluateContent, type RestrictionViolation } from "./evaluate";
|
|
5
6
|
|
|
6
7
|
const SKIP_DIR = new Set([
|
|
@@ -62,12 +63,12 @@ function isTextish(name: string): boolean {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export function restrictionSummary(restrictions: ProjectRestrictionSet | undefined): string {
|
|
65
|
-
if (!restrictions || restrictions.rules.length === 0) return "No restrictions";
|
|
66
|
+
if (!restrictions || (restrictions.rules.length === 0 && !hasGuidance(restrictions.guidance))) return "No restrictions";
|
|
66
67
|
const count = restrictions.rules.length;
|
|
67
68
|
const scope = restrictions.scope === "sync_from_repo"
|
|
68
69
|
? "from the repository"
|
|
69
70
|
: restrictions.scope === "project_and_repo"
|
|
70
71
|
? "Project and repository"
|
|
71
72
|
: "this Project";
|
|
72
|
-
return `${count} rule${count === 1 ? "" : "s"} · ${scope}`;
|
|
73
|
+
return `${count} rule${count === 1 ? "" : "s"}${hasGuidance(restrictions.guidance) ? " + agent guidance" : ""} · ${scope}`;
|
|
73
74
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasGuidance } from "./guidance";
|
|
1
2
|
import { inspectRepository } from "../catalog";
|
|
2
3
|
import { computerId } from "../identity/computer";
|
|
3
4
|
import { localMeshStore } from "../local-remote/local";
|
|
@@ -19,7 +20,10 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
19
20
|
const restrictions = readRepoRestrictions(root, projectId ?? "ci")
|
|
20
21
|
?? (projectId ? loadRestrictions(projectId) : undefined);
|
|
21
22
|
|
|
23
|
+
const advisory = hasGuidance(restrictions?.guidance)
|
|
24
|
+
? "Agent guidance is advisory and not machine-checked by this command.\n" : "";
|
|
22
25
|
if (!restrictions || restrictions.rules.length === 0) {
|
|
26
|
+
if (advisory) return { exitCode: 0, stdout: advisory, stderr: "" };
|
|
23
27
|
return { exitCode: 0, stdout: "GrantTap restrictions: none configured.\n", stderr: "" };
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -27,7 +31,7 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
27
31
|
if (violations.length === 0) {
|
|
28
32
|
return {
|
|
29
33
|
exitCode: 0,
|
|
30
|
-
stdout: `GrantTap restrictions: ${restrictions.rules.length} rule(s) clean.\n`,
|
|
34
|
+
stdout: `GrantTap restrictions: ${restrictions.rules.length} rule(s) clean.\n${advisory}`,
|
|
31
35
|
stderr: "",
|
|
32
36
|
};
|
|
33
37
|
}
|
|
@@ -36,5 +40,5 @@ export function runRestrictionsCheck(root: string): RestrictionsCheckResult {
|
|
|
36
40
|
.map((item) => `${item.effect.toUpperCase()} ${item.reason}\n`)
|
|
37
41
|
.join("");
|
|
38
42
|
const denied = violations.some((item) => item.effect === "deny");
|
|
39
|
-
return { exitCode: denied ? 1 : 0, stdout:
|
|
43
|
+
return { exitCode: denied ? 1 : 0, stdout: advisory, stderr };
|
|
40
44
|
}
|
|
@@ -38,7 +38,7 @@ export function functionLineCounts(source: string): number[] {
|
|
|
38
38
|
|
|
39
39
|
function unparsedFunctionForm(filePath: string, content: string): boolean {
|
|
40
40
|
const extension = filePath.split(".").at(-1)?.toLowerCase();
|
|
41
|
-
if (extension
|
|
41
|
+
if (["py", "rb"].includes(extension ?? "")) return /^\s*(?:async\s+)?def\s+\w+/m.test(content);
|
|
42
42
|
if (!["ts", "tsx", "js", "jsx", "mjs", "cjs"].includes(extension ?? "")) return false;
|
|
43
43
|
if (/=>/.test(content)) return true;
|
|
44
44
|
return /^\s*(?:async\s+)?(?!(?:if|while|for|switch|catch)\b)[A-Za-z_$][\w$]*\s*\([^;\n]*\)\s*\{/m
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Project coding guidance
|
|
2
|
+
|
|
3
|
+
`projectGuidancePrompt(projectId, text)` is the public prompt boundary. It loads
|
|
4
|
+
only the accepted local restriction set for the named Project, adds its
|
|
5
|
+
advisory principles, architecture preference, design targets and exact Markdown,
|
|
6
|
+
then preserves the original request. Empty guidance is a no-op. It never edits
|
|
7
|
+
provider configuration or raises tool permissions.
|
|
8
|
+
|
|
9
|
+
Continuation delivery calls this through the existing routing prompt for all
|
|
10
|
+
supported providers. New phone Tasks use the Project returned by validated
|
|
11
|
+
workspace admission. Transfer starts use the existing Task's Project scope.
|
|
12
|
+
A loose cwd or unvalidated message Project ID does not supply instruction scope.
|
|
13
|
+
Original delivery identities, origin records and run journals retain the person's
|
|
14
|
+
original request; attached guidance is not a separately delivered prompt.
|
|
15
|
+
|
|
16
|
+
The optional protocol field is backward compatible for reading old sets. A Mac
|
|
17
|
+
that predates this contract must be upgraded before a phone sends guidance;
|
|
18
|
+
strict old protocol receivers cannot honestly acknowledge a field they reject.
|
|
19
|
+
The canonical policy receipt proves local acceptance, not semantic compliance.
|
|
20
|
+
|
|
21
|
+
The private store and `.granttap/restrictions.json` export preserve all guidance.
|
|
22
|
+
The explicit repository check reports guidance as advisory and never claims to
|
|
23
|
+
machine-check Clean Code, architecture, class or parameter targets. Existing
|
|
24
|
+
write checks own numeric enforcement; Ruby/Python function forms outside the
|
|
25
|
+
brace scanner request parser review.
|
|
26
|
+
|
|
27
|
+
License: GrantTap Commercial Source License, in the repository-root `LICENSE`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadRestrictions } from '../store';
|
|
2
|
+
import type { ProjectRestrictionGuidance } from '../../../../../../packages/protocol/messages/restrictions';
|
|
3
|
+
|
|
4
|
+
const principles: Record<string, string> = {
|
|
5
|
+
single_responsibility: 'Keep each module focused on one responsibility and reason to change.',
|
|
6
|
+
small_interfaces: 'Expose small explicit public interfaces; hide implementation details.',
|
|
7
|
+
composition: 'Prefer composition to inheritance when it reduces coupling.',
|
|
8
|
+
dependency_inversion: 'Keep domain logic independent of infrastructure through explicit boundaries.',
|
|
9
|
+
behavior_tests: 'Test observable behavior and durable state, including failure paths.',
|
|
10
|
+
clear_names: 'Use clear domain names; keep control flow readable and nesting shallow.',
|
|
11
|
+
explicit_errors: 'Handle failures explicitly; do not silently swallow errors.',
|
|
12
|
+
no_duplication: 'Remove duplicated knowledge without premature abstraction.',
|
|
13
|
+
};
|
|
14
|
+
const architectures: Record<string, string> = {
|
|
15
|
+
preserve: 'Preserve the established architecture and public boundaries.',
|
|
16
|
+
feature_modules: 'Prefer feature modules that own behavior, public entry points and local tests.',
|
|
17
|
+
layered: 'Prefer explicit presentation, application, domain and infrastructure layers.',
|
|
18
|
+
clean: 'Prefer Clean Architecture: dependencies point toward independent domain and use cases.',
|
|
19
|
+
hexagonal: 'Prefer hexagonal architecture with domain ports and external adapters.',
|
|
20
|
+
modular_monolith: 'Prefer a modular monolith with explicit boundaries between business modules.',
|
|
21
|
+
mvvm: 'Prefer MVVM for presentation, with testable state and domain logic outside views.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function hasGuidance(value: ProjectRestrictionGuidance | undefined): boolean {
|
|
25
|
+
return !!value && (value.principles.length > 0 || value.architecture !== 'preserve'
|
|
26
|
+
|| value.markdown.trim().length > 0 || value.classLineTarget != null || value.parameterTarget != null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** projectId comes from the published session or validated task admission, never a loose path. */
|
|
30
|
+
export function projectGuidancePrompt(projectId: string | undefined, text: string): string {
|
|
31
|
+
if (!projectId) return text;
|
|
32
|
+
const value = loadRestrictions(projectId)?.guidance;
|
|
33
|
+
if (!value || !hasGuidance(value)) return text;
|
|
34
|
+
const lines = [
|
|
35
|
+
'GrantTap Project guidance (advisory, applies to this Project):',
|
|
36
|
+
'Follow applicable higher-priority instructions and enforced policies. These preferences do not grant permissions.',
|
|
37
|
+
architectures[value.architecture]!,
|
|
38
|
+
'Apply preferences incrementally; do not rewrite the architecture without a task that calls for it.',
|
|
39
|
+
...value.principles.map(key => principles[key]!),
|
|
40
|
+
];
|
|
41
|
+
if (value.classLineTarget != null) lines.push(`Aim for classes/types no longer than ${value.classLineTarget} lines; advisory target.`);
|
|
42
|
+
if (value.parameterTarget != null) lines.push(`Aim for at most ${value.parameterTarget} parameters per function; advisory target.`);
|
|
43
|
+
if (value.markdown.trim()) lines.push('Project Markdown instructions:', value.markdown);
|
|
44
|
+
return `${lines.join('\n')}\n\nCurrent request:\n${text}`;
|
|
45
|
+
}
|
|
@@ -68,6 +68,7 @@ export function readRepoRestrictions(
|
|
|
68
68
|
scope?: unknown;
|
|
69
69
|
repositoryId?: unknown;
|
|
70
70
|
rules?: unknown;
|
|
71
|
+
guidance?: unknown;
|
|
71
72
|
};
|
|
72
73
|
const parsed = RestrictionSchema.safeParse({
|
|
73
74
|
projectId,
|
|
@@ -75,6 +76,7 @@ export function readRepoRestrictions(
|
|
|
75
76
|
scope: raw.scope,
|
|
76
77
|
repositoryId: raw.repositoryId,
|
|
77
78
|
rules: raw.rules,
|
|
79
|
+
guidance: raw.guidance,
|
|
78
80
|
source: "repo",
|
|
79
81
|
});
|
|
80
82
|
return parsed.success ? parsed.data : undefined;
|
|
@@ -93,6 +95,7 @@ export function writeRepoRestrictions(root: string, restrictions: ProjectRestric
|
|
|
93
95
|
scope: restrictions.scope,
|
|
94
96
|
repositoryId: restrictions.repositoryId,
|
|
95
97
|
rules: restrictions.rules,
|
|
98
|
+
guidance: restrictions.guidance,
|
|
96
99
|
source: restrictions.source,
|
|
97
100
|
}, null, 2)}\n`, { mode: 0o644 });
|
|
98
101
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectGuidancePrompt } from "../../mesh/restrictions/guidance";
|
|
1
2
|
import type { UserMessage } from "../../../../../packages/protocol/schema";
|
|
2
3
|
import { ATTACHMENT_MISSING_ERROR } from "../../../../../packages/protocol/schema";
|
|
3
4
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
@@ -42,6 +43,7 @@ async function startNewAgentTask(input: {
|
|
|
42
43
|
return "rejected";
|
|
43
44
|
}
|
|
44
45
|
const requestedCwd = message.cwd?.trim();
|
|
46
|
+
let admittedProjectId: string | undefined;
|
|
45
47
|
if (requestedCwd) {
|
|
46
48
|
const admitted = evaluateCreateTask({
|
|
47
49
|
cwd: requestedCwd, agent, model: message.model,
|
|
@@ -52,6 +54,7 @@ async function startNewAgentTask(input: {
|
|
|
52
54
|
await say(admitted.detail, undefined, true);
|
|
53
55
|
return "rejected";
|
|
54
56
|
}
|
|
57
|
+
admittedProjectId = admitted.projectId;
|
|
55
58
|
if ("queued" in admitted && admitted.queued) {
|
|
56
59
|
// The phone's durable outbox retries after reconnect. This monitor has
|
|
57
60
|
// no worker for its separate pinned-task queue, so do not launch here.
|
|
@@ -87,7 +90,7 @@ async function startNewAgentTask(input: {
|
|
|
87
90
|
}[agent];
|
|
88
91
|
let run: Promise<ReplyResult>;
|
|
89
92
|
try {
|
|
90
|
-
run = create(message.text, requestedCwd, PHONE_RUN_LIMIT_MS, resolved.attachments,
|
|
93
|
+
run = create(projectGuidancePrompt(admittedProjectId, message.text), requestedCwd, PHONE_RUN_LIMIT_MS, resolved.attachments,
|
|
91
94
|
message.model, undefined, message.effort).catch(() => {
|
|
92
95
|
input.origin?.registry.complete(input.origin.scope, { ok: true });
|
|
93
96
|
return { ok: false, error: 'The provider task stopped unexpectedly.' } as ReplyResult;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectGuidancePrompt } from "../mesh/restrictions/guidance";
|
|
1
2
|
/** Deliver phone messages into existing sessions or start a new Codex task. */
|
|
2
3
|
import { localTaskTransfers, localTransferContinuations } from "../task-transfer";
|
|
3
4
|
import { localRoutingRequests } from '../task-routing/requests';
|
|
@@ -66,13 +67,13 @@ export function deliverToSession(
|
|
|
66
67
|
return queued(session.sessionId, async () => (await localTaskTransfers().deliver(
|
|
67
68
|
session.sessionId, text, attachments, options, {
|
|
68
69
|
start: request => request.provider === "grok"
|
|
69
|
-
? createGrokSession(request.text, request.cwd, timeoutMs, request.attachments,
|
|
70
|
+
? createGrokSession(projectGuidancePrompt(session.projectId, request.text), request.cwd, timeoutMs, request.attachments,
|
|
70
71
|
request.model, request.operationKey, request.effort, nativeId => {
|
|
71
72
|
request.onNativeSession(nativeId);
|
|
72
73
|
options.onAgentActivity?.(nativeId);
|
|
73
74
|
})
|
|
74
75
|
: ({ claude: createClaudeSession, codex: createCodexSession,
|
|
75
|
-
cursor: createCursorSession }[request.provider])(request.text, request.cwd,
|
|
76
|
+
cursor: createCursorSession }[request.provider])(projectGuidancePrompt(session.projectId, request.text), request.cwd,
|
|
76
77
|
timeoutMs, request.attachments, request.model, request.operationKey, request.effort),
|
|
77
78
|
resume: (native, body, images, settings) => withAttachments(images, body, prepared => runDelivery(native,
|
|
78
79
|
native.agent === "codex" ? prepared.prompt : prepared.claudePrompt, timeoutMs, prepared.images, settings)),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectGuidancePrompt } from "../../mesh/restrictions/guidance";
|
|
1
2
|
import type { SessionInfo } from "../../../../../packages/protocol/schema";
|
|
2
3
|
import type { DeliveryOptions } from "./types";
|
|
3
4
|
|
|
@@ -14,5 +15,6 @@ export function routingPrompt(
|
|
|
14
15
|
const invocation = session.agent === "claude" ? `/${options.skill}` : `$${options.skill}`;
|
|
15
16
|
instructions.push(`Use the project skill ${invocation} for this request.`);
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
const routed = instructions.length === 0 ? text : `${instructions.join("\n")}\n\n${text}`;
|
|
19
|
+
return projectGuidancePrompt(session.projectId, routed);
|
|
18
20
|
}
|
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.107
|
|
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.107\";\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 22.13.0+ 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.107";
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ProjectRestrictionSet } from "./restrictions";
|
|
3
|
+
export * from "./restrictions";
|
|
2
4
|
import { Identifier, Label, MeshProvider } from "./mesh/endpoint";
|
|
3
5
|
|
|
4
6
|
export const ProjectCapabilityKind = z.enum([
|
|
@@ -89,45 +91,6 @@ export const ProjectExecutionPolicy = z.object({
|
|
|
89
91
|
});
|
|
90
92
|
export type ProjectExecutionPolicy = z.infer<typeof ProjectExecutionPolicy>;
|
|
91
93
|
|
|
92
|
-
/** Quality gates a Project can share with CI and its repository. */
|
|
93
|
-
export const ProjectRestrictionKind = z.enum([
|
|
94
|
-
"max_file_lines", "max_function_lines", "max_file_bytes", "custom",
|
|
95
|
-
]);
|
|
96
|
-
export type ProjectRestrictionKind = z.infer<typeof ProjectRestrictionKind>;
|
|
97
|
-
export const ProjectRestrictionScope = z.enum([
|
|
98
|
-
"project", "project_and_repo", "sync_from_repo",
|
|
99
|
-
]);
|
|
100
|
-
export type ProjectRestrictionScope = z.infer<typeof ProjectRestrictionScope>;
|
|
101
|
-
export const ProjectRestrictionRule = z.object({
|
|
102
|
-
ruleId: Identifier,
|
|
103
|
-
kind: ProjectRestrictionKind,
|
|
104
|
-
limit: z.number().int().positive().max(1_000_000).optional(),
|
|
105
|
-
name: Label.optional(),
|
|
106
|
-
paths: z.array(z.string().trim().min(1).max(256)).max(16).optional(),
|
|
107
|
-
effect: z.enum(["ask", "deny"]).default("deny"),
|
|
108
|
-
}).strict().superRefine((value, ctx) => {
|
|
109
|
-
if (value.kind !== "custom" && value.limit == null) {
|
|
110
|
-
ctx.addIssue({
|
|
111
|
-
code: z.ZodIssueCode.custom, path: ["limit"], message: "limit required",
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
if (value.kind === "custom" && !value.name) {
|
|
115
|
-
ctx.addIssue({
|
|
116
|
-
code: z.ZodIssueCode.custom, path: ["name"], message: "custom needs a name",
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
export type ProjectRestrictionRule = z.infer<typeof ProjectRestrictionRule>;
|
|
121
|
-
export const ProjectRestrictionSet = z.object({
|
|
122
|
-
projectId: Identifier,
|
|
123
|
-
revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
124
|
-
scope: ProjectRestrictionScope,
|
|
125
|
-
repositoryId: z.string().trim().min(1).max(512).optional(),
|
|
126
|
-
rules: z.array(ProjectRestrictionRule).max(32),
|
|
127
|
-
source: z.enum(["phone", "repo"]).default("phone"),
|
|
128
|
-
}).strict();
|
|
129
|
-
export type ProjectRestrictionSet = z.infer<typeof ProjectRestrictionSet>;
|
|
130
|
-
|
|
131
94
|
/** Shared Project environment. Secret values travel under the Project key only. */
|
|
132
95
|
export const ProjectEnvVar = z.object({
|
|
133
96
|
key: z.string().regex(/^[A-Z][A-Z0-9_]{0,127}$/),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Identifier, Label } from '../mesh/endpoint';
|
|
3
|
+
|
|
4
|
+
export const ProjectRestrictionKind = z.enum([
|
|
5
|
+
'max_file_lines', 'max_function_lines', 'max_file_bytes', 'custom',
|
|
6
|
+
]);
|
|
7
|
+
export type ProjectRestrictionKind = z.infer<typeof ProjectRestrictionKind>;
|
|
8
|
+
export const ProjectRestrictionScope = z.enum(['project', 'project_and_repo', 'sync_from_repo']);
|
|
9
|
+
export type ProjectRestrictionScope = z.infer<typeof ProjectRestrictionScope>;
|
|
10
|
+
export const ProjectRestrictionRule = z.object({
|
|
11
|
+
ruleId: Identifier, kind: ProjectRestrictionKind,
|
|
12
|
+
limit: z.number().int().positive().max(1_000_000).optional(),
|
|
13
|
+
name: Label.optional(),
|
|
14
|
+
paths: z.array(z.string().trim().min(1).max(256)).max(16).optional(),
|
|
15
|
+
effect: z.enum(['ask', 'deny']).default('deny'),
|
|
16
|
+
}).strict().superRefine((value, ctx) => {
|
|
17
|
+
if (value.kind !== 'custom' && value.limit == null) {
|
|
18
|
+
ctx.addIssue({ code: 'custom', path: ['limit'], message: 'limit required' });
|
|
19
|
+
}
|
|
20
|
+
if (value.kind === 'custom' && !value.name) {
|
|
21
|
+
ctx.addIssue({ code: 'custom', path: ['name'], message: 'custom needs a name' });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
export type ProjectRestrictionRule = z.infer<typeof ProjectRestrictionRule>;
|
|
25
|
+
|
|
26
|
+
/** Agent preferences, never a claim that a semantic rule has a machine checker. */
|
|
27
|
+
export const ProjectRestrictionGuidance = z.object({
|
|
28
|
+
principles: z.array(z.enum([
|
|
29
|
+
'single_responsibility', 'small_interfaces', 'composition', 'dependency_inversion',
|
|
30
|
+
'behavior_tests', 'clear_names', 'explicit_errors', 'no_duplication',
|
|
31
|
+
])).max(8).refine(items => new Set(items).size === items.length, 'duplicate principle'),
|
|
32
|
+
architecture: z.enum([
|
|
33
|
+
'preserve', 'feature_modules', 'layered', 'clean', 'hexagonal', 'modular_monolith', 'mvvm',
|
|
34
|
+
]),
|
|
35
|
+
classLineTarget: z.number().int().positive().max(1_000_000).optional(),
|
|
36
|
+
parameterTarget: z.number().int().positive().max(1_000).optional(),
|
|
37
|
+
markdown: z.string().refine(text => !text.includes('\0') && new TextEncoder().encode(text).length <= 12_000,
|
|
38
|
+
'Markdown must fit 12000 UTF-8 bytes and contain no NUL'),
|
|
39
|
+
}).strict();
|
|
40
|
+
export type ProjectRestrictionGuidance = z.infer<typeof ProjectRestrictionGuidance>;
|
|
41
|
+
|
|
42
|
+
export const ProjectRestrictionSet = z.object({
|
|
43
|
+
projectId: Identifier,
|
|
44
|
+
revision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
45
|
+
scope: ProjectRestrictionScope,
|
|
46
|
+
repositoryId: z.string().trim().min(1).max(512).optional(),
|
|
47
|
+
rules: z.array(ProjectRestrictionRule).max(32),
|
|
48
|
+
source: z.enum(['phone', 'repo']).default('phone'),
|
|
49
|
+
guidance: ProjectRestrictionGuidance.optional(),
|
|
50
|
+
}).strict();
|
|
51
|
+
export type ProjectRestrictionSet = z.infer<typeof ProjectRestrictionSet>;
|