@vibekiln/cutline-mcp-cli 0.6.0 → 0.8.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.
package/Dockerfile CHANGED
@@ -3,7 +3,11 @@ FROM node:20-slim AS base
3
3
  WORKDIR /app
4
4
 
5
5
  # Install the CLI globally from npm (includes bundled servers)
6
- RUN npm install -g @vibekiln/cutline-mcp-cli@latest
6
+ # Override at build time for staging package:
7
+ # --build-arg PACKAGE_NAME=@kylewadegrove/cutline-mcp-cli-staging
8
+ ARG PACKAGE_NAME=@vibekiln/cutline-mcp-cli
9
+ ARG PACKAGE_TAG=latest
10
+ RUN npm install -g "${PACKAGE_NAME}@${PACKAGE_TAG}"
7
11
 
8
12
  # Default to the main constraints server (cutline-server.js)
9
13
  # Override with: docker run ... cutline-mcp serve premortem
@@ -1,11 +1,35 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
4
+ import { createInterface } from 'node:readline';
4
5
  import { resolve, join } from 'node:path';
5
6
  import { getRefreshToken } from '../auth/keychain.js';
6
7
  import { fetchFirebaseApiKey } from '../utils/config.js';
7
8
  import { saveConfig, loadConfig } from '../utils/config-store.js';
8
9
  const CUTLINE_CONFIG = '.cutline/config.json';
10
+ function prompt(question) {
11
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
12
+ return new Promise((resolvePrompt) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close();
15
+ resolvePrompt(answer.trim());
16
+ });
17
+ });
18
+ }
19
+ async function fetchProducts(idToken, baseUrl) {
20
+ try {
21
+ const res = await fetch(`${baseUrl}/mcpListProducts`, {
22
+ headers: { Authorization: `Bearer ${idToken}` },
23
+ });
24
+ if (!res.ok)
25
+ return { products: [], requestOk: false };
26
+ const data = await res.json();
27
+ return { products: data.products ?? [], requestOk: true };
28
+ }
29
+ catch {
30
+ return { products: [], requestOk: false };
31
+ }
32
+ }
9
33
  async function authenticate(options) {
10
34
  const refreshToken = await getRefreshToken();
11
35
  if (!refreshToken)
@@ -53,6 +77,13 @@ function readCutlineConfig(projectRoot) {
53
77
  return null;
54
78
  }
55
79
  }
80
+ function writeCutlineConfig(projectRoot, config) {
81
+ const cutlineDir = join(projectRoot, '.cutline');
82
+ const configPath = join(cutlineDir, 'config.json');
83
+ mkdirSync(cutlineDir, { recursive: true });
84
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
85
+ return configPath;
86
+ }
56
87
  function cursorRgrRule(config, tier) {
57
88
  const productId = config?.product_id ?? '<from .cutline/config.json>';
58
89
  const productName = config?.product_name ?? 'your product';
@@ -203,7 +234,8 @@ function ensureGitignore(projectRoot, patterns) {
203
234
  }
204
235
  export async function initCommand(options) {
205
236
  const projectRoot = resolve(options.projectRoot ?? process.cwd());
206
- const config = readCutlineConfig(projectRoot);
237
+ let config = readCutlineConfig(projectRoot);
238
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
207
239
  const scanUrl = options.staging
208
240
  ? 'https://cutline-staging.web.app/scan'
209
241
  : 'https://thecutline.ai/scan';
@@ -222,6 +254,79 @@ export async function initCommand(options) {
222
254
  tier = auth.tier;
223
255
  spinner.succeed(chalk.green(`Authenticated as ${auth.email} (${tier})`));
224
256
  }
257
+ if (tier === 'premium' && auth?.idToken && auth.baseUrl) {
258
+ const productsSpinner = ora('Fetching product graphs for normalization...').start();
259
+ const { products, requestOk } = await fetchProducts(auth.idToken, auth.baseUrl);
260
+ productsSpinner.stop();
261
+ if (requestOk && products.length > 0) {
262
+ const currentIndex = config?.product_id
263
+ ? products.findIndex((p) => p.id === config?.product_id)
264
+ : -1;
265
+ let selected;
266
+ if (isInteractive) {
267
+ console.log(chalk.bold('\n Select canonical product for rule generation\n'));
268
+ products.forEach((p, i) => {
269
+ const date = p.createdAt
270
+ ? chalk.dim(` (${new Date(p.createdAt).toLocaleDateString()})`)
271
+ : '';
272
+ const currentTag = currentIndex === i ? chalk.green(' [current]') : '';
273
+ console.log(` ${chalk.cyan(`${i + 1}.`)} ${chalk.white(p.name)}${date}${currentTag}`);
274
+ if (p.brief)
275
+ console.log(` ${chalk.dim(p.brief)}`);
276
+ });
277
+ console.log();
278
+ const defaultHint = currentIndex >= 0
279
+ ? ` (Enter keeps ${products[currentIndex].name})`
280
+ : '';
281
+ const answer = await prompt(chalk.cyan(` Select a product number${defaultHint}: `));
282
+ const choice = parseInt(answer, 10);
283
+ if (!answer && currentIndex >= 0) {
284
+ selected = products[currentIndex];
285
+ }
286
+ else if (Number.isFinite(choice) && choice >= 1 && choice <= products.length) {
287
+ selected = products[choice - 1];
288
+ }
289
+ else if (currentIndex >= 0) {
290
+ selected = products[currentIndex];
291
+ console.log(chalk.yellow(` Invalid selection. Keeping "${selected.name}".`));
292
+ }
293
+ else {
294
+ selected = products[0];
295
+ console.log(chalk.yellow(` Invalid selection. Defaulting to "${selected.name}".`));
296
+ }
297
+ }
298
+ else {
299
+ selected = currentIndex >= 0 ? products[currentIndex] : products[0];
300
+ if (currentIndex < 0) {
301
+ console.log(chalk.dim(` Auto-selected product: ${selected.name}`));
302
+ }
303
+ }
304
+ const changed = selected.id !== config?.product_id || selected.name !== config?.product_name;
305
+ if (changed) {
306
+ const configPath = writeCutlineConfig(projectRoot, {
307
+ product_id: selected.id,
308
+ product_name: selected.name,
309
+ linked_email: auth.email ?? null,
310
+ });
311
+ console.log(chalk.green(` ✓ Normalized product to "${selected.name}"`));
312
+ console.log(chalk.dim(` ${configPath}`));
313
+ }
314
+ config = {
315
+ product_id: selected.id,
316
+ product_name: selected.name,
317
+ linked_email: auth.email ?? config?.linked_email ?? null,
318
+ };
319
+ console.log();
320
+ }
321
+ else if (requestOk) {
322
+ console.log(chalk.yellow(' No completed product graphs found for this premium account.'));
323
+ console.log(chalk.dim(' Generate a deep dive first, then re-run cutline-mcp init for normalization.\n'));
324
+ }
325
+ else {
326
+ console.log(chalk.yellow(' Could not fetch products for normalization (network/auth issue).'));
327
+ console.log(chalk.dim(' Continuing with existing .cutline/config.json values.\n'));
328
+ }
329
+ }
225
330
  if (config) {
226
331
  console.log(chalk.dim(` Product: ${config.product_name ?? config.product_id}`));
227
332
  }
@@ -12,6 +12,18 @@ const SERVER_MAP = {
12
12
  output: 'output-server.js',
13
13
  integrations: 'integrations-server.js',
14
14
  };
15
+ function handleChildMessage(message, pending) {
16
+ const parsed = message;
17
+ const id = normalizeId(parsed?.id);
18
+ if (!id)
19
+ return;
20
+ const entry = pending.get(id);
21
+ if (!entry)
22
+ return;
23
+ clearTimeout(entry.timer);
24
+ pending.delete(id);
25
+ entry.resolve(message);
26
+ }
15
27
  function readJsonBody(req) {
16
28
  return new Promise((resolveBody, rejectBody) => {
17
29
  const chunks = [];
@@ -77,44 +89,68 @@ function serveHttpBridge(serverName, serverPath, opts) {
77
89
  if (!child.stdin || child.killed) {
78
90
  throw new Error('MCP stdio server is not available');
79
91
  }
80
- const body = JSON.stringify(message);
81
- const packet = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`;
82
- child.stdin.write(packet, 'utf8');
92
+ // Our bundled servers (MCP SDK 1.x) use line-delimited JSON over stdio.
93
+ // Write one JSON-RPC envelope per line.
94
+ const line = `${JSON.stringify(message)}\n`;
95
+ child.stdin.write(line, 'utf8');
83
96
  };
84
97
  child.stdout?.on('data', (chunk) => {
85
98
  stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
99
+ // 1) Support MCP framed responses (Content-Length) for compatibility.
86
100
  while (true) {
87
- const separatorIndex = stdoutBuffer.indexOf('\r\n\r\n');
101
+ const crlfSeparator = stdoutBuffer.indexOf('\r\n\r\n');
102
+ const lfSeparator = stdoutBuffer.indexOf('\n\n');
103
+ let separatorIndex = -1;
104
+ let separatorLength = 0;
105
+ if (crlfSeparator >= 0 && (lfSeparator < 0 || crlfSeparator < lfSeparator)) {
106
+ separatorIndex = crlfSeparator;
107
+ separatorLength = 4;
108
+ }
109
+ else if (lfSeparator >= 0) {
110
+ separatorIndex = lfSeparator;
111
+ separatorLength = 2;
112
+ }
88
113
  if (separatorIndex < 0)
89
114
  break;
90
115
  const headers = stdoutBuffer.slice(0, separatorIndex).toString('utf8');
91
116
  const lengthMatch = headers.match(/content-length:\s*(\d+)/i);
92
- if (!lengthMatch) {
93
- stdoutBuffer = stdoutBuffer.slice(separatorIndex + 4);
94
- continue;
95
- }
117
+ if (!lengthMatch)
118
+ break;
96
119
  const contentLength = Number(lengthMatch[1]);
97
- const packetLength = separatorIndex + 4 + contentLength;
120
+ const packetLength = separatorIndex + separatorLength + contentLength;
98
121
  if (stdoutBuffer.length < packetLength)
99
122
  break;
100
- const jsonBytes = stdoutBuffer.slice(separatorIndex + 4, packetLength);
123
+ const jsonBytes = stdoutBuffer.slice(separatorIndex + separatorLength, packetLength);
101
124
  stdoutBuffer = stdoutBuffer.slice(packetLength);
102
125
  try {
103
126
  const message = JSON.parse(jsonBytes.toString('utf8'));
104
- const id = normalizeId(message?.id);
105
- if (!id)
106
- continue;
107
- const entry = pending.get(id);
108
- if (!entry)
109
- continue;
110
- clearTimeout(entry.timer);
111
- pending.delete(id);
112
- entry.resolve(message);
127
+ handleChildMessage(message, pending);
113
128
  }
114
129
  catch {
115
130
  // Ignore malformed child output and keep processing subsequent frames.
116
131
  }
117
132
  }
133
+ // 2) Support line-delimited JSON responses used by our bundled servers.
134
+ const head = stdoutBuffer.slice(0, Math.min(stdoutBuffer.length, 32)).toString('utf8');
135
+ const looksLikeFramedPrefix = /^\s*content-length\s*:/i.test(head);
136
+ if (!looksLikeFramedPrefix) {
137
+ const text = stdoutBuffer.toString('utf8');
138
+ const lines = text.split(/\r?\n/);
139
+ const remainder = lines.pop() ?? '';
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ if (!trimmed)
143
+ continue;
144
+ try {
145
+ const message = JSON.parse(trimmed);
146
+ handleChildMessage(message, pending);
147
+ }
148
+ catch {
149
+ // Ignore non-JSON stdout lines (if any).
150
+ }
151
+ }
152
+ stdoutBuffer = Buffer.from(remainder, 'utf8');
153
+ }
118
154
  });
119
155
  child.on('exit', (code, signal) => {
120
156
  failAllPending(new Error(`MCP stdio server exited unexpectedly (${signal ? `signal ${signal}` : `code ${code ?? 'unknown'}`})`));
@@ -7395,6 +7395,18 @@ function mcpAudit(entry) {
7395
7395
  }));
7396
7396
  }
7397
7397
  var DEFAULT_MODEL = process.env.MODEL_ID || "gemini-2.5-pro";
7398
+ var GOVERNANCE_ENFORCEMENT = (process.env.CUTLINE_GOVERNANCE_ENFORCEMENT || "advisory").toLowerCase() === "enforced";
7399
+ function buildGovernanceEnvelope(input) {
7400
+ return {
7401
+ decision: input.decision,
7402
+ decisionReasons: input.reasons ?? [],
7403
+ requiredActions: input.requiredActions ?? [],
7404
+ executionPermission: input.executionPermission ?? "conditional",
7405
+ ...input.closePermission ? { closePermission: input.closePermission } : {},
7406
+ ...input.requiredBranches?.length ? { requiredBranches: input.requiredBranches } : {},
7407
+ ...input.mode ? { mode: input.mode } : {}
7408
+ };
7409
+ }
7398
7410
  var generateStructuredContent = (options) => withLlmMonitor(options.modelId || DEFAULT_MODEL, () => cfGenerateStructuredContent(options));
7399
7411
  var explorationSessions = /* @__PURE__ */ new Map();
7400
7412
  function generateId(prefix = "id") {
@@ -8003,6 +8015,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
8003
8015
  max_constraints: { type: "number", description: "Max constraints to return (default: 5)" },
8004
8016
  use_semantic: { type: "boolean", description: "Use semantic search if embeddings available (default: true)" },
8005
8017
  phase: { type: "string", enum: ["test_spec", "functional", "security", "performance", "economics", "full", "auto"], description: "RGR phase filter. 'auto' uses complexity heuristic. Default: 'full'" },
8018
+ requestedOutcome: {
8019
+ type: "string",
8020
+ enum: ["approve", "revise", "execute", "close"],
8021
+ description: "Governance intent for this transition (default: approve)"
8022
+ },
8006
8023
  auto_scope_expand: { type: "boolean", description: "If true, auto-seed a new graph entity when scope expansion intent is confidently detected and entity fields are provided." },
8007
8024
  scope_entity_name: { type: "string", description: "Optional explicit entity name to seed during scope expansion (for example 'Vibe Check Extension')." },
8008
8025
  scope_entity_type: { type: "string", enum: ["feature", "component", "data_type"], description: "Entity type for auto scope expansion." },
@@ -8223,7 +8240,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
8223
8240
  properties: {
8224
8241
  product_id: { type: "string", description: "Product ID" },
8225
8242
  file_path: { type: "string", description: "File path to assess (e.g., 'src/api/upload.ts')" },
8226
- depth: { type: "number", description: "BFS traversal depth for subgraph extraction (default: 2)" }
8243
+ depth: { type: "number", description: "BFS traversal depth for subgraph extraction (default: 2)" },
8244
+ requestedOutcome: {
8245
+ type: "string",
8246
+ enum: ["approve", "revise", "execute", "close"],
8247
+ description: "Governance intent for this transition (default: approve)"
8248
+ }
8227
8249
  },
8228
8250
  required: ["product_id", "file_path"]
8229
8251
  }
@@ -8240,6 +8262,39 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
8240
8262
  required: ["product_id", "phase"]
8241
8263
  }
8242
8264
  },
8265
+ {
8266
+ name: "request_execution_gate",
8267
+ description: "\u{1F512} PREMIUM - Governance checkpoint before downstream actions (code generation, file edits, migrations, deploy). Uses RGR completion, audit signals, and constraint acknowledgments to return approved/conditional/blocked.",
8268
+ inputSchema: {
8269
+ type: "object",
8270
+ properties: {
8271
+ product_id: { type: "string", description: "Product ID" },
8272
+ target_action: { type: "string", enum: ["generate_code", "modify_files", "run_migration", "deploy"], description: "Planned downstream action" },
8273
+ target_artifacts: { type: "array", items: { type: "string" }, description: "Files/modules the action will touch" },
8274
+ based_on_step_ids: { type: "array", items: { type: "string" }, description: "Optional planning step IDs this execution is based on" },
8275
+ open_critical_findings: { type: "number", description: "Count of unresolved critical/high findings from latest audit context" },
8276
+ touched_sensitive_paths: { type: "array", items: { type: "string" }, description: "Sensitive files touched in this action (auth, billing, security, AI, integrations, user flows)" },
8277
+ constraints_acknowledged: { type: "boolean", description: "Whether relevant constraints have been reviewed and acknowledged for touched sensitive paths" }
8278
+ },
8279
+ required: ["product_id", "target_action", "target_artifacts"]
8280
+ }
8281
+ },
8282
+ {
8283
+ name: "submit_done_claim",
8284
+ description: "\u{1F512} PREMIUM - Governance close gate. Evaluates completion claims against RGR state, unresolved critical findings, and provided evidence references.",
8285
+ inputSchema: {
8286
+ type: "object",
8287
+ properties: {
8288
+ product_id: { type: "string", description: "Product ID" },
8289
+ completion_summary: { type: "string", description: "What was completed" },
8290
+ claimed_criteria_satisfied: { type: "array", items: { type: "string" }, description: "Success criteria the agent claims are satisfied" },
8291
+ known_limitations: { type: "array", items: { type: "string" }, description: "Known limitations or intentional exclusions" },
8292
+ evidence_refs: { type: "array", items: { type: "string" }, description: "Evidence links/refs (tests, reports, docs, PRs)" },
8293
+ open_critical_findings: { type: "number", description: "Count of unresolved critical/high findings from latest audit context" }
8294
+ },
8295
+ required: ["product_id", "completion_summary", "claimed_criteria_satisfied"]
8296
+ }
8297
+ },
8243
8298
  {
8244
8299
  name: "export_readiness_badge",
8245
8300
  description: "\u{1F512} PREMIUM - Export a public readiness badge and verification page for this product. Computes an architecture readiness grade from the constraint graph, stores a public snapshot, and returns embeddable badge snippets (Markdown for GitHub README, HTML for website footer) plus a full CUTLINE-REPORT.md. The verification page at thecutline.ai/verify/{id} provides SEO backlinks.",
@@ -9182,7 +9237,7 @@ Meta: ${JSON.stringify(output.meta)}` }
9182
9237
  return void 0;
9183
9238
  };
9184
9239
  var detectFramework = detectFramework2;
9185
- const { product_id, file_paths, code_snippet, task_description, mode = "auto", max_constraints = 5, use_semantic = true, phase: autoPhase = "full", auto_scope_expand = false, scope_entity_name, scope_entity_type, scope_entity_description, scope_entity_tags, scope_parent_id, scope_similarity_threshold } = args;
9240
+ const { product_id, file_paths, code_snippet, task_description, mode = "auto", max_constraints = 5, use_semantic = true, phase: autoPhase = "full", requestedOutcome = "approve", auto_scope_expand = false, scope_entity_name, scope_entity_type, scope_entity_description, scope_entity_tags, scope_parent_id, scope_similarity_threshold } = args;
9186
9241
  if (!product_id) {
9187
9242
  throw new McpError(ErrorCode.InvalidParams, "product_id is required");
9188
9243
  }
@@ -9192,6 +9247,19 @@ Meta: ${JSON.stringify(output.meta)}` }
9192
9247
  task_description
9193
9248
  };
9194
9249
  if (!hasEnoughSignal(fileContext)) {
9250
+ const governance2 = buildGovernanceEnvelope({
9251
+ decision: "revise",
9252
+ reasons: [{
9253
+ constraintId: "context.signal.missing",
9254
+ severity: "warning",
9255
+ message: "Insufficient context to match constraints deterministically."
9256
+ }],
9257
+ requiredActions: [
9258
+ "Provide file_paths, code_snippet, or task_description.",
9259
+ "Re-run constraints_auto with richer context."
9260
+ ],
9261
+ executionPermission: "denied"
9262
+ });
9195
9263
  return {
9196
9264
  content: [{
9197
9265
  type: "text",
@@ -9202,7 +9270,8 @@ Meta: ${JSON.stringify(output.meta)}` }
9202
9270
  meta: {
9203
9271
  mode: "silent",
9204
9272
  signal_strength: 0
9205
- }
9273
+ },
9274
+ governance: governance2
9206
9275
  })
9207
9276
  }]
9208
9277
  };
@@ -9238,6 +9307,16 @@ Meta: ${JSON.stringify(output.meta)}` }
9238
9307
  }
9239
9308
  const actualMode = mode === "auto" ? analysis.suggested_mode : mode;
9240
9309
  if (actualMode === "silent") {
9310
+ const governance2 = buildGovernanceEnvelope({
9311
+ decision: "approved",
9312
+ reasons: [{
9313
+ constraintId: "constraints.mode.silent",
9314
+ severity: "info",
9315
+ message: "Silent mode selected; constraints were analyzed but not injected into output."
9316
+ }],
9317
+ requiredActions: requestedOutcome === "execute" ? ["If execution is next, run request_execution_gate before downstream write/deploy actions."] : [],
9318
+ executionPermission: requestedOutcome === "execute" ? "conditional" : "denied"
9319
+ });
9241
9320
  return {
9242
9321
  content: [{
9243
9322
  type: "text",
@@ -9252,7 +9331,8 @@ Meta: ${JSON.stringify(output.meta)}` }
9252
9331
  keywords_detected: analysis.signal.keywords?.slice(0, 5),
9253
9332
  scope_expansion: scopeExpansion,
9254
9333
  scope_seeded: scopeSeedResult || void 0
9255
- }
9334
+ },
9335
+ governance: governance2
9256
9336
  })
9257
9337
  }]
9258
9338
  };
@@ -9264,6 +9344,19 @@ Meta: ${JSON.stringify(output.meta)}` }
9264
9344
  lightNodes = await getAllNodesLight(product_id);
9265
9345
  }
9266
9346
  if (lightNodes.length === 0) {
9347
+ const governance2 = buildGovernanceEnvelope({
9348
+ decision: "blocked",
9349
+ reasons: [{
9350
+ constraintId: "constraints.graph.empty",
9351
+ severity: "high",
9352
+ message: "No ingested constraints are available for this product."
9353
+ }],
9354
+ requiredActions: [
9355
+ "Run constraints_ingest_* or graph_ingest_requirements first.",
9356
+ "Re-run constraints_auto after ingestion."
9357
+ ],
9358
+ executionPermission: "denied"
9359
+ });
9267
9360
  return {
9268
9361
  content: [{
9269
9362
  type: "text",
@@ -9275,7 +9368,8 @@ Meta: ${JSON.stringify(output.meta)}` }
9275
9368
  mode: actualMode,
9276
9369
  detected_domains: analysis.detected_domains,
9277
9370
  signal_strength: Math.round(analysis.confidence * 100) / 100
9278
- }
9371
+ },
9372
+ governance: governance2
9279
9373
  })
9280
9374
  }]
9281
9375
  };
@@ -9413,6 +9507,36 @@ Meta: ${JSON.stringify(output.meta)}` }
9413
9507
  ${recommendation}`;
9414
9508
  }
9415
9509
  }
9510
+ const criticalCount = topConstraints.filter((c) => c.severity === "critical").length;
9511
+ const warningCount = topConstraints.filter((c) => c.severity === "warning").length;
9512
+ const branchRequired = detectedConflicts.length > 0;
9513
+ const shouldBlock = actualMode === "blocking" && criticalCount > 0;
9514
+ const governanceDecision = shouldBlock ? "blocked" : branchRequired ? "branch" : criticalCount > 0 || warningCount > 0 ? "revise" : "approved";
9515
+ const governance = buildGovernanceEnvelope({
9516
+ decision: governanceDecision,
9517
+ reasons: [
9518
+ ...criticalCount > 0 ? [{
9519
+ constraintId: "constraints.severity.critical",
9520
+ severity: "critical",
9521
+ message: `${criticalCount} critical constraint(s) detected for the proposed transition.`
9522
+ }] : [],
9523
+ ...branchRequired ? [{
9524
+ constraintId: "constraints.conflict.detected",
9525
+ severity: "high",
9526
+ message: `${detectedConflicts.length} constraint conflict(s) detected; compare alternatives before execution.`
9527
+ }] : []
9528
+ ],
9529
+ requiredActions: [
9530
+ ...criticalCount > 0 ? ["Address critical constraints before implementation handoff."] : [],
9531
+ ...branchRequired ? ["Compare at least two implementation alternatives and resolve conflicts explicitly."] : [],
9532
+ ...requestedOutcome === "execute" ? ["Run request_execution_gate before invoking code-write or deploy actions."] : []
9533
+ ],
9534
+ executionPermission: shouldBlock ? "denied" : requestedOutcome === "execute" ? "conditional" : "denied",
9535
+ requiredBranches: branchRequired ? [
9536
+ { branchId: "A", instruction: "Propose the lowest-risk compliant implementation path." },
9537
+ { branchId: "B", instruction: "Propose an alternative path and document trade-offs." }
9538
+ ] : void 0
9539
+ });
9416
9540
  return {
9417
9541
  content: [{
9418
9542
  type: "text",
@@ -9433,9 +9557,12 @@ ${recommendation}`;
9433
9557
  used_category_prefilter: usePreFilter,
9434
9558
  phase: autoPhase,
9435
9559
  rgr_plan: autoRgrPlan || void 0,
9560
+ requested_outcome: requestedOutcome,
9436
9561
  scope_expansion: scopeExpansion,
9437
- scope_seeded: scopeSeedResult || void 0
9438
- }
9562
+ scope_seeded: scopeSeedResult || void 0,
9563
+ requested_outcome: requestedOutcome
9564
+ },
9565
+ governance
9439
9566
  })
9440
9567
  }]
9441
9568
  };
@@ -10253,7 +10380,7 @@ ${JSON.stringify(metrics, null, 2)}` }
10253
10380
  // RGR_PLAN
10254
10381
  // ─────────────────────────────────────────────────────────────────
10255
10382
  case "rgr_plan": {
10256
- const { product_id, file_path, depth: rgrDepth = 2 } = args;
10383
+ const { product_id, file_path, depth: rgrDepth = 2, requestedOutcome = "approve" } = args;
10257
10384
  if (!product_id || !file_path) {
10258
10385
  throw new McpError(ErrorCode.InvalidParams, "product_id and file_path are required");
10259
10386
  }
@@ -10264,10 +10391,26 @@ ${JSON.stringify(metrics, null, 2)}` }
10264
10391
  getAllBindings(product_id)
10265
10392
  ]);
10266
10393
  if (rgrEntities.length === 0) {
10394
+ const governance2 = buildGovernanceEnvelope({
10395
+ decision: "blocked",
10396
+ reasons: [{
10397
+ constraintId: "graph.entities.missing",
10398
+ severity: "high",
10399
+ message: `No graph entities found for product "${product_id}".`
10400
+ }],
10401
+ requiredActions: [
10402
+ "Run graph_ingest_requirements or constraints_ingest_* to seed the graph.",
10403
+ "Re-run rgr_plan after ingestion."
10404
+ ],
10405
+ executionPermission: "denied"
10406
+ });
10267
10407
  return {
10268
10408
  content: [{
10269
10409
  type: "text",
10270
- text: `No graph entities found for product "${product_id}". Run graph_ingest_requirements first.`
10410
+ text: JSON.stringify({
10411
+ message: `No graph entities found for product "${product_id}". Run graph_ingest_requirements first.`,
10412
+ governance: governance2
10413
+ }, null, 2)
10271
10414
  }]
10272
10415
  };
10273
10416
  }
@@ -10286,6 +10429,19 @@ ${JSON.stringify(metrics, null, 2)}` }
10286
10429
  }
10287
10430
  }
10288
10431
  if (rgrMatched.length === 0) {
10432
+ const governance2 = buildGovernanceEnvelope({
10433
+ decision: "revise",
10434
+ reasons: [{
10435
+ constraintId: "graph.binding.missing",
10436
+ severity: "warning",
10437
+ message: `No graph entity is bound to "${file_path}".`
10438
+ }],
10439
+ requiredActions: [
10440
+ "Bind codebase entities using graph_bind_codebase/graph_bind_confirm.",
10441
+ "Proceed with full delivery only if no bound entity is expected."
10442
+ ],
10443
+ executionPermission: requestedOutcome === "execute" ? "conditional" : "denied"
10444
+ });
10289
10445
  return {
10290
10446
  content: [{
10291
10447
  type: "text",
@@ -10293,7 +10449,8 @@ ${JSON.stringify(metrics, null, 2)}` }
10293
10449
  strategy: "full",
10294
10450
  phases: [{ name: "full", label: "Full Delivery", description: "No entity bound to this file. Deliver all constraints.", categories: [], constraint_count: 0 }],
10295
10451
  total_constraints: 0,
10296
- rationale: `No entity bound to "${file_path}". Cannot assess constraint complexity.`
10452
+ rationale: `No entity bound to "${file_path}". Cannot assess constraint complexity.`,
10453
+ governance: governance2
10297
10454
  }, null, 2)
10298
10455
  }]
10299
10456
  };
@@ -10302,13 +10459,36 @@ ${JSON.stringify(metrics, null, 2)}` }
10302
10459
  const fileConstraints = rgrSubgraph?.constraints ?? [];
10303
10460
  const plan = planRgrPhases(fileConstraints);
10304
10461
  const complexity = assessComplexity(fileConstraints);
10462
+ const branchNeeded = plan.strategy === "phased" && (complexity.critical_count > 0 || requestedOutcome === "execute");
10463
+ const governance = buildGovernanceEnvelope({
10464
+ decision: branchNeeded ? "branch" : "approved",
10465
+ reasons: branchNeeded ? [{
10466
+ constraintId: "rgr.phased.required",
10467
+ severity: "high",
10468
+ message: `${complexity.critical_count} critical constraint(s) across ${complexity.category_count} categories require phased comparison.`
10469
+ }] : [{
10470
+ constraintId: "rgr.plan.ready",
10471
+ severity: "info",
10472
+ message: "Plan assessed and ready for implementation sequencing."
10473
+ }],
10474
+ requiredActions: [
10475
+ ...plan.strategy === "phased" ? ["Follow phased RGR execution order from this plan."] : ["Deliver full constraint set in a single pass."],
10476
+ ...requestedOutcome === "execute" ? ["Request execution permission via request_execution_gate before downstream write/deploy actions."] : []
10477
+ ],
10478
+ executionPermission: requestedOutcome === "execute" ? "conditional" : "denied",
10479
+ requiredBranches: branchNeeded ? [
10480
+ { branchId: "A", instruction: "Prioritize functional progress (test_spec/functional) then apply NFR phases." },
10481
+ { branchId: "B", instruction: "Prioritize security/performance constraints before broad implementation." }
10482
+ ] : void 0
10483
+ });
10305
10484
  return {
10306
10485
  content: [{
10307
10486
  type: "text",
10308
10487
  text: JSON.stringify({
10309
10488
  ...plan,
10310
10489
  entity: rgrMatched[0].name,
10311
- complexity
10490
+ complexity,
10491
+ governance
10312
10492
  }, null, 2)
10313
10493
  }]
10314
10494
  };
@@ -10379,6 +10559,157 @@ ${JSON.stringify(metrics, null, 2)}` }
10379
10559
  };
10380
10560
  }
10381
10561
  // ─────────────────────────────────────────────────────────────────
10562
+ // REQUEST_EXECUTION_GATE
10563
+ // ─────────────────────────────────────────────────────────────────
10564
+ case "request_execution_gate": {
10565
+ const { product_id, target_action, target_artifacts, based_on_step_ids = [], open_critical_findings = 0, touched_sensitive_paths = [], constraints_acknowledged = false } = args;
10566
+ if (!product_id || !target_action || !Array.isArray(target_artifacts) || target_artifacts.length === 0) {
10567
+ throw new McpError(ErrorCode.InvalidParams, "product_id, target_action, and non-empty target_artifacts are required");
10568
+ }
10569
+ const meta = await getGraphMetadata(product_id);
10570
+ const completedPhases = meta?.rgr_completed_phases ?? [];
10571
+ const metrics = meta?.metrics;
10572
+ const unmetCheckpoints = [];
10573
+ const blockingFindings = [];
10574
+ const requiredNextSteps = [];
10575
+ if (!completedPhases.includes("security")) {
10576
+ unmetCheckpoints.push("security_commitment");
10577
+ blockingFindings.push({
10578
+ constraintId: "rgr.phase.security",
10579
+ severity: "high",
10580
+ message: "Security phase is not marked complete."
10581
+ });
10582
+ requiredNextSteps.push('Complete security remediation and call rgr_complete_phase(product_id, phase: "security").');
10583
+ }
10584
+ if (target_action === "deploy" && !completedPhases.includes("performance")) {
10585
+ unmetCheckpoints.push("implementation_readiness");
10586
+ blockingFindings.push({
10587
+ constraintId: "rgr.phase.performance",
10588
+ severity: "warning",
10589
+ message: "Deploy requested before performance phase completion."
10590
+ });
10591
+ requiredNextSteps.push('Complete performance checks and call rgr_complete_phase(product_id, phase: "performance").');
10592
+ }
10593
+ if (open_critical_findings > 0) {
10594
+ unmetCheckpoints.push("verification");
10595
+ blockingFindings.push({
10596
+ constraintId: "audit.findings.open",
10597
+ severity: "critical",
10598
+ message: `${open_critical_findings} unresolved critical/high findings are still open.`
10599
+ });
10600
+ requiredNextSteps.push("Resolve or explicitly accept outstanding critical/high findings before execution.");
10601
+ }
10602
+ if (touched_sensitive_paths.length > 0 && !constraints_acknowledged) {
10603
+ unmetCheckpoints.push("implementation_readiness");
10604
+ blockingFindings.push({
10605
+ constraintId: "constraints.ack.required",
10606
+ severity: "high",
10607
+ message: "Sensitive paths were provided without explicit constraint acknowledgment."
10608
+ });
10609
+ requiredNextSteps.push("Run constraints_auto for touched sensitive paths and acknowledge applicable constraints.");
10610
+ }
10611
+ const hasBlocks = blockingFindings.some((f) => f.severity === "critical" || f.severity === "high");
10612
+ const denied = hasBlocks && GOVERNANCE_ENFORCEMENT;
10613
+ const decision = denied ? "blocked" : blockingFindings.length > 0 ? "conditional" : "approved";
10614
+ const governance = buildGovernanceEnvelope({
10615
+ decision: denied ? "blocked" : blockingFindings.length > 0 ? "revise" : "approved",
10616
+ reasons: blockingFindings,
10617
+ requiredActions: requiredNextSteps,
10618
+ executionPermission: denied ? "denied" : blockingFindings.length > 0 ? "conditional" : "granted",
10619
+ mode: GOVERNANCE_ENFORCEMENT ? "enforced" : "advisory"
10620
+ });
10621
+ return {
10622
+ content: [{
10623
+ type: "text",
10624
+ text: JSON.stringify({
10625
+ decision,
10626
+ unmetCheckpoints: [...new Set(unmetCheckpoints)],
10627
+ blockingFindings,
10628
+ requiredNextSteps,
10629
+ based_on_step_ids,
10630
+ product_id,
10631
+ target_action,
10632
+ target_artifacts,
10633
+ governanceMode: GOVERNANCE_ENFORCEMENT ? "enforced" : "advisory",
10634
+ readiness: {
10635
+ rgr_completed_phases: completedPhases,
10636
+ security_readiness_pct: metrics?.security_readiness_pct ?? null,
10637
+ engineering_readiness_pct: metrics?.engineering_readiness_pct ?? null
10638
+ },
10639
+ governance
10640
+ }, null, 2)
10641
+ }]
10642
+ };
10643
+ }
10644
+ // ─────────────────────────────────────────────────────────────────
10645
+ // SUBMIT_DONE_CLAIM
10646
+ // ─────────────────────────────────────────────────────────────────
10647
+ case "submit_done_claim": {
10648
+ const { product_id, completion_summary, claimed_criteria_satisfied, known_limitations = [], evidence_refs = [], open_critical_findings = 0 } = args;
10649
+ if (!product_id || !completion_summary || !Array.isArray(claimed_criteria_satisfied)) {
10650
+ throw new McpError(ErrorCode.InvalidParams, "product_id, completion_summary, and claimed_criteria_satisfied are required");
10651
+ }
10652
+ const meta = await getGraphMetadata(product_id);
10653
+ const completedPhases = meta?.rgr_completed_phases ?? [];
10654
+ const unsatisfiedCriteria = [];
10655
+ const blockingFindings = [];
10656
+ const requiredNextSteps = [];
10657
+ if (!completedPhases.includes("security")) {
10658
+ unsatisfiedCriteria.push("Security phase completion not recorded.");
10659
+ blockingFindings.push({
10660
+ constraintId: "done.rgr.security_missing",
10661
+ severity: "high",
10662
+ message: "Cannot close work item before security phase completion."
10663
+ });
10664
+ requiredNextSteps.push('Complete and record security phase via rgr_complete_phase(product_id, phase: "security").');
10665
+ }
10666
+ if (evidence_refs.length === 0) {
10667
+ unsatisfiedCriteria.push("No evidence references provided for completion claim.");
10668
+ blockingFindings.push({
10669
+ constraintId: "done.evidence.required",
10670
+ severity: "warning",
10671
+ message: "Done claims require evidence refs (tests, reports, or docs)."
10672
+ });
10673
+ requiredNextSteps.push("Provide evidence_refs for tests, audit reruns, or implementation verification.");
10674
+ }
10675
+ if (open_critical_findings > 0) {
10676
+ unsatisfiedCriteria.push(`${open_critical_findings} unresolved critical/high findings remain.`);
10677
+ blockingFindings.push({
10678
+ constraintId: "done.critical_findings.open",
10679
+ severity: "critical",
10680
+ message: "Outstanding critical/high findings prevent closure."
10681
+ });
10682
+ requiredNextSteps.push("Resolve or explicitly disposition outstanding critical/high findings before closure.");
10683
+ }
10684
+ const hasHardBlock = blockingFindings.some((f) => f.severity === "critical" || f.severity === "high");
10685
+ const decision = hasHardBlock ? "blocked" : unsatisfiedCriteria.length > 0 ? "incomplete" : "complete";
10686
+ const governance = buildGovernanceEnvelope({
10687
+ decision: hasHardBlock ? "blocked" : unsatisfiedCriteria.length > 0 ? "incomplete" : "approved",
10688
+ reasons: blockingFindings,
10689
+ requiredActions: requiredNextSteps,
10690
+ executionPermission: "denied",
10691
+ closePermission: decision === "complete" ? "granted" : "denied",
10692
+ mode: GOVERNANCE_ENFORCEMENT ? "enforced" : "advisory"
10693
+ });
10694
+ return {
10695
+ content: [{
10696
+ type: "text",
10697
+ text: JSON.stringify({
10698
+ decision,
10699
+ closePermission: decision === "complete" ? "granted" : "denied",
10700
+ completionSummary: completion_summary,
10701
+ claimedCriteriaSatisfied: claimed_criteria_satisfied,
10702
+ knownLimitations: known_limitations,
10703
+ evidenceRefs: evidence_refs,
10704
+ unsatisfiedCriteria,
10705
+ blockingFindings,
10706
+ requiredNextSteps,
10707
+ governance
10708
+ }, null, 2)
10709
+ }]
10710
+ };
10711
+ }
10712
+ // ─────────────────────────────────────────────────────────────────
10382
10713
  // GENERATE_CUTLINE_MD
10383
10714
  // ─────────────────────────────────────────────────────────────────
10384
10715
  case "generate_cutline_md": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibekiln/cutline-mcp-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI and MCP servers for Cutline — authenticate, then run constraint-aware MCP servers in Cursor or any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",