patchwork-os 0.2.0-beta.10.canary.98 → 0.2.0-beta.11
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/README.md +20 -1
- package/dist/adapters/grok.js +6 -1
- package/dist/adapters/grok.js.map +1 -1
- package/dist/approvalHttp.js +61 -66
- package/dist/approvalHttp.js.map +1 -1
- package/dist/bridge.js +3 -2
- package/dist/bridge.js.map +1 -1
- package/dist/claudeOrchestrator.js +15 -0
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/connect.d.ts +47 -0
- package/dist/commands/connect.js +419 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/patchworkInit.d.ts +7 -0
- package/dist/commands/patchworkInit.js +26 -0
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +14 -0
- package/dist/commands/recipe.js +51 -0
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +15 -0
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/connectors/postgres.d.ts +7 -0
- package/dist/connectors/postgres.js +16 -2
- package/dist/connectors/postgres.js.map +1 -1
- package/dist/connectors/snowflake.js +6 -0
- package/dist/connectors/snowflake.js.map +1 -1
- package/dist/connectors/tokenStorage.d.ts +8 -0
- package/dist/connectors/tokenStorage.js +42 -3
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/drivers/claude/api.js +15 -2
- package/dist/drivers/claude/api.js.map +1 -1
- package/dist/drivers/claude/subprocess.js +1 -29
- package/dist/drivers/claude/subprocess.js.map +1 -1
- package/dist/drivers/gemini/index.js +9 -5
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/openai/index.js +29 -4
- package/dist/drivers/openai/index.js.map +1 -1
- package/dist/drivers/outputCap.d.ts +27 -0
- package/dist/drivers/outputCap.js +50 -0
- package/dist/drivers/outputCap.js.map +1 -0
- package/dist/featureFlags.d.ts +9 -0
- package/dist/featureFlags.js +16 -0
- package/dist/featureFlags.js.map +1 -1
- package/dist/fp/commandDescription.d.ts +6 -0
- package/dist/fp/commandDescription.js +4 -4
- package/dist/fp/commandDescription.js.map +1 -1
- package/dist/fp/policyParser.js +13 -7
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/haltPushDispatch.js +7 -22
- package/dist/haltPushDispatch.js.map +1 -1
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/mcpRoutes.js +1 -1
- package/dist/mcpRoutes.js.map +1 -1
- package/dist/oauth.d.ts +13 -0
- package/dist/oauth.js +34 -1
- package/dist/oauth.js.map +1 -1
- package/dist/recipeOrchestration.js +10 -0
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +39 -0
- package/dist/recipeRoutes.js +214 -12
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +20 -0
- package/dist/recipes/agentExecutor.js +32 -10
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +41 -2
- package/dist/recipes/chainedRunner.js +134 -6
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.js +30 -0
- package/dist/recipes/connectorPreflight.js.map +1 -1
- package/dist/recipes/haltCategory.d.ts +5 -0
- package/dist/recipes/haltCategory.js +7 -1
- package/dist/recipes/haltCategory.js.map +1 -1
- package/dist/recipes/judgeVerdict.d.ts +10 -0
- package/dist/recipes/judgeVerdict.js +10 -0
- package/dist/recipes/judgeVerdict.js.map +1 -1
- package/dist/recipes/names.d.ts +5 -0
- package/dist/recipes/names.js +10 -5
- package/dist/recipes/names.js.map +1 -1
- package/dist/recipes/parser.js +32 -9
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/pricing/costRouter.d.ts +43 -0
- package/dist/recipes/pricing/costRouter.js +44 -0
- package/dist/recipes/pricing/costRouter.js.map +1 -0
- package/dist/recipes/pricing/priceTable.d.ts +76 -0
- package/dist/recipes/pricing/priceTable.js +144 -0
- package/dist/recipes/pricing/priceTable.js.map +1 -0
- package/dist/recipes/runBudget.d.ts +82 -32
- package/dist/recipes/runBudget.js +182 -49
- package/dist/recipes/runBudget.js.map +1 -1
- package/dist/recipes/schema.d.ts +25 -4
- package/dist/recipes/schemaGenerator.d.ts +9 -0
- package/dist/recipes/schemaGenerator.js +386 -75
- package/dist/recipes/schemaGenerator.js.map +1 -1
- package/dist/recipes/simulation/aggregateRunRisk.d.ts +43 -0
- package/dist/recipes/simulation/aggregateRunRisk.js +117 -0
- package/dist/recipes/simulation/aggregateRunRisk.js.map +1 -0
- package/dist/recipes/simulation/costProjector.d.ts +32 -0
- package/dist/recipes/simulation/costProjector.js +194 -0
- package/dist/recipes/simulation/costProjector.js.map +1 -0
- package/dist/recipes/simulation/sideEffects.d.ts +32 -0
- package/dist/recipes/simulation/sideEffects.js +62 -0
- package/dist/recipes/simulation/sideEffects.js.map +1 -0
- package/dist/recipes/simulation/simulate.d.ts +36 -0
- package/dist/recipes/simulation/simulate.js +264 -0
- package/dist/recipes/simulation/simulate.js.map +1 -0
- package/dist/recipes/simulation/simulateMockedRun.d.ts +52 -0
- package/dist/recipes/simulation/simulateMockedRun.js +72 -0
- package/dist/recipes/simulation/simulateMockedRun.js.map +1 -0
- package/dist/recipes/simulation/synthesizeMockedOutputs.d.ts +31 -0
- package/dist/recipes/simulation/synthesizeMockedOutputs.js +50 -0
- package/dist/recipes/simulation/synthesizeMockedOutputs.js.map +1 -0
- package/dist/recipes/simulation/types.d.ts +198 -0
- package/dist/recipes/simulation/types.js +30 -0
- package/dist/recipes/simulation/types.js.map +1 -0
- package/dist/recipes/stepObservation.d.ts +12 -0
- package/dist/recipes/stepObservation.js +21 -1
- package/dist/recipes/stepObservation.js.map +1 -1
- package/dist/recipes/tools/airtable.d.ts +15 -0
- package/dist/recipes/tools/airtable.js +240 -0
- package/dist/recipes/tools/airtable.js.map +1 -0
- package/dist/recipes/tools/caldiy.d.ts +13 -0
- package/dist/recipes/tools/caldiy.js +214 -0
- package/dist/recipes/tools/caldiy.js.map +1 -0
- package/dist/recipes/tools/circleci.d.ts +10 -0
- package/dist/recipes/tools/circleci.js +204 -0
- package/dist/recipes/tools/circleci.js.map +1 -0
- package/dist/recipes/tools/cloudflare.d.ts +13 -0
- package/dist/recipes/tools/cloudflare.js +211 -0
- package/dist/recipes/tools/cloudflare.js.map +1 -0
- package/dist/recipes/tools/docs.d.ts +18 -0
- package/dist/recipes/tools/docs.js +94 -0
- package/dist/recipes/tools/docs.js.map +1 -0
- package/dist/recipes/tools/elasticsearch.d.ts +11 -0
- package/dist/recipes/tools/elasticsearch.js +156 -0
- package/dist/recipes/tools/elasticsearch.js.map +1 -0
- package/dist/recipes/tools/figma.d.ts +12 -0
- package/dist/recipes/tools/figma.js +194 -0
- package/dist/recipes/tools/figma.js.map +1 -0
- package/dist/recipes/tools/grafana.d.ts +11 -0
- package/dist/recipes/tools/grafana.js +216 -0
- package/dist/recipes/tools/grafana.js.map +1 -0
- package/dist/recipes/tools/http.d.ts +1 -1
- package/dist/recipes/tools/http.js +10 -28
- package/dist/recipes/tools/http.js.map +1 -1
- package/dist/recipes/tools/index.d.ts +26 -0
- package/dist/recipes/tools/index.js +26 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/tools/monday.d.ts +22 -0
- package/dist/recipes/tools/monday.js +242 -0
- package/dist/recipes/tools/monday.js.map +1 -0
- package/dist/recipes/tools/obsidian.d.ts +15 -0
- package/dist/recipes/tools/obsidian.js +172 -0
- package/dist/recipes/tools/obsidian.js.map +1 -0
- package/dist/recipes/tools/paystack.d.ts +11 -0
- package/dist/recipes/tools/paystack.js +211 -0
- package/dist/recipes/tools/paystack.js.map +1 -0
- package/dist/recipes/tools/pipedrive.d.ts +16 -0
- package/dist/recipes/tools/pipedrive.js +233 -0
- package/dist/recipes/tools/pipedrive.js.map +1 -0
- package/dist/recipes/tools/postgres.d.ts +15 -0
- package/dist/recipes/tools/postgres.js +185 -0
- package/dist/recipes/tools/postgres.js.map +1 -0
- package/dist/recipes/tools/posthog.d.ts +16 -0
- package/dist/recipes/tools/posthog.js +219 -0
- package/dist/recipes/tools/posthog.js.map +1 -0
- package/dist/recipes/tools/redis.d.ts +9 -0
- package/dist/recipes/tools/redis.js +140 -0
- package/dist/recipes/tools/redis.js.map +1 -0
- package/dist/recipes/tools/resend.d.ts +8 -0
- package/dist/recipes/tools/resend.js +153 -0
- package/dist/recipes/tools/resend.js.map +1 -0
- package/dist/recipes/tools/salesforce.d.ts +16 -0
- package/dist/recipes/tools/salesforce.js +184 -0
- package/dist/recipes/tools/salesforce.js.map +1 -0
- package/dist/recipes/tools/sendgrid.d.ts +9 -0
- package/dist/recipes/tools/sendgrid.js +174 -0
- package/dist/recipes/tools/sendgrid.js.map +1 -0
- package/dist/recipes/tools/shopify.d.ts +16 -0
- package/dist/recipes/tools/shopify.js +265 -0
- package/dist/recipes/tools/shopify.js.map +1 -0
- package/dist/recipes/tools/snowflake.d.ts +16 -0
- package/dist/recipes/tools/snowflake.js +173 -0
- package/dist/recipes/tools/snowflake.js.map +1 -0
- package/dist/recipes/tools/stripe.js +13 -12
- package/dist/recipes/tools/stripe.js.map +1 -1
- package/dist/recipes/tools/supabase.d.ts +9 -0
- package/dist/recipes/tools/supabase.js +132 -0
- package/dist/recipes/tools/supabase.js.map +1 -0
- package/dist/recipes/tools/todoist.d.ts +15 -0
- package/dist/recipes/tools/todoist.js +227 -0
- package/dist/recipes/tools/todoist.js.map +1 -0
- package/dist/recipes/tools/twilio.d.ts +11 -0
- package/dist/recipes/tools/twilio.js +180 -0
- package/dist/recipes/tools/twilio.js.map +1 -0
- package/dist/recipes/tools/vercel.d.ts +9 -0
- package/dist/recipes/tools/vercel.js +145 -0
- package/dist/recipes/tools/vercel.js.map +1 -0
- package/dist/recipes/tools/webflow.d.ts +11 -0
- package/dist/recipes/tools/webflow.js +243 -0
- package/dist/recipes/tools/webflow.js.map +1 -0
- package/dist/recipes/tools/woocommerce.d.ts +18 -0
- package/dist/recipes/tools/woocommerce.js +259 -0
- package/dist/recipes/tools/woocommerce.js.map +1 -0
- package/dist/recipes/tools/wrapConnectorExecute.d.ts +25 -0
- package/dist/recipes/tools/wrapConnectorExecute.js +37 -0
- package/dist/recipes/tools/wrapConnectorExecute.js.map +1 -0
- package/dist/recipes/validation.js +264 -0
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +112 -3
- package/dist/recipes/yamlRunner.js +482 -19
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/runLog.d.ts +46 -0
- package/dist/runLog.js +4 -0
- package/dist/runLog.js.map +1 -1
- package/dist/schemas/recipe.v1.json +217 -11
- package/dist/server.d.ts +3 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.js +3 -2
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/gitWrite.js +5 -1
- package/dist/tools/gitWrite.js.map +1 -1
- package/dist/tools/terminal.js +15 -49
- package/dist/tools/terminal.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +7 -0
- package/dist/transport.js.map +1 -1
- package/package.json +3 -3
- package/scripts/mcp-stdio-shim.cjs +218 -84
- package/templates/recipes/incident-to-pr.yaml +187 -0
|
@@ -30,6 +30,27 @@ const { randomUUID } = require("node:crypto");
|
|
|
30
30
|
// Resolve ws from the bridge's own node_modules (script lives in scripts/)
|
|
31
31
|
const { WebSocket } = require(path.join(__dirname, "..", "node_modules", "ws"));
|
|
32
32
|
|
|
33
|
+
// process.kill(pid, 0) probes liveness without delivering a signal: it throws
|
|
34
|
+
// ESRCH when the process is gone and EPERM when it exists but isn't ours (still
|
|
35
|
+
// alive). A stale lock whose bridge has exited would otherwise be "selected"
|
|
36
|
+
// (newest mtime within its tier) and every connect would hang on a refused port
|
|
37
|
+
// until backoff gives up — the multi-bridge footgun. Mirrors the shared
|
|
38
|
+
// src/bridgeLockDiscovery.ts isLive() check.
|
|
39
|
+
function isAlive(pid) {
|
|
40
|
+
if (typeof pid !== "number" || !Number.isFinite(pid)) return true; // unknown pid → don't exclude
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return err.code === "EPERM";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remembers the last lock we logged a selection for, so the diagnostic below
|
|
50
|
+
// fires once at startup (and again only when the selection actually changes)
|
|
51
|
+
// rather than on every poll tick.
|
|
52
|
+
let lastSelectedLockPath = null;
|
|
53
|
+
|
|
33
54
|
function findLockFile() {
|
|
34
55
|
const lockDir = path.join(
|
|
35
56
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
|
|
@@ -38,49 +59,40 @@ function findLockFile() {
|
|
|
38
59
|
// Three-tier preference: orchestrator > bridge > any lock (each by newest mtime).
|
|
39
60
|
// Orchestrator locks win because child bridge restarts produce newer lock files,
|
|
40
61
|
// which would hijack the shim away from the orchestrator if we only used mtime.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let bridgeMtime = 0;
|
|
45
|
-
let fallbackLock = null;
|
|
46
|
-
let fallbackMtime = 0;
|
|
62
|
+
const orchestrator = { path: null, mtime: 0, data: null };
|
|
63
|
+
const bridge = { path: null, mtime: 0, data: null };
|
|
64
|
+
const fallback = { path: null, mtime: 0, data: null };
|
|
47
65
|
try {
|
|
48
66
|
for (const f of fs.readdirSync(lockDir)) {
|
|
49
67
|
if (!f.endsWith(".lock")) continue;
|
|
50
68
|
const fullPath = path.join(lockDir, f);
|
|
51
69
|
try {
|
|
52
70
|
const stat = fs.statSync(fullPath);
|
|
53
|
-
let
|
|
54
|
-
let isOrchestrator = false;
|
|
71
|
+
let data = null;
|
|
55
72
|
try {
|
|
56
|
-
|
|
57
|
-
isBridge = data.isBridge === true;
|
|
58
|
-
isOrchestrator = data.orchestrator === true;
|
|
59
|
-
// Skip locks that don't match the requested workspace
|
|
60
|
-
if (
|
|
61
|
-
workspaceFilter &&
|
|
62
|
-
data.workspace &&
|
|
63
|
-
data.workspace !== workspaceFilter
|
|
64
|
-
)
|
|
65
|
-
continue;
|
|
73
|
+
data = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
66
74
|
} catch {
|
|
67
|
-
// unparseable —
|
|
75
|
+
// unparseable — treated as a fallback lock with no metadata
|
|
68
76
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
// Skip locks whose owning bridge process has exited. Without this a
|
|
78
|
+
// wedged/stale lock can be picked and the connection hangs.
|
|
79
|
+
if (data && !isAlive(data.pid)) continue;
|
|
80
|
+
// When a workspace filter is set, require an EXACT workspace match — a
|
|
81
|
+
// lock with a missing/blank workspace field must not be picked under the
|
|
82
|
+
// filter (otherwise the shim can latch onto an unrelated bridge).
|
|
83
|
+
if (workspaceFilter && (!data || data.workspace !== workspaceFilter))
|
|
84
|
+
continue;
|
|
85
|
+
const isBridge = data ? data.isBridge === true : false;
|
|
86
|
+
const isOrchestrator = data ? data.orchestrator === true : false;
|
|
87
|
+
const tier = isOrchestrator
|
|
88
|
+
? orchestrator
|
|
89
|
+
: isBridge
|
|
90
|
+
? bridge
|
|
91
|
+
: fallback;
|
|
92
|
+
if (stat.mtimeMs > tier.mtime) {
|
|
93
|
+
tier.mtime = stat.mtimeMs;
|
|
94
|
+
tier.path = fullPath;
|
|
95
|
+
tier.data = data;
|
|
84
96
|
}
|
|
85
97
|
} catch {
|
|
86
98
|
// skip unreadable files
|
|
@@ -90,7 +102,25 @@ function findLockFile() {
|
|
|
90
102
|
// lock dir doesn't exist
|
|
91
103
|
}
|
|
92
104
|
// Prefer orchestrator > bridge > fallback (each newest mtime within tier)
|
|
93
|
-
|
|
105
|
+
const chosen = orchestrator.path
|
|
106
|
+
? orchestrator
|
|
107
|
+
: bridge.path
|
|
108
|
+
? bridge
|
|
109
|
+
: fallback.path
|
|
110
|
+
? fallback
|
|
111
|
+
: null;
|
|
112
|
+
// One-line stderr diagnostic so operators can see which bridge/workspace the
|
|
113
|
+
// shim latched onto — silent wrong-bridge selection was a real footgun. stderr
|
|
114
|
+
// is not the JSON-RPC stream, so this never corrupts protocol traffic.
|
|
115
|
+
if (chosen && chosen.path !== lastSelectedLockPath) {
|
|
116
|
+
lastSelectedLockPath = chosen.path;
|
|
117
|
+
const wsName = chosen.data?.workspace || "(unknown workspace)";
|
|
118
|
+
const pid = chosen.data?.pid || "?";
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
`mcp-stdio-shim: selected ${path.basename(chosen.path)} (workspace=${wsName}, pid=${pid})\n`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return chosen ? chosen.path : null;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
function parseLock(lockPath) {
|
|
@@ -199,11 +229,16 @@ function scheduleBackoffReconnect(errorType) {
|
|
|
199
229
|
// Parse named flags first so they don't pollute positional port/token detection.
|
|
200
230
|
const args = process.argv.slice(2);
|
|
201
231
|
let workspaceFilter = null;
|
|
232
|
+
let pingMode = false;
|
|
202
233
|
for (let i = 0; i < args.length; i++) {
|
|
203
|
-
if (args[i] === "--
|
|
234
|
+
if (args[i] === "--ping" || args[i] === "--probe") {
|
|
235
|
+
pingMode = true;
|
|
236
|
+
args.splice(i, 1);
|
|
237
|
+
i--;
|
|
238
|
+
} else if (args[i] === "--workspace" && args[i + 1] !== undefined) {
|
|
204
239
|
workspaceFilter = args[i + 1];
|
|
205
240
|
args.splice(i, 2);
|
|
206
|
-
|
|
241
|
+
i--;
|
|
207
242
|
}
|
|
208
243
|
}
|
|
209
244
|
const explicitPort = args[0] && args[1] ? Number(args[0]) : null;
|
|
@@ -369,8 +404,104 @@ function startPoll() {
|
|
|
369
404
|
}, POLL_INTERVAL_MS);
|
|
370
405
|
}
|
|
371
406
|
|
|
372
|
-
// ---
|
|
373
|
-
|
|
407
|
+
// --- Ping mode: one-shot probe, then exit ---
|
|
408
|
+
// `grok mcp doctor` (and similar stdio MCP verifiers) hang on this shim because
|
|
409
|
+
// it is a long-lived relay that only exits on stdin EOF. `--ping` / `--probe`
|
|
410
|
+
// gives those tools a clean check: connect, run initialize + tools/list, print a
|
|
411
|
+
// one-line summary, and exit. It skips the relay, lock watcher, and reconnect
|
|
412
|
+
// loops entirely.
|
|
413
|
+
if (pingMode) {
|
|
414
|
+
let pingPort = explicitPort;
|
|
415
|
+
let pingToken = explicitToken;
|
|
416
|
+
if (pingPort === null) {
|
|
417
|
+
const lockFile = findLockFile();
|
|
418
|
+
if (!lockFile) {
|
|
419
|
+
process.stderr.write("mcp-stdio-shim: --ping: no bridge lock found.\n");
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const parsed = parseLock(lockFile);
|
|
424
|
+
pingPort = parsed.port;
|
|
425
|
+
pingToken = parsed.authToken;
|
|
426
|
+
} catch (err) {
|
|
427
|
+
process.stderr.write(
|
|
428
|
+
`mcp-stdio-shim: --ping: lock unreadable (${err.message}).\n`,
|
|
429
|
+
);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let pingDone = false;
|
|
434
|
+
const pingWs = new WebSocket(`ws://127.0.0.1:${pingPort}`, {
|
|
435
|
+
headers: {
|
|
436
|
+
"x-claude-code-ide-authorization": pingToken,
|
|
437
|
+
"x-claude-code-session-id": CLIENT_SESSION_ID,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
const failPing = (msg) => {
|
|
441
|
+
if (pingDone) return;
|
|
442
|
+
pingDone = true;
|
|
443
|
+
process.stderr.write(`mcp-stdio-shim: --ping: ${msg}\n`);
|
|
444
|
+
try {
|
|
445
|
+
pingWs.terminate();
|
|
446
|
+
} catch {
|
|
447
|
+
/* ignore */
|
|
448
|
+
}
|
|
449
|
+
process.exit(1);
|
|
450
|
+
};
|
|
451
|
+
const pingTimer = setTimeout(
|
|
452
|
+
() => failPing(`timed out after 5s (port ${pingPort})`),
|
|
453
|
+
5000,
|
|
454
|
+
);
|
|
455
|
+
pingWs.on("open", () => {
|
|
456
|
+
pingWs.send(
|
|
457
|
+
JSON.stringify({
|
|
458
|
+
jsonrpc: "2.0",
|
|
459
|
+
id: 1,
|
|
460
|
+
method: "initialize",
|
|
461
|
+
params: {
|
|
462
|
+
protocolVersion: "2025-06-18",
|
|
463
|
+
capabilities: {},
|
|
464
|
+
clientInfo: { name: "shim-ping", version: "1" },
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
pingWs.on("message", (raw) => {
|
|
470
|
+
let msg;
|
|
471
|
+
try {
|
|
472
|
+
msg = JSON.parse(raw.toString());
|
|
473
|
+
} catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (msg.id === 1) {
|
|
477
|
+
pingWs.send(
|
|
478
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
479
|
+
);
|
|
480
|
+
pingWs.send(
|
|
481
|
+
JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
|
|
482
|
+
);
|
|
483
|
+
} else if (msg.id === 2) {
|
|
484
|
+
clearTimeout(pingTimer);
|
|
485
|
+
pingDone = true;
|
|
486
|
+
const n =
|
|
487
|
+
msg.result && Array.isArray(msg.result.tools)
|
|
488
|
+
? msg.result.tools.length
|
|
489
|
+
: "?";
|
|
490
|
+
process.stdout.write(`bridge OK — port ${pingPort}, ${n} tools\n`);
|
|
491
|
+
try {
|
|
492
|
+
pingWs.close();
|
|
493
|
+
} catch {
|
|
494
|
+
/* ignore */
|
|
495
|
+
}
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
pingWs.on("error", (err) => failPing(`connect failed (${err.message})`));
|
|
500
|
+
pingWs.on("close", (code) =>
|
|
501
|
+
failPing(`closed before tools/list (code ${code})`),
|
|
502
|
+
);
|
|
503
|
+
} else if (explicitPort !== null) {
|
|
504
|
+
// --- Initial connection ---
|
|
374
505
|
connect(explicitPort, explicitToken);
|
|
375
506
|
} else {
|
|
376
507
|
const lockFile = findLockFile();
|
|
@@ -398,7 +529,7 @@ if (explicitPort !== null) {
|
|
|
398
529
|
// Watches ~/.claude/ide/ for .lock file changes. When a new bridge lock appears
|
|
399
530
|
// on a different port, reconnects automatically. This means Claude Desktop never
|
|
400
531
|
// needs to be restarted when the bridge restarts on a new port.
|
|
401
|
-
if (explicitPort === null) {
|
|
532
|
+
if (!pingMode && explicitPort === null) {
|
|
402
533
|
const lockDir = path.join(
|
|
403
534
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
|
|
404
535
|
"ide",
|
|
@@ -439,52 +570,38 @@ if (explicitPort === null) {
|
|
|
439
570
|
|
|
440
571
|
// --- stdin → bridge (newline-delimited JSON-RPC) ---
|
|
441
572
|
// Messages that arrive before the WebSocket is open are queued and flushed on connect.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
573
|
+
// Skipped in --ping mode — the one-shot probe manages its own lifecycle and exits.
|
|
574
|
+
if (!pingMode) {
|
|
575
|
+
process.stdin.setEncoding("utf8");
|
|
576
|
+
process.stdin.on("data", (chunk) => {
|
|
577
|
+
buffer += chunk;
|
|
578
|
+
const lines = buffer.split("\n");
|
|
579
|
+
buffer = lines.pop() ?? "";
|
|
580
|
+
for (const line of lines) {
|
|
581
|
+
const trimmed = line.trim();
|
|
582
|
+
if (!trimmed) continue;
|
|
583
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
584
|
+
try {
|
|
585
|
+
ws.send(trimmed);
|
|
586
|
+
} catch (sendErr) {
|
|
587
|
+
process.stderr.write(
|
|
588
|
+
`mcp-stdio-shim: ws.send failed (${sendErr.message}) — queuing message.\n`,
|
|
589
|
+
);
|
|
590
|
+
if (pendingLines.length < MAX_PENDING_LINES) {
|
|
591
|
+
pendingLines.push(trimmed);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} else if (pendingLines.length < MAX_PENDING_LINES) {
|
|
595
|
+
pendingLines.push(trimmed);
|
|
596
|
+
} else {
|
|
454
597
|
process.stderr.write(
|
|
455
|
-
`mcp-stdio-shim:
|
|
598
|
+
`mcp-stdio-shim: pendingLines overflow (${MAX_PENDING_LINES}) — dropping message (bridge not connected)\n`,
|
|
456
599
|
);
|
|
457
|
-
if (pendingLines.length < MAX_PENDING_LINES) {
|
|
458
|
-
pendingLines.push(trimmed);
|
|
459
|
-
}
|
|
460
600
|
}
|
|
461
|
-
} else if (pendingLines.length < MAX_PENDING_LINES) {
|
|
462
|
-
pendingLines.push(trimmed);
|
|
463
|
-
} else {
|
|
464
|
-
process.stderr.write(
|
|
465
|
-
`mcp-stdio-shim: pendingLines overflow (${MAX_PENDING_LINES}) — dropping message (bridge not connected)\n`,
|
|
466
|
-
);
|
|
467
601
|
}
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
process.stdin.on("end", () => {
|
|
472
|
-
if (ws) {
|
|
473
|
-
try {
|
|
474
|
-
ws.close();
|
|
475
|
-
} catch {
|
|
476
|
-
/* ignore */
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
process.exit(0);
|
|
480
|
-
});
|
|
602
|
+
});
|
|
481
603
|
|
|
482
|
-
process.stdin.on("
|
|
483
|
-
// EPIPE / ERR_STREAM_DESTROYED means the MCP host closed the pipe — clean shutdown.
|
|
484
|
-
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
|
|
485
|
-
process.stderr.write(
|
|
486
|
-
`mcp-stdio-shim: stdin closed (${err.code}) — shutting down.\n`,
|
|
487
|
-
);
|
|
604
|
+
process.stdin.on("end", () => {
|
|
488
605
|
if (ws) {
|
|
489
606
|
try {
|
|
490
607
|
ws.close();
|
|
@@ -493,6 +610,23 @@ process.stdin.on("error", (err) => {
|
|
|
493
610
|
}
|
|
494
611
|
}
|
|
495
612
|
process.exit(0);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
process.stdin.on("error", (err) => {
|
|
616
|
+
// EPIPE / ERR_STREAM_DESTROYED means the MCP host closed the pipe — clean shutdown.
|
|
617
|
+
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
|
|
618
|
+
process.stderr.write(
|
|
619
|
+
`mcp-stdio-shim: stdin closed (${err.code}) — shutting down.\n`,
|
|
620
|
+
);
|
|
621
|
+
if (ws) {
|
|
622
|
+
try {
|
|
623
|
+
ws.close();
|
|
624
|
+
} catch {
|
|
625
|
+
/* ignore */
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
process.stderr.write(`mcp-stdio-shim: stdin error: ${err.message}\n`);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/patchworkos/recipes/main/schema/recipe.v1.json
|
|
2
|
+
apiVersion: patchwork.sh/v1
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: incident-to-pr
|
|
5
|
+
description: |
|
|
6
|
+
FLAGSHIP — the end-to-end AI-native incident-response loop. Turns a
|
|
7
|
+
production incident into a reviewed, ready-to-merge pull request. Every
|
|
8
|
+
primitive Patchwork added during the connector + judge→refine campaign
|
|
9
|
+
shows up here in one pipeline:
|
|
10
|
+
|
|
11
|
+
1. investigate — an agent root-causes the incident against THIS repo
|
|
12
|
+
using the read/git tools it has via MCP (no writes).
|
|
13
|
+
2. draft_fix — an agent writes the smallest correct patch + a PR
|
|
14
|
+
title/body. This is the draft the judge reviews.
|
|
15
|
+
3. review_fix — a JUDGE step (agent.kind: judge) gives the draft a
|
|
16
|
+
cold-eyes senior-engineer review. On `request_changes`
|
|
17
|
+
it DRIVES the judge→refine loop: the draft step is
|
|
18
|
+
re-run with the judge's fix-list injected, up to
|
|
19
|
+
`max_revisions` cycles, until the judge approves. This
|
|
20
|
+
is the moat — quality is gated by an AI reviewer, not
|
|
21
|
+
by hope.
|
|
22
|
+
4. open_pr — an agent opens the PR via the githubCreatePR MCP tool.
|
|
23
|
+
Opening a PR is a high-risk WRITE, so the runtime
|
|
24
|
+
approval gate pauses here for a human to sign off — the
|
|
25
|
+
human-in-the-loop checkpoint before anything lands.
|
|
26
|
+
5. notify — posts the PR link + judge verdict to Slack.
|
|
27
|
+
|
|
28
|
+
REQUIREMENTS
|
|
29
|
+
- Bridge running with `--driver subprocess` so the agent steps have MCP
|
|
30
|
+
tool access (read files, git, githubCreatePR).
|
|
31
|
+
- GitHub connector connected (for the PR) and Slack connector connected
|
|
32
|
+
(for the alert) — visit /connections, or `patchwork connect github` /
|
|
33
|
+
`patchwork connect slack`.
|
|
34
|
+
- An approval gate of "high" or "all" to exercise the open_pr pause
|
|
35
|
+
(with the gate off, the PR opens unattended).
|
|
36
|
+
|
|
37
|
+
MANUAL RUN
|
|
38
|
+
patchwork recipe run incident-to-pr \
|
|
39
|
+
--var INCIDENT_TITLE="checkout 500s on /api/pay" \
|
|
40
|
+
--var STACK_TRACE="TypeError: cannot read 'id' of undefined at pay.ts:42"
|
|
41
|
+
|
|
42
|
+
WEBHOOK CONVERSION (PagerDuty / Sentry / Datadog)
|
|
43
|
+
Replace the trigger block with:
|
|
44
|
+
trigger:
|
|
45
|
+
type: webhook
|
|
46
|
+
path: /hooks/incident-to-pr
|
|
47
|
+
and reference {{payload.title}} / {{payload.body}} in place of the vars.
|
|
48
|
+
|
|
49
|
+
trigger:
|
|
50
|
+
type: manual
|
|
51
|
+
vars:
|
|
52
|
+
- name: INCIDENT_TITLE
|
|
53
|
+
description: "One-line incident summary (seeds the PR title)"
|
|
54
|
+
required: true
|
|
55
|
+
- name: STACK_TRACE
|
|
56
|
+
description: "Stack trace / error text to root-cause (optional, recommended)"
|
|
57
|
+
required: false
|
|
58
|
+
- name: SEVERITY
|
|
59
|
+
description: "critical | high | medium | low"
|
|
60
|
+
required: false
|
|
61
|
+
default: "high"
|
|
62
|
+
- name: SLACK_CHANNEL
|
|
63
|
+
description: "Slack channel for the resolution alert"
|
|
64
|
+
required: false
|
|
65
|
+
default: "#incidents"
|
|
66
|
+
|
|
67
|
+
steps:
|
|
68
|
+
- id: investigate
|
|
69
|
+
agent:
|
|
70
|
+
driver: claude-code
|
|
71
|
+
prompt: |
|
|
72
|
+
A production incident has fired. Root-cause it against THIS repository.
|
|
73
|
+
|
|
74
|
+
Incident: {{INCIDENT_TITLE}}
|
|
75
|
+
Severity: {{SEVERITY}}
|
|
76
|
+
Stack trace / error:
|
|
77
|
+
{{STACK_TRACE}}
|
|
78
|
+
|
|
79
|
+
Use the read and git tools available to you — read files, search the
|
|
80
|
+
codebase, inspect recent commits and run git blame on the implicated
|
|
81
|
+
files. Do NOT write any files in this step.
|
|
82
|
+
|
|
83
|
+
Respond with a concise root-cause analysis containing:
|
|
84
|
+
- the offending file(s) and line(s)
|
|
85
|
+
- what is going wrong and why
|
|
86
|
+
- the commit that most likely introduced it (if identifiable)
|
|
87
|
+
- the smallest correct fix you would make
|
|
88
|
+
into: investigation
|
|
89
|
+
risk: low
|
|
90
|
+
|
|
91
|
+
- id: draft_fix
|
|
92
|
+
awaits:
|
|
93
|
+
- investigate
|
|
94
|
+
agent:
|
|
95
|
+
driver: claude-code
|
|
96
|
+
prompt: |
|
|
97
|
+
Using the root-cause analysis below, write a concrete, MINIMAL fix.
|
|
98
|
+
|
|
99
|
+
Root-cause analysis:
|
|
100
|
+
{{investigation}}
|
|
101
|
+
|
|
102
|
+
Output exactly these four markdown sections and nothing else:
|
|
103
|
+
|
|
104
|
+
## PR title
|
|
105
|
+
<conventional-commit-style title, <= 70 chars>
|
|
106
|
+
|
|
107
|
+
## Summary
|
|
108
|
+
<2-4 sentences: what the bug was and how this change fixes it>
|
|
109
|
+
|
|
110
|
+
## Patch
|
|
111
|
+
<a unified diff of the smallest change that fixes the root cause —
|
|
112
|
+
touch as few lines as possible; no drive-by refactors>
|
|
113
|
+
|
|
114
|
+
## Test
|
|
115
|
+
<the test you would add or modify to prove the fix (a diff), or a
|
|
116
|
+
short note if existing coverage already exercises the path>
|
|
117
|
+
|
|
118
|
+
Do NOT write files in this step — only emit the four sections.
|
|
119
|
+
into: fix_draft
|
|
120
|
+
risk: low
|
|
121
|
+
|
|
122
|
+
- id: review_fix
|
|
123
|
+
awaits:
|
|
124
|
+
- draft_fix
|
|
125
|
+
agent:
|
|
126
|
+
kind: judge
|
|
127
|
+
reviews: fix_draft
|
|
128
|
+
max_revisions: 2
|
|
129
|
+
on_exhausted: proceed
|
|
130
|
+
driver: claude-code
|
|
131
|
+
prompt: |
|
|
132
|
+
You are a senior engineer doing a cold-eyes review of the proposed
|
|
133
|
+
fix below BEFORE it becomes a pull request. Judge it on:
|
|
134
|
+
- correctness: does it actually fix the root cause?
|
|
135
|
+
- minimality: smallest possible change, nothing unrelated?
|
|
136
|
+
- safety: regression risk, edge cases, missing guards?
|
|
137
|
+
- tests: is there a test that would FAIL without the fix?
|
|
138
|
+
- PR hygiene: clear, accurate title and summary?
|
|
139
|
+
|
|
140
|
+
Return a verdict: approve | request_changes | unparseable.
|
|
141
|
+
If request_changes, list concrete, actionable fixes — each one a
|
|
142
|
+
specific change the author must make. Those drive the revision loop;
|
|
143
|
+
the draft is regenerated with your fix-list and re-reviewed.
|
|
144
|
+
into: review
|
|
145
|
+
risk: low
|
|
146
|
+
|
|
147
|
+
- id: open_pr
|
|
148
|
+
awaits:
|
|
149
|
+
- review_fix
|
|
150
|
+
agent:
|
|
151
|
+
driver: claude-code
|
|
152
|
+
prompt: |
|
|
153
|
+
The fix below has passed judge review. Open a pull request for it.
|
|
154
|
+
|
|
155
|
+
Approved fix:
|
|
156
|
+
{{fix_draft}}
|
|
157
|
+
|
|
158
|
+
Do this:
|
|
159
|
+
1. Create a branch off the default branch named
|
|
160
|
+
fix/<short-slug-derived-from-the-PR-title>.
|
|
161
|
+
2. Apply the change from the "## Patch" section and add the test
|
|
162
|
+
from the "## Test" section.
|
|
163
|
+
3. Open a pull request using the githubCreatePR tool, with the
|
|
164
|
+
"## PR title" as the title and the "## Summary" — followed by a
|
|
165
|
+
line "Fixes incident: {{INCIDENT_TITLE}}" — as the body.
|
|
166
|
+
|
|
167
|
+
Opening a PR is a write action; it will pause at the approval gate
|
|
168
|
+
for a human to confirm before it lands. After it succeeds, respond
|
|
169
|
+
with ONLY this JSON object (no markdown fences):
|
|
170
|
+
{"prUrl": "<url>", "prNumber": <number>, "branch": "<branch>"}
|
|
171
|
+
|
|
172
|
+
If the GitHub connector is not connected, respond with:
|
|
173
|
+
{"error": "GitHub connector not connected — visit /connections"}
|
|
174
|
+
into: pr
|
|
175
|
+
risk: high
|
|
176
|
+
|
|
177
|
+
- id: notify
|
|
178
|
+
awaits:
|
|
179
|
+
- open_pr
|
|
180
|
+
tool: slack.post_message
|
|
181
|
+
channel: "{{SLACK_CHANNEL}}"
|
|
182
|
+
text: |
|
|
183
|
+
:white_check_mark: Incident fix ready for review
|
|
184
|
+
*Incident:* {{INCIDENT_TITLE}} ({{SEVERITY}})
|
|
185
|
+
*Pull request:* {{pr}}
|
|
186
|
+
*Judge review:* {{review}}
|
|
187
|
+
risk: medium
|