@flyingrobots/graft 0.4.0 → 0.5.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 (111) hide show
  1. package/ARCHITECTURE.md +386 -0
  2. package/CHANGELOG.md +47 -0
  3. package/CODE_OF_CONDUCT.md +65 -0
  4. package/README.md +153 -17
  5. package/bin/graft.js +4 -14
  6. package/docs/ADVANCED_GUIDE.md +49 -0
  7. package/docs/CLI.md +43 -0
  8. package/docs/GUIDE.md +321 -32
  9. package/docs/MCP.md +44 -0
  10. package/package.json +15 -4
  11. package/src/adapters/node-fs.ts +4 -0
  12. package/src/adapters/node-git.ts +47 -0
  13. package/src/adapters/node-process-runner.ts +27 -0
  14. package/src/cli/index-cmd.ts +75 -11
  15. package/src/cli/init.ts +808 -57
  16. package/src/cli/main.ts +437 -0
  17. package/src/contracts/capabilities.ts +341 -0
  18. package/src/contracts/causal-ontology.ts +622 -0
  19. package/src/contracts/causal-surface-next-action.ts +18 -0
  20. package/src/contracts/output-schemas.ts +1169 -0
  21. package/src/git/diff.ts +25 -21
  22. package/src/git/target-git-hook-bootstrap.ts +56 -0
  23. package/src/hooks/posttooluse-read.ts +21 -74
  24. package/src/hooks/pretooluse-read.ts +20 -56
  25. package/src/hooks/read-governor.ts +95 -0
  26. package/src/hooks/read-messages.ts +53 -0
  27. package/src/mcp/burden.ts +123 -0
  28. package/src/mcp/cache.ts +51 -0
  29. package/src/mcp/cached-file.ts +10 -8
  30. package/src/mcp/context.ts +65 -2
  31. package/src/mcp/daemon-control-plane.ts +554 -0
  32. package/src/mcp/daemon-job-scheduler.ts +279 -0
  33. package/src/mcp/daemon-repos.ts +216 -0
  34. package/src/mcp/daemon-server.ts +396 -0
  35. package/src/mcp/daemon-worker-pool.ts +310 -0
  36. package/src/mcp/daemon-worker-process.ts +52 -0
  37. package/src/mcp/metrics.ts +108 -1
  38. package/src/mcp/monitor-tick-job.ts +99 -0
  39. package/src/mcp/persisted-local-history.ts +1246 -0
  40. package/src/mcp/persistent-monitor-runtime.ts +549 -0
  41. package/src/mcp/policy.ts +84 -0
  42. package/src/mcp/receipt.ts +82 -12
  43. package/src/mcp/repo-concurrency.ts +318 -0
  44. package/src/mcp/repo-state.ts +777 -0
  45. package/src/mcp/repo-tool-job.ts +302 -0
  46. package/src/mcp/run-capture-config.ts +33 -0
  47. package/src/mcp/runtime-causal-context.ts +72 -0
  48. package/src/mcp/runtime-observability.ts +219 -0
  49. package/src/mcp/runtime-staged-target.ts +161 -0
  50. package/src/mcp/runtime-workspace-overlay.ts +255 -0
  51. package/src/mcp/semantic-transition-guidance.ts +60 -0
  52. package/src/mcp/semantic-transition-summary.ts +130 -0
  53. package/src/mcp/server.ts +696 -55
  54. package/src/mcp/stdio-server.ts +12 -0
  55. package/src/mcp/stdio.ts +2 -5
  56. package/src/mcp/tools/activity-view.ts +325 -0
  57. package/src/mcp/tools/causal-attach.ts +67 -0
  58. package/src/mcp/tools/causal-status.ts +58 -0
  59. package/src/mcp/tools/changed-since.ts +13 -11
  60. package/src/mcp/tools/code-find.ts +164 -0
  61. package/src/mcp/tools/code-refs.ts +466 -0
  62. package/src/mcp/tools/code-show.ts +252 -0
  63. package/src/mcp/tools/daemon-monitors.ts +14 -0
  64. package/src/mcp/tools/daemon-repos.ts +22 -0
  65. package/src/mcp/tools/daemon-sessions.ts +14 -0
  66. package/src/mcp/tools/daemon-status.ts +12 -0
  67. package/src/mcp/tools/doctor.ts +45 -2
  68. package/src/mcp/tools/explain.ts +4 -0
  69. package/src/mcp/tools/file-outline.ts +7 -3
  70. package/src/mcp/tools/git-files.ts +73 -0
  71. package/src/mcp/tools/graft-diff.ts +12 -4
  72. package/src/mcp/tools/map.ts +92 -38
  73. package/src/mcp/tools/monitor-pause.ts +18 -0
  74. package/src/mcp/tools/monitor-resume.ts +18 -0
  75. package/src/mcp/tools/monitor-start.ts +20 -0
  76. package/src/mcp/tools/monitor-stop.ts +18 -0
  77. package/src/mcp/tools/precision-match.ts +51 -0
  78. package/src/mcp/tools/precision-query.ts +127 -0
  79. package/src/mcp/tools/precision.ts +312 -0
  80. package/src/mcp/tools/run-capture.ts +126 -44
  81. package/src/mcp/tools/safe-read.ts +14 -12
  82. package/src/mcp/tools/since.ts +7 -2
  83. package/src/mcp/tools/state.ts +11 -3
  84. package/src/mcp/tools/stats.ts +5 -1
  85. package/src/mcp/tools/workspace-authorizations.ts +14 -0
  86. package/src/mcp/tools/workspace-authorize.ts +20 -0
  87. package/src/mcp/tools/workspace-bind.ts +25 -0
  88. package/src/mcp/tools/workspace-rebind.ts +25 -0
  89. package/src/mcp/tools/workspace-revoke.ts +18 -0
  90. package/src/mcp/tools/workspace-status.ts +12 -0
  91. package/src/mcp/warp-pool.ts +36 -0
  92. package/src/mcp/workspace-router.ts +984 -0
  93. package/src/operations/file-outline.ts +12 -2
  94. package/src/operations/graft-diff.ts +56 -10
  95. package/src/operations/safe-read.ts +27 -4
  96. package/src/operations/state.ts +6 -9
  97. package/src/parser/lang.ts +19 -3
  98. package/src/parser/outline.ts +191 -2
  99. package/src/parser/types.ts +9 -1
  100. package/src/policy/types.ts +4 -3
  101. package/src/ports/filesystem.ts +1 -0
  102. package/src/ports/git.ts +16 -0
  103. package/src/ports/process-runner.ts +22 -0
  104. package/src/release/security-gate.ts +102 -0
  105. package/src/session/tracker.ts +31 -0
  106. package/src/version.ts +3 -0
  107. package/src/warp/indexer.ts +171 -56
  108. package/src/warp/observers.ts +1 -1
  109. package/src/warp/open.ts +4 -3
  110. package/src/warp/plumbing.d.ts +5 -1
  111. package/src/warp/writer-id.ts +30 -0
@@ -3,26 +3,69 @@ import { z } from "zod";
3
3
  import { extractOutline } from "../../parser/outline.js";
4
4
  import { detectLang } from "../../parser/lang.js";
5
5
  import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
6
- import { execFileSync } from "node:child_process";
6
+ import { GitFileQuery, listGitFiles } from "./git-files.js";
7
+ import { evaluateMcpRefusal, type McpPolicyRefusal } from "../policy.js";
8
+ import { collectSymbols, normalizeRepoPath } from "./precision.js";
7
9
 
8
- interface FileEntry {
10
+ class StructuralMapRequest {
11
+ readonly directory: string;
12
+
13
+ constructor(args: Record<string, unknown>, projectRoot: string) {
14
+ const rawPath = args["path"];
15
+ if (rawPath !== undefined && typeof rawPath !== "string") {
16
+ throw new Error("StructuralMapRequest: path must be a string when provided");
17
+ }
18
+ this.directory = rawPath !== undefined && rawPath.trim().length > 0
19
+ ? normalizeRepoPath(projectRoot, rawPath)
20
+ : ".";
21
+ Object.freeze(this);
22
+ }
23
+
24
+ toGitFileQuery(projectRoot: string): GitFileQuery {
25
+ return GitFileQuery.project(projectRoot, this.directory === "." ? "" : this.directory);
26
+ }
27
+ }
28
+
29
+ class StructuralMapSymbol {
30
+ readonly name: string;
31
+ readonly kind: string;
32
+ readonly signature?: string;
33
+ readonly exported: boolean;
34
+ readonly startLine?: number;
35
+ readonly endLine?: number;
36
+
37
+ constructor(opts: {
38
+ name: string;
39
+ kind: string;
40
+ signature?: string;
41
+ exported: boolean;
42
+ startLine?: number;
43
+ endLine?: number;
44
+ }) {
45
+ this.name = opts.name;
46
+ this.kind = opts.kind;
47
+ this.exported = opts.exported;
48
+ if (opts.signature !== undefined) this.signature = opts.signature;
49
+ if (opts.startLine !== undefined) this.startLine = opts.startLine;
50
+ if (opts.endLine !== undefined) this.endLine = opts.endLine;
51
+ Object.freeze(this);
52
+ }
53
+ }
54
+
55
+ class StructuralMapFile {
9
56
  path: string;
10
57
  lang: string;
11
- symbols: { name: string; kind: string; signature?: string | undefined; exported: boolean; startLine?: number | undefined; endLine?: number | undefined }[];
12
- }
58
+ symbols: readonly StructuralMapSymbol[];
13
59
 
14
- /**
15
- * List files in a directory (git ls-files for tracked files).
16
- */
17
- function listFiles(dirPath: string, cwd: string): string[] {
18
- try {
19
- const args = dirPath.length > 0
20
- ? ["ls-files", "--", dirPath]
21
- : ["ls-files"];
22
- return execFileSync("git", args, { cwd, encoding: "utf-8" })
23
- .trim().split("\n").filter((l) => l.length > 0);
24
- } catch {
25
- return [];
60
+ constructor(opts: {
61
+ path: string;
62
+ lang: string;
63
+ symbols: readonly StructuralMapSymbol[];
64
+ }) {
65
+ this.path = opts.path;
66
+ this.lang = opts.lang;
67
+ this.symbols = Object.freeze([...opts.symbols]);
68
+ Object.freeze(this);
26
69
  }
27
70
  }
28
71
 
@@ -36,45 +79,56 @@ export const mapTool: ToolDefinition = {
36
79
  path: z.string().optional(),
37
80
  },
38
81
  createHandler(ctx: ToolContext): ToolHandler {
39
- return (args) => {
40
- const dirPath = (args["path"] as string | undefined) ?? "";
82
+ return async (args) => {
83
+ const request = new StructuralMapRequest(args, ctx.projectRoot);
41
84
 
42
- const filePaths = listFiles(dirPath, ctx.projectRoot);
43
- const files: FileEntry[] = [];
85
+ const filePaths = (await listGitFiles(request.toGitFileQuery(ctx.projectRoot), ctx.git)).paths;
86
+ const files: StructuralMapFile[] = [];
87
+ const refused: McpPolicyRefusal[] = [];
44
88
 
45
89
  for (const filePath of filePaths) {
46
- const lang = detectLang(filePath);
47
- if (lang === null) continue;
48
-
49
90
  let content: string;
50
91
  try {
51
- content = ctx.fs.readFileSync(path.join(ctx.projectRoot, filePath), "utf-8");
92
+ content = await ctx.fs.readFile(path.join(ctx.projectRoot, filePath), "utf-8");
52
93
  } catch {
53
94
  continue;
54
95
  }
55
96
 
97
+ const actual = {
98
+ lines: content.split("\n").length,
99
+ bytes: Buffer.byteLength(content),
100
+ };
101
+ const refusal = evaluateMcpRefusal(ctx, filePath, actual);
102
+ if (refusal !== null) {
103
+ refused.push(refusal);
104
+ continue;
105
+ }
106
+
107
+ const lang = detectLang(filePath);
108
+ if (lang === null) continue;
109
+
56
110
  const result = extractOutline(content, lang);
57
- const symbols: FileEntry["symbols"] = result.entries.map((entry) => {
58
- const jump = result.jumpTable?.find((j) => j.symbol === entry.name);
59
- return {
60
- name: entry.name,
61
- kind: entry.kind,
62
- signature: entry.signature,
63
- exported: entry.exported,
64
- startLine: jump?.start,
65
- endLine: jump?.end,
66
- };
67
- });
68
-
69
- files.push({ path: filePath, lang, symbols });
111
+ const symbols = collectSymbols(result.entries, filePath, result.jumpTable ?? []).map((symbol) =>
112
+ new StructuralMapSymbol({
113
+ name: symbol.name,
114
+ kind: symbol.kind,
115
+ exported: symbol.exported,
116
+ ...(symbol.signature !== undefined ? { signature: symbol.signature } : {}),
117
+ ...(symbol.startLine !== undefined ? { startLine: symbol.startLine } : {}),
118
+ ...(symbol.endLine !== undefined ? { endLine: symbol.endLine } : {}),
119
+ })
120
+ );
121
+
122
+ files.push(new StructuralMapFile({ path: filePath, lang, symbols }));
70
123
  }
71
124
 
72
125
  files.sort((a, b) => a.path.localeCompare(b.path));
73
126
  const totalSymbols = files.reduce((n, f) => n + f.symbols.length, 0);
74
127
 
75
128
  return ctx.respond("graft_map", {
76
- directory: dirPath.length > 0 ? dirPath : ".",
129
+ directory: request.directory,
77
130
  files,
131
+ ...(refused.length > 0 ? { refused } : {}),
78
132
  summary: `${String(files.length)} files, ${String(totalSymbols)} symbols`,
79
133
  });
80
134
  };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
3
+
4
+ export const monitorPauseTool: ToolDefinition = {
5
+ name: "monitor_pause",
6
+ description:
7
+ "Pause the repo-scoped background index monitor for an authorized workspace.",
8
+ schema: {
9
+ cwd: z.string(),
10
+ },
11
+ createHandler(ctx: ToolContext): ToolHandler {
12
+ return async (args) => {
13
+ return ctx.respond("monitor_pause", { ...await ctx.pauseMonitor({
14
+ cwd: args["cwd"] as string,
15
+ }) });
16
+ };
17
+ },
18
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
3
+
4
+ export const monitorResumeTool: ToolDefinition = {
5
+ name: "monitor_resume",
6
+ description:
7
+ "Resume a paused repo-scoped background index monitor for an authorized workspace.",
8
+ schema: {
9
+ cwd: z.string(),
10
+ },
11
+ createHandler(ctx: ToolContext): ToolHandler {
12
+ return async (args) => {
13
+ return ctx.respond("monitor_resume", { ...await ctx.resumeMonitor({
14
+ cwd: args["cwd"] as string,
15
+ }) });
16
+ };
17
+ },
18
+ };
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
3
+
4
+ export const monitorStartTool: ToolDefinition = {
5
+ name: "monitor_start",
6
+ description:
7
+ "Start or resume the repo-scoped background index monitor for an authorized workspace.",
8
+ schema: {
9
+ cwd: z.string(),
10
+ pollIntervalMs: z.number().int().positive().optional(),
11
+ },
12
+ createHandler(ctx: ToolContext): ToolHandler {
13
+ return async (args) => {
14
+ return ctx.respond("monitor_start", { ...await ctx.startMonitor({
15
+ cwd: args["cwd"] as string,
16
+ pollIntervalMs: args["pollIntervalMs"] as number | undefined,
17
+ }) });
18
+ };
19
+ },
20
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
3
+
4
+ export const monitorStopTool: ToolDefinition = {
5
+ name: "monitor_stop",
6
+ description:
7
+ "Stop the repo-scoped background index monitor for an authorized workspace.",
8
+ schema: {
9
+ cwd: z.string(),
10
+ },
11
+ createHandler(ctx: ToolContext): ToolHandler {
12
+ return async (args) => {
13
+ return ctx.respond("monitor_stop", { ...await ctx.stopMonitor({
14
+ cwd: args["cwd"] as string,
15
+ }) });
16
+ };
17
+ },
18
+ };
@@ -0,0 +1,51 @@
1
+ export class PrecisionSymbolMatch {
2
+ readonly name: string;
3
+ readonly kind: string;
4
+ readonly path: string;
5
+ readonly signature?: string;
6
+ readonly exported: boolean;
7
+ readonly startLine?: number;
8
+ readonly endLine?: number;
9
+
10
+ constructor(opts: {
11
+ name: string;
12
+ kind: string;
13
+ path: string;
14
+ signature?: string;
15
+ exported: boolean;
16
+ startLine?: number;
17
+ endLine?: number;
18
+ }) {
19
+ if (opts.name.trim().length === 0) {
20
+ throw new Error("PrecisionSymbolMatch: name must be non-empty");
21
+ }
22
+ if (opts.kind.trim().length === 0) {
23
+ throw new Error("PrecisionSymbolMatch: kind must be non-empty");
24
+ }
25
+ if (opts.path.trim().length === 0) {
26
+ throw new Error("PrecisionSymbolMatch: path must be non-empty");
27
+ }
28
+ if (opts.startLine !== undefined && (!Number.isInteger(opts.startLine) || opts.startLine < 1)) {
29
+ throw new Error("PrecisionSymbolMatch: startLine must be an integer >= 1");
30
+ }
31
+ if (opts.endLine !== undefined && (!Number.isInteger(opts.endLine) || opts.endLine < 1)) {
32
+ throw new Error("PrecisionSymbolMatch: endLine must be an integer >= 1");
33
+ }
34
+ if (
35
+ opts.startLine !== undefined &&
36
+ opts.endLine !== undefined &&
37
+ opts.endLine < opts.startLine
38
+ ) {
39
+ throw new Error("PrecisionSymbolMatch: endLine must be >= startLine");
40
+ }
41
+
42
+ this.name = opts.name.trim();
43
+ this.kind = opts.kind.trim();
44
+ this.path = opts.path.trim();
45
+ this.exported = opts.exported;
46
+ if (opts.signature !== undefined) this.signature = opts.signature;
47
+ if (opts.startLine !== undefined) this.startLine = opts.startLine;
48
+ if (opts.endLine !== undefined) this.endLine = opts.endLine;
49
+ Object.freeze(this);
50
+ }
51
+ }
@@ -0,0 +1,127 @@
1
+ import picomatch from "picomatch";
2
+ import { PrecisionSymbolMatch } from "./precision-match.js";
3
+
4
+ type SymbolQueryMode = "glob" | "plain";
5
+
6
+ export class RankedPrecisionSymbolMatch {
7
+ readonly match: PrecisionSymbolMatch;
8
+ readonly score: number;
9
+
10
+ constructor(opts: {
11
+ match: PrecisionSymbolMatch;
12
+ score: number;
13
+ }) {
14
+ if (opts.score < 0) {
15
+ throw new Error("RankedPrecisionSymbolMatch: score must be >= 0");
16
+ }
17
+ this.match = opts.match;
18
+ this.score = opts.score;
19
+ Object.freeze(this);
20
+ }
21
+ }
22
+
23
+ class PrecisionSymbolQuery {
24
+ readonly text: string;
25
+ readonly mode: SymbolQueryMode;
26
+ readonly #globMatcher?: (name: string) => boolean;
27
+
28
+ constructor(query: string) {
29
+ const normalizedQuery = query.trim();
30
+ if (normalizedQuery.length === 0) {
31
+ throw new Error("PrecisionSymbolQuery: query must be non-empty");
32
+ }
33
+ this.text = normalizedQuery;
34
+ if (picomatch.scan(normalizedQuery).isGlob) {
35
+ this.mode = "glob";
36
+ this.#globMatcher = picomatch(normalizedQuery, { nocase: true });
37
+ } else {
38
+ this.mode = "plain";
39
+ }
40
+ Object.freeze(this);
41
+ }
42
+
43
+ score(name: string): number | null {
44
+ if (this.mode === "glob") {
45
+ return this.#globMatcher?.(name) === true ? 0 : null;
46
+ }
47
+
48
+ const loweredName = name.toLowerCase();
49
+ const loweredQuery = this.text.toLowerCase();
50
+ if (name === this.text) return 0;
51
+ if (loweredName === loweredQuery) return 1;
52
+ if (name.startsWith(this.text)) return 2;
53
+ if (loweredName.startsWith(loweredQuery)) return 3;
54
+ return loweredName.includes(loweredQuery) ? 4 : null;
55
+ }
56
+ }
57
+
58
+ export class PrecisionSearchRequest {
59
+ readonly exactName?: string;
60
+ readonly query?: PrecisionSymbolQuery;
61
+ readonly kind?: string;
62
+ readonly filePath?: string;
63
+ readonly pathPrefix?: string;
64
+ readonly ceiling?: number;
65
+
66
+ constructor(opts: {
67
+ exactName?: string;
68
+ query?: string;
69
+ kind?: string;
70
+ filePath?: string;
71
+ pathPrefix?: string;
72
+ ceiling?: number;
73
+ }) {
74
+ const exactName = opts.exactName?.trim();
75
+ const query = opts.query?.trim();
76
+ const kind = opts.kind?.trim().toLowerCase();
77
+ const filePath = opts.filePath?.trim();
78
+ const pathPrefix = opts.pathPrefix?.trim();
79
+
80
+ if ((exactName?.length ?? 0) === 0 && (query?.length ?? 0) === 0) {
81
+ throw new Error("PrecisionSearchRequest: exactName or query is required");
82
+ }
83
+ if (
84
+ opts.ceiling !== undefined &&
85
+ (!Number.isInteger(opts.ceiling) || opts.ceiling < 1)
86
+ ) {
87
+ throw new Error("PrecisionSearchRequest: ceiling must be an integer >= 1");
88
+ }
89
+
90
+ if (exactName !== undefined && exactName.length > 0) this.exactName = exactName;
91
+ if (query !== undefined && query.length > 0) this.query = new PrecisionSymbolQuery(query);
92
+ if (kind !== undefined && kind.length > 0) this.kind = kind;
93
+ if (filePath !== undefined && filePath.length > 0) this.filePath = filePath;
94
+ if (pathPrefix !== undefined && pathPrefix.length > 0) this.pathPrefix = pathPrefix;
95
+ if (opts.ceiling !== undefined) this.ceiling = opts.ceiling;
96
+ Object.freeze(this);
97
+ }
98
+
99
+ selectLens(): "file" | "exact" | "all" {
100
+ if (this.filePath !== undefined) return "file";
101
+ if (this.exactName !== undefined) return "exact";
102
+ return "all";
103
+ }
104
+
105
+ rank(match: PrecisionSymbolMatch): RankedPrecisionSymbolMatch | null {
106
+ if (this.exactName !== undefined && match.name !== this.exactName) return null;
107
+ if (this.kind !== undefined && match.kind.toLowerCase() !== this.kind) return null;
108
+ if (this.filePath !== undefined && match.path !== this.filePath) return null;
109
+ if (this.pathPrefix !== undefined && !match.path.startsWith(this.pathPrefix)) return null;
110
+ if (this.query === undefined) {
111
+ return new RankedPrecisionSymbolMatch({ match, score: 0 });
112
+ }
113
+ const score = this.query.score(match.name);
114
+ return score === null ? null : new RankedPrecisionSymbolMatch({ match, score });
115
+ }
116
+
117
+ sort(matches: readonly RankedPrecisionSymbolMatch[]): PrecisionSymbolMatch[] {
118
+ return [...matches]
119
+ .sort((a, b) =>
120
+ a.score - b.score ||
121
+ (this.query?.mode === "plain" ? a.match.name.length - b.match.name.length : 0) ||
122
+ a.match.path.localeCompare(b.match.path) ||
123
+ a.match.name.localeCompare(b.match.name)
124
+ )
125
+ .map((entry) => entry.match);
126
+ }
127
+ }