@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
@@ -0,0 +1,437 @@
1
+ import * as path from "node:path";
2
+ import { CanonicalJsonCodec } from "../adapters/canonical-json.js";
3
+ import {
4
+ CAPABILITY_REGISTRY,
5
+ cliCommandKey,
6
+ cliCommandPath,
7
+ type CliCommandName,
8
+ type McpToolName,
9
+ } from "../contracts/capabilities.js";
10
+ import {
11
+ attachCliSchemaMeta,
12
+ cliCommandMcpTool,
13
+ validateCliOutput,
14
+ } from "../contracts/output-schemas.js";
15
+ import { createGraftServer, type McpToolResult } from "../mcp/server.js";
16
+ import { startDaemonServer, type GraftDaemonServer } from "../mcp/daemon-server.js";
17
+ import { startStdioServer } from "../mcp/stdio-server.js";
18
+ import { runIndex } from "./index-cmd.js";
19
+ import { runInit } from "./init.js";
20
+
21
+ const codec = new CanonicalJsonCodec();
22
+
23
+ interface Writer {
24
+ write(chunk: string): unknown;
25
+ }
26
+
27
+ export interface RunCliOptions {
28
+ cwd?: string | undefined;
29
+ args?: readonly string[] | undefined;
30
+ stdout?: Writer | undefined;
31
+ stderr?: Writer | undefined;
32
+ startServer?: ((cwd: string) => Promise<void>) | undefined;
33
+ startDaemon?: ((options: { socketPath?: string | undefined }) => Promise<GraftDaemonServer>) | undefined;
34
+ }
35
+
36
+ interface GlobalCliOptions {
37
+ cwd: string;
38
+ argv: string[];
39
+ }
40
+
41
+ interface ParsedCommand {
42
+ command: CliCommandName;
43
+ json: boolean;
44
+ args: Record<string, unknown>;
45
+ }
46
+
47
+ export function resolveEntrypointArgs(
48
+ args: readonly string[],
49
+ stdinIsTTY: boolean | undefined,
50
+ stdoutIsTTY: boolean | undefined,
51
+ ): string[] {
52
+ if (args.length === 0 && stdinIsTTY !== true && stdoutIsTTY !== true) {
53
+ return ["serve"];
54
+ }
55
+ return [...args];
56
+ }
57
+
58
+ function writeLine(writer: Writer, line = ""): void {
59
+ writer.write(`${line}\n`);
60
+ }
61
+
62
+ function parseToolResult(result: McpToolResult): Record<string, unknown> {
63
+ const payload = result.content.find((item) => item.type === "text");
64
+ if (payload === undefined) {
65
+ throw new Error("Tool result did not contain a text payload");
66
+ }
67
+ const parsed = JSON.parse(payload.text) as unknown;
68
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
69
+ throw new Error("Tool result was not a JSON object");
70
+ }
71
+ return parsed as Record<string, unknown>;
72
+ }
73
+
74
+ function emitPeerCommand(
75
+ command: CliCommandName,
76
+ data: Record<string, unknown>,
77
+ json: boolean,
78
+ writer: Writer,
79
+ ): void {
80
+ const { _schema: _mcpSchema, ...rest } = data;
81
+ const validated = validateCliOutput(command, attachCliSchemaMeta(command, rest));
82
+ if (json) {
83
+ writer.write(`${codec.encode(validated)}\n`);
84
+ return;
85
+ }
86
+ writer.write(`${JSON.stringify(validated, null, 2)}\n`);
87
+ }
88
+
89
+ function consumeFlag(args: string[], flag: string): boolean {
90
+ const index = args.indexOf(flag);
91
+ if (index === -1) return false;
92
+ args.splice(index, 1);
93
+ return true;
94
+ }
95
+
96
+ function consumeOption(args: string[], flag: string): string | undefined {
97
+ const index = args.indexOf(flag);
98
+ if (index === -1) return undefined;
99
+ if (index === args.length - 1) {
100
+ throw new Error(`Missing value for ${flag}`);
101
+ }
102
+ const [value] = args.splice(index, 2).slice(1);
103
+ return value;
104
+ }
105
+
106
+ function consumePositional(args: string[], label: string): string {
107
+ const value = args.shift();
108
+ if (value === undefined) {
109
+ throw new Error(`Missing ${label}`);
110
+ }
111
+ return value;
112
+ }
113
+
114
+ function expectNoArgs(args: string[]): void {
115
+ if (args.length > 0) {
116
+ throw new Error(`Unexpected arguments: ${args.join(" ")}`);
117
+ }
118
+ }
119
+
120
+ function parsePositiveInt(raw: string | undefined, flag: string): number {
121
+ if (raw === undefined) {
122
+ throw new Error(`Missing value for ${flag}`);
123
+ }
124
+ const value = Number.parseInt(raw, 10);
125
+ if (!Number.isInteger(value) || value <= 0) {
126
+ throw new Error(`${flag} must be a positive integer`);
127
+ }
128
+ return value;
129
+ }
130
+
131
+ function parseDaemonCommand(cwd: string, argv: string[]): { socketPath?: string } {
132
+ const socketPath = consumeOption(argv, "--socket");
133
+ expectNoArgs(argv);
134
+ return socketPath !== undefined ? { socketPath: path.resolve(cwd, socketPath) } : {};
135
+ }
136
+
137
+ function parseGlobalOptions(
138
+ cwd: string,
139
+ args: readonly string[],
140
+ ): GlobalCliOptions {
141
+ const rest = [...args];
142
+ const override = consumeOption(rest, "--cwd");
143
+ return {
144
+ cwd: override !== undefined ? path.resolve(cwd, override) : cwd,
145
+ argv: rest,
146
+ };
147
+ }
148
+
149
+ function parseReadCommand(argv: string[]): ParsedCommand {
150
+ const subcommand = consumePositional(argv, "read subcommand");
151
+ const json = consumeFlag(argv, "--json");
152
+
153
+ if (subcommand === "safe") {
154
+ const filePath = consumePositional(argv, "path");
155
+ expectNoArgs(argv);
156
+ return { command: "read_safe", json, args: { path: filePath } };
157
+ }
158
+
159
+ if (subcommand === "outline") {
160
+ const filePath = consumePositional(argv, "path");
161
+ expectNoArgs(argv);
162
+ return { command: "read_outline", json, args: { path: filePath } };
163
+ }
164
+
165
+ if (subcommand === "range") {
166
+ const filePath = consumePositional(argv, "path");
167
+ const start = parsePositiveInt(consumeOption(argv, "--start"), "--start");
168
+ const end = parsePositiveInt(consumeOption(argv, "--end"), "--end");
169
+ expectNoArgs(argv);
170
+ return { command: "read_range", json, args: { path: filePath, start, end } };
171
+ }
172
+
173
+ if (subcommand === "changed") {
174
+ const filePath = consumePositional(argv, "path");
175
+ const consume = consumeFlag(argv, "--consume");
176
+ expectNoArgs(argv);
177
+ return { command: "read_changed", json, args: { path: filePath, consume } };
178
+ }
179
+
180
+ throw new Error(`Unknown read subcommand: ${subcommand}`);
181
+ }
182
+
183
+ function parseStructCommand(argv: string[]): ParsedCommand {
184
+ const subcommand = consumePositional(argv, "struct subcommand");
185
+ const json = consumeFlag(argv, "--json");
186
+
187
+ if (subcommand === "diff") {
188
+ const base = consumeOption(argv, "--base");
189
+ const head = consumeOption(argv, "--head");
190
+ const filePath = consumeOption(argv, "--path");
191
+ expectNoArgs(argv);
192
+ return {
193
+ command: "struct_diff",
194
+ json,
195
+ args: {
196
+ ...(base !== undefined ? { base } : {}),
197
+ ...(head !== undefined ? { head } : {}),
198
+ ...(filePath !== undefined ? { path: filePath } : {}),
199
+ },
200
+ };
201
+ }
202
+
203
+ if (subcommand === "since") {
204
+ const base = consumePositional(argv, "base ref");
205
+ const head = consumeOption(argv, "--head");
206
+ expectNoArgs(argv);
207
+ return {
208
+ command: "struct_since",
209
+ json,
210
+ args: {
211
+ base,
212
+ ...(head !== undefined ? { head } : {}),
213
+ },
214
+ };
215
+ }
216
+
217
+ if (subcommand === "map") {
218
+ const directory = argv[0]?.startsWith("--") === false ? consumePositional(argv, "directory") : undefined;
219
+ expectNoArgs(argv);
220
+ return {
221
+ command: "struct_map",
222
+ json,
223
+ args: directory !== undefined ? { path: directory } : {},
224
+ };
225
+ }
226
+
227
+ throw new Error(`Unknown struct subcommand: ${subcommand}`);
228
+ }
229
+
230
+ function parseSymbolCommand(argv: string[]): ParsedCommand {
231
+ const subcommand = consumePositional(argv, "symbol subcommand");
232
+ const json = consumeFlag(argv, "--json");
233
+
234
+ if (subcommand === "show") {
235
+ const symbol = consumePositional(argv, "symbol");
236
+ const filePath = consumeOption(argv, "--path");
237
+ const ref = consumeOption(argv, "--ref");
238
+ expectNoArgs(argv);
239
+ return {
240
+ command: "symbol_show",
241
+ json,
242
+ args: {
243
+ symbol,
244
+ ...(filePath !== undefined ? { path: filePath } : {}),
245
+ ...(ref !== undefined ? { ref } : {}),
246
+ },
247
+ };
248
+ }
249
+
250
+ if (subcommand === "find") {
251
+ const query = consumePositional(argv, "query");
252
+ const kind = consumeOption(argv, "--kind");
253
+ const filePath = consumeOption(argv, "--path");
254
+ expectNoArgs(argv);
255
+ return {
256
+ command: "symbol_find",
257
+ json,
258
+ args: {
259
+ query,
260
+ ...(kind !== undefined ? { kind } : {}),
261
+ ...(filePath !== undefined ? { path: filePath } : {}),
262
+ },
263
+ };
264
+ }
265
+
266
+ throw new Error(`Unknown symbol subcommand: ${subcommand}`);
267
+ }
268
+
269
+ function parseDiagCommand(argv: string[]): ParsedCommand {
270
+ const subcommand = consumePositional(argv, "diag subcommand");
271
+ const json = consumeFlag(argv, "--json");
272
+
273
+ if (subcommand === "doctor") {
274
+ expectNoArgs(argv);
275
+ return { command: "diag_doctor", json, args: {} };
276
+ }
277
+
278
+ if (subcommand === "activity") {
279
+ const limit = consumeOption(argv, "--limit");
280
+ expectNoArgs(argv);
281
+ return {
282
+ command: "diag_activity",
283
+ json,
284
+ args: {
285
+ ...(limit !== undefined ? { limit: parsePositiveInt(limit, "--limit") } : {}),
286
+ },
287
+ };
288
+ }
289
+
290
+ if (subcommand === "explain") {
291
+ const code = consumePositional(argv, "reason code");
292
+ expectNoArgs(argv);
293
+ return { command: "diag_explain", json, args: { code } };
294
+ }
295
+
296
+ if (subcommand === "stats") {
297
+ expectNoArgs(argv);
298
+ return { command: "diag_stats", json, args: {} };
299
+ }
300
+
301
+ if (subcommand === "capture") {
302
+ const tail = consumeOption(argv, "--tail");
303
+ const separator = argv.indexOf("--");
304
+ const commandTokens = separator === -1 ? [...argv] : argv.slice(separator + 1);
305
+ if (separator !== -1) {
306
+ argv.splice(separator);
307
+ } else {
308
+ argv.length = 0;
309
+ }
310
+ expectNoArgs(argv);
311
+ if (commandTokens.length === 0) {
312
+ throw new Error("Missing shell command");
313
+ }
314
+ return {
315
+ command: "diag_capture",
316
+ json,
317
+ args: {
318
+ command: commandTokens.join(" "),
319
+ ...(tail !== undefined ? { tail: parsePositiveInt(tail, "--tail") } : {}),
320
+ },
321
+ };
322
+ }
323
+
324
+ throw new Error(`Unknown diag subcommand: ${subcommand}`);
325
+ }
326
+
327
+ function parseCommand(argv: string[]): ParsedCommand {
328
+ const group = consumePositional(argv, "command");
329
+
330
+ if (group === "read") return parseReadCommand(argv);
331
+ if (group === "struct") return parseStructCommand(argv);
332
+ if (group === "symbol") return parseSymbolCommand(argv);
333
+ if (group === "diag") return parseDiagCommand(argv);
334
+
335
+ throw new Error(`Unknown command: ${group}`);
336
+ }
337
+
338
+ function renderHelp(writer: Writer): void {
339
+ writeLine(writer, "graft CLI");
340
+ writeLine(writer);
341
+ writeLine(writer, "No args prints help.");
342
+ writeLine(writer);
343
+ writeLine(writer, "Global options:");
344
+ writeLine(writer, " --cwd <path> Run a command against another repo root");
345
+ writeLine(writer, " help Show this help");
346
+ writeLine(writer, " serve Start the MCP stdio server");
347
+ writeLine(writer, " daemon Start the local MCP daemon");
348
+ writeLine(writer);
349
+
350
+ const grouped = new Map<string, { path: readonly string[]; description: string }[]>();
351
+ for (const capability of CAPABILITY_REGISTRY) {
352
+ if (capability.cliPath === undefined) continue;
353
+ const section = capability.cliPath.length > 1 ? capability.cliPath[0] : "project";
354
+ const bucket = grouped.get(section) ?? [];
355
+ bucket.push({ path: capability.cliPath, description: capability.description });
356
+ grouped.set(section, bucket);
357
+ }
358
+
359
+ for (const section of ["project", "read", "struct", "symbol", "diag"]) {
360
+ const entries = grouped.get(section);
361
+ if (entries === undefined) continue;
362
+ writeLine(writer, `${section}:`);
363
+ for (const entry of entries) {
364
+ writeLine(writer, ` ${entry.path.join(" ")} ${entry.description}`);
365
+ }
366
+ writeLine(writer);
367
+ }
368
+ }
369
+
370
+ async function invokePeerCommand(
371
+ cwd: string,
372
+ command: CliCommandName,
373
+ tool: McpToolName,
374
+ args: Record<string, unknown>,
375
+ ): Promise<Record<string, unknown>> {
376
+ const server = createGraftServer({
377
+ projectRoot: cwd,
378
+ graftDir: path.join(cwd, ".graft"),
379
+ });
380
+ return parseToolResult(await server.callTool(tool, args));
381
+ }
382
+
383
+ export async function runCli(options: RunCliOptions = {}): Promise<void> {
384
+ const stdout = options.stdout ?? process.stdout;
385
+ const stderr = options.stderr ?? process.stderr;
386
+ const baseCwd = options.cwd ?? process.cwd();
387
+ const { cwd, argv } = parseGlobalOptions(baseCwd, options.args ?? process.argv.slice(2));
388
+
389
+ if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help") {
390
+ renderHelp(stdout);
391
+ return;
392
+ }
393
+
394
+ if (argv[0] === "init") {
395
+ runInit({ cwd, args: argv.slice(1), stdout, stderr });
396
+ return;
397
+ }
398
+
399
+ if (argv[0] === "index") {
400
+ await runIndex({ cwd, args: argv.slice(1), stdout, stderr });
401
+ return;
402
+ }
403
+
404
+ if (argv[0] === "serve") {
405
+ if (argv.length > 1) {
406
+ process.exitCode = 1;
407
+ writeLine(stderr, `Error: Unexpected arguments: ${argv.slice(1).join(" ")}`);
408
+ return;
409
+ }
410
+ await (options.startServer ?? startStdioServer)(cwd);
411
+ return;
412
+ }
413
+
414
+ if (argv[0] === "daemon") {
415
+ try {
416
+ const daemon = await (options.startDaemon ?? startDaemonServer)(parseDaemonCommand(cwd, argv.slice(1)));
417
+ writeLine(stdout, `Daemon listening on ${daemon.socketPath}`);
418
+ } catch (err: unknown) {
419
+ process.exitCode = 1;
420
+ writeLine(stderr, `Error: ${err instanceof Error ? err.message : String(err)}`);
421
+ }
422
+ return;
423
+ }
424
+
425
+ try {
426
+ const parsed = parseCommand([...argv]);
427
+ const tool = cliCommandMcpTool(parsed.command);
428
+ if (tool === null) {
429
+ throw new Error(`Command ${cliCommandKey(cliCommandPath(parsed.command))} has no MCP peer`);
430
+ }
431
+ const result = await invokePeerCommand(cwd, parsed.command, tool, parsed.args);
432
+ emitPeerCommand(parsed.command, result, parsed.json, stdout);
433
+ } catch (err: unknown) {
434
+ process.exitCode = 1;
435
+ writeLine(stderr, `Error: ${err instanceof Error ? err.message : String(err)}`);
436
+ }
437
+ }