patchwork-os 0.2.0-beta.1 → 0.2.0-beta.3
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.bridge.md +5 -5
- package/README.md +156 -12
- package/deploy/deploy-dashboard.sh +25 -1
- package/deploy/macos/README.md +153 -0
- package/deploy/macos/com.patchwork.bridge.plist.template +54 -0
- package/deploy/macos/com.patchwork.tunnel.plist.template +76 -0
- package/deploy/macos/install-mac-bridge.sh +244 -0
- package/deploy/macos/uninstall-mac-bridge.sh +22 -0
- package/dist/activityLog.d.ts +6 -0
- package/dist/activityLog.js +8 -0
- package/dist/activityLog.js.map +1 -1
- package/dist/analyticsPrefs.d.ts +35 -2
- package/dist/analyticsPrefs.js +120 -21
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/analyticsSend.js +5 -1
- package/dist/analyticsSend.js.map +1 -1
- package/dist/approvalHttp.d.ts +14 -0
- package/dist/approvalHttp.js +172 -1
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +27 -2
- package/dist/approvalQueue.js +44 -7
- package/dist/approvalQueue.js.map +1 -1
- package/dist/automation.d.ts +34 -3
- package/dist/automation.js +85 -10
- package/dist/automation.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +114 -8
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeLockDiscovery.d.ts +27 -1
- package/dist/bridgeLockDiscovery.js +37 -11
- package/dist/bridgeLockDiscovery.js.map +1 -1
- package/dist/claudeOrchestrator.js +5 -2
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/patchworkInit.d.ts +5 -0
- package/dist/commands/patchworkInit.js +86 -7
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +51 -0
- package/dist/commands/recipe.js +363 -3
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +6 -3
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/commands/task.js +2 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/config.d.ts +17 -2
- package/dist/config.js +54 -17
- package/dist/config.js.map +1 -1
- package/dist/connectors/baseConnector.js +25 -3
- package/dist/connectors/baseConnector.js.map +1 -1
- package/dist/connectors/tokenStorage.js +46 -10
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/drivers/gemini/index.d.ts +22 -0
- package/dist/drivers/gemini/index.js +240 -129
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/local/index.d.ts +17 -0
- package/dist/drivers/local/index.js +99 -0
- package/dist/drivers/local/index.js.map +1 -1
- package/dist/drivers/openai/index.js +30 -2
- package/dist/drivers/openai/index.js.map +1 -1
- package/dist/extensionClient.d.ts +8 -0
- package/dist/extensionClient.js +24 -2
- package/dist/extensionClient.js.map +1 -1
- package/dist/featureFlags.d.ts +76 -0
- package/dist/featureFlags.js +166 -2
- package/dist/featureFlags.js.map +1 -1
- package/dist/fp/automationInterpreter.d.ts +9 -1
- package/dist/fp/automationInterpreter.js +151 -34
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +30 -0
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/automationState.d.ts +23 -4
- package/dist/fp/automationState.js +28 -4
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/interpreterContext.d.ts +66 -1
- package/dist/fp/interpreterContext.js +140 -1
- package/dist/fp/interpreterContext.js.map +1 -1
- package/dist/fp/policyParser.js +29 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/index.js +765 -69
- package/dist/index.js.map +1 -1
- package/dist/lockfile.js +4 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/oauth.d.ts +9 -0
- package/dist/oauth.js +33 -0
- package/dist/oauth.js.map +1 -1
- package/dist/patchworkConfig.d.ts +16 -0
- package/dist/patchworkConfig.js +5 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipeOrchestration.js +35 -1
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +36 -0
- package/dist/recipeRoutes.js +231 -32
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +25 -5
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.js +16 -2
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.d.ts +53 -0
- package/dist/recipes/connectorPreflight.js +79 -0
- package/dist/recipes/connectorPreflight.js.map +1 -0
- package/dist/recipes/githubInstallSource.d.ts +62 -0
- package/dist/recipes/githubInstallSource.js +125 -0
- package/dist/recipes/githubInstallSource.js.map +1 -0
- package/dist/recipes/haltCategory.d.ts +80 -0
- package/dist/recipes/haltCategory.js +125 -0
- package/dist/recipes/haltCategory.js.map +1 -0
- package/dist/recipes/idempotencyKey.d.ts +126 -0
- package/dist/recipes/idempotencyKey.js +298 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/judgeSummary.d.ts +50 -0
- package/dist/recipes/judgeSummary.js +47 -0
- package/dist/recipes/judgeSummary.js.map +1 -0
- package/dist/recipes/judgeVerdict.d.ts +48 -0
- package/dist/recipes/judgeVerdict.js +174 -0
- package/dist/recipes/judgeVerdict.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +9 -0
- package/dist/recipes/migrations/index.js +133 -0
- package/dist/recipes/migrations/index.js.map +1 -1
- package/dist/recipes/runBudget.d.ts +70 -0
- package/dist/recipes/runBudget.js +109 -0
- package/dist/recipes/runBudget.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +7 -0
- package/dist/recipes/scheduler.js +31 -14
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +36 -0
- package/dist/recipes/toolRegistry.js +19 -0
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/file.js +5 -2
- package/dist/recipes/tools/file.js.map +1 -1
- package/dist/recipes/tools/http.d.ts +10 -0
- package/dist/recipes/tools/http.js +176 -0
- package/dist/recipes/tools/http.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +1 -0
- package/dist/recipes/tools/index.js +1 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/validation.js +1 -1
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +88 -7
- package/dist/recipes/yamlRunner.js +216 -25
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +3 -1
- package/dist/recipesHttp.js +9 -3
- package/dist/recipesHttp.js.map +1 -1
- package/dist/runLog.d.ts +28 -0
- package/dist/runLog.js +5 -0
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +111 -1
- package/dist/server.js +480 -6
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.d.ts +9 -4
- package/dist/streamableHttp.js +34 -15
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/bridgeDoctor.js +6 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/ccRoutines.d.ts +221 -0
- package/dist/tools/ccRoutines.js +264 -0
- package/dist/tools/ccRoutines.js.map +1 -0
- package/dist/tools/getCodeCoverage.js +7 -3
- package/dist/tools/getCodeCoverage.js.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/openInBrowser.js +6 -1
- package/dist/tools/openInBrowser.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +56 -11
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/dist/tools/testRunners/vitestJest.js +3 -1
- package/dist/tools/testRunners/vitestJest.js.map +1 -1
- package/dist/tools/utils.js +13 -7
- package/dist/tools/utils.js.map +1 -1
- package/package.json +16 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/smoke/run-all.mjs +162 -0
- package/scripts/start-all.mjs +513 -0
- package/scripts/start-all.ps1 +209 -0
- package/scripts/start-all.sh +73 -17
- package/scripts/start-orchestrator.ps1 +158 -0
- package/scripts/start-remote.mjs +122 -0
- package/templates/automation-policies/recipe-authoring.json +1 -1
- package/templates/automation-policies/security-first.json +1 -1
- package/templates/automation-policies/strict-lint.json +1 -1
- package/templates/automation-policies/test-driven.json +1 -1
- package/templates/automation-policy.example.json +1 -1
- package/templates/co.patchwork-os.bridge.plist +1 -1
- package/templates/recipes/approval-queue-ui-test.yaml +1 -1
- package/templates/recipes/ctx-loop-test.yaml +1 -1
- package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
- package/dist/commands/marketplace.d.ts +0 -16
- package/dist/commands/marketplace.js +0 -32
- package/dist/commands/marketplace.js.map +0 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
- package/dist/recipes/legacyRecipeCompat.js +0 -131
- package/dist/recipes/legacyRecipeCompat.js.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
5
|
import { dirname, join, resolve } from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -49,6 +51,59 @@ What it does:
|
|
|
49
51
|
5. Print next steps
|
|
50
52
|
`);
|
|
51
53
|
}
|
|
54
|
+
function findDashboardDir() {
|
|
55
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const candidates = [
|
|
57
|
+
join(here, "..", "..", "dashboard"),
|
|
58
|
+
join(here, "..", "..", "..", "dashboard"),
|
|
59
|
+
];
|
|
60
|
+
for (const c of candidates) {
|
|
61
|
+
if (existsSync(join(c, "package.json")))
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function ensureDashboardEnv(patchworkDir) {
|
|
67
|
+
const envPath = join(patchworkDir, ".env");
|
|
68
|
+
let existing = "";
|
|
69
|
+
if (existsSync(envPath))
|
|
70
|
+
existing = readFileSync(envPath, "utf8");
|
|
71
|
+
const hasPassword = /^DASHBOARD_PASSWORD=.+$/m.test(existing);
|
|
72
|
+
const hasSecret = /^DASHBOARD_SESSION_SECRET=.+$/m.test(existing);
|
|
73
|
+
if (hasPassword && hasSecret)
|
|
74
|
+
return { password: "[already set]", generated: false };
|
|
75
|
+
const password = randomBytes(8).toString("hex");
|
|
76
|
+
const secret = randomBytes(32).toString("hex");
|
|
77
|
+
const additions = [];
|
|
78
|
+
if (!hasPassword)
|
|
79
|
+
additions.push(`DASHBOARD_PASSWORD=${password}`);
|
|
80
|
+
if (!hasSecret)
|
|
81
|
+
additions.push(`DASHBOARD_SESSION_SECRET=${secret}`);
|
|
82
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
83
|
+
writeFileSync(envPath, `${existing + sep + additions.join("\n")}\n`, {
|
|
84
|
+
mode: 0o600,
|
|
85
|
+
});
|
|
86
|
+
// Also write dashboard/.env.local so Next.js picks up credentials without start-all sourcing
|
|
87
|
+
const dashDir = findDashboardDir();
|
|
88
|
+
if (dashDir) {
|
|
89
|
+
const localPath = join(dashDir, ".env.local");
|
|
90
|
+
let local = existsSync(localPath) ? readFileSync(localPath, "utf8") : "";
|
|
91
|
+
if (/^DASHBOARD_PASSWORD=/m.test(local)) {
|
|
92
|
+
local = local.replace(/^DASHBOARD_PASSWORD=.*$/m, `DASHBOARD_PASSWORD=${password}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
local += `${local.endsWith("\n") ? "" : "\n"}DASHBOARD_PASSWORD=${password}\n`;
|
|
96
|
+
}
|
|
97
|
+
if (/^DASHBOARD_SESSION_SECRET=/m.test(local)) {
|
|
98
|
+
local = local.replace(/^DASHBOARD_SESSION_SECRET=.*$/m, `DASHBOARD_SESSION_SECRET=${secret}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
local += `DASHBOARD_SESSION_SECRET=${secret}\n`;
|
|
102
|
+
}
|
|
103
|
+
writeFileSync(localPath, local, { mode: 0o600 });
|
|
104
|
+
}
|
|
105
|
+
return { password, generated: true };
|
|
106
|
+
}
|
|
52
107
|
function findTemplatesDir() {
|
|
53
108
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
54
109
|
// dist/commands/patchworkInit.js → ../../templates/recipes
|
|
@@ -62,6 +117,18 @@ function findTemplatesDir() {
|
|
|
62
117
|
return c;
|
|
63
118
|
return null;
|
|
64
119
|
}
|
|
120
|
+
function detectGeminiCli() {
|
|
121
|
+
try {
|
|
122
|
+
const r = spawnSync("gemini", ["--version"], {
|
|
123
|
+
stdio: "pipe",
|
|
124
|
+
timeout: 3000,
|
|
125
|
+
});
|
|
126
|
+
return r.status === 0;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
65
132
|
async function detectOllama(timeoutMs = 500) {
|
|
66
133
|
try {
|
|
67
134
|
const controller = new AbortController();
|
|
@@ -94,6 +161,9 @@ export async function runPatchworkInit(argv, opts = {}) {
|
|
|
94
161
|
mkdirSync(dir, { recursive: true });
|
|
95
162
|
}
|
|
96
163
|
log(` ✓ ~/.patchwork scaffolded\n`);
|
|
164
|
+
const dashEnv = ensureDashboardEnv(patchworkDir);
|
|
165
|
+
if (dashEnv.generated)
|
|
166
|
+
log(` ✓ ~/.patchwork/.env created with dashboard credentials\n`);
|
|
97
167
|
const templatesDir = findTemplatesDir();
|
|
98
168
|
let recipesCopied = 0;
|
|
99
169
|
let recipesSkipped = 0;
|
|
@@ -124,6 +194,10 @@ export async function runPatchworkInit(argv, opts = {}) {
|
|
|
124
194
|
else {
|
|
125
195
|
log(` ! recipe templates not found (expected templates/recipes/)\n`);
|
|
126
196
|
}
|
|
197
|
+
const geminiCliDetected = detectGeminiCli();
|
|
198
|
+
log(geminiCliDetected
|
|
199
|
+
? ` ✓ Gemini CLI detected — driver will default to gemini\n`
|
|
200
|
+
: ` · Gemini CLI not detected (install from https://github.com/google-gemini/gemini-cli)\n`);
|
|
127
201
|
let ollamaDetected = false;
|
|
128
202
|
if (!parsed.skipOllama) {
|
|
129
203
|
ollamaDetected = await detectOllama();
|
|
@@ -157,6 +231,7 @@ export async function runPatchworkInit(argv, opts = {}) {
|
|
|
157
231
|
const fresh = {
|
|
158
232
|
model: ollamaDetected ? "local" : "claude",
|
|
159
233
|
recipesDir,
|
|
234
|
+
...(geminiCliDetected ? { driver: "gemini" } : {}),
|
|
160
235
|
dashboard: {
|
|
161
236
|
port: 3200,
|
|
162
237
|
requireApproval: ["high"],
|
|
@@ -202,18 +277,22 @@ export async function runPatchworkInit(argv, opts = {}) {
|
|
|
202
277
|
const restartLine = preToolUseHook === "added"
|
|
203
278
|
? `\n ⚠ Restart Claude Code so the new PreToolUse hook takes effect.\n (CC reads hooks at session start — existing sessions won't see the change.)\n`
|
|
204
279
|
: "";
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
280
|
+
const dashPasswordLine = dashEnv.generated
|
|
281
|
+
? `\n Dashboard password: ${dashEnv.password} (saved to ~/.patchwork/.env)\n`
|
|
282
|
+
: "";
|
|
283
|
+
log(`${restartLine}${dashPasswordLine}
|
|
284
|
+
Next:
|
|
285
|
+
1. patchwork start # launch bridge + dashboard
|
|
286
|
+
2. open http://localhost:3200 # dashboard (use password printed above)
|
|
287
|
+
3. patchwork-os recipe run daily-status # zero-config: yesterday's commits + today's hints
|
|
288
|
+
4. patchwork-os init --with-connectors # add gmail/github/linear/etc. recipes\n`);
|
|
211
289
|
return {
|
|
212
290
|
configPath,
|
|
213
291
|
recipesDir,
|
|
214
292
|
recipesCopied,
|
|
215
293
|
recipesSkipped,
|
|
216
294
|
ollamaDetected,
|
|
295
|
+
geminiCliDetected,
|
|
217
296
|
configAction,
|
|
218
297
|
preToolUseHook,
|
|
219
298
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patchworkInit.js","sourceRoot":"","sources":["../../src/commands/patchworkInit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"patchworkInit.js","sourceRoot":"","sources":["../../src/commands/patchworkInit.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,43 @@ export declare function runNew(options: NewOptions): {
|
|
|
17
17
|
content: string;
|
|
18
18
|
};
|
|
19
19
|
export declare function listTemplates(): string[];
|
|
20
|
+
export interface InteractivePromptDeps {
|
|
21
|
+
/** Free-text prompt. Returns the user's trimmed answer. */
|
|
22
|
+
ask: (question: string) => Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* List-select prompt. Returns the 1-based index of the choice the
|
|
25
|
+
* user picked. Implementations must reject 0 / out-of-range / NaN.
|
|
26
|
+
*/
|
|
27
|
+
pickFromList: (question: string, options: string[]) => Promise<number>;
|
|
28
|
+
/** Y/N prompt. Returns true on yes. */
|
|
29
|
+
confirm: (question: string) => Promise<boolean>;
|
|
30
|
+
/** Optional preview hook — called once before the final write confirm. */
|
|
31
|
+
preview?: (yaml: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface InteractiveNewOptions {
|
|
34
|
+
outputDir?: string;
|
|
35
|
+
deps: InteractivePromptDeps;
|
|
36
|
+
/**
|
|
37
|
+
* AI-suggest path. Injected so tests can stub the bridge-discovery +
|
|
38
|
+
* fetch pair without touching the network or the lock-file dir. When
|
|
39
|
+
* omitted, runNewInteractive uses the production defaults (findBridgeLock
|
|
40
|
+
* + global fetch). The CLI dispatch passes nothing.
|
|
41
|
+
*/
|
|
42
|
+
aiSuggest?: {
|
|
43
|
+
findBridge?: () => {
|
|
44
|
+
port: number;
|
|
45
|
+
authToken: string;
|
|
46
|
+
} | null;
|
|
47
|
+
fetch?: typeof globalThis.fetch;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface InteractiveNewResult {
|
|
51
|
+
path: string;
|
|
52
|
+
content: string;
|
|
53
|
+
/** Lint issues surfaced by validateRecipeDefinition (warnings only — write proceeds). */
|
|
54
|
+
warnings: LintIssue[];
|
|
55
|
+
}
|
|
56
|
+
export declare function runNewInteractive(options: InteractiveNewOptions): Promise<InteractiveNewResult>;
|
|
20
57
|
export interface SchemaWriteResult {
|
|
21
58
|
outputDir: string;
|
|
22
59
|
filesWritten: string[];
|
|
@@ -61,6 +98,20 @@ export interface RunRecipeOptions {
|
|
|
61
98
|
vars?: Record<string, string>;
|
|
62
99
|
workdir?: string;
|
|
63
100
|
deps?: Partial<RunnerDeps>;
|
|
101
|
+
/**
|
|
102
|
+
* PR5c — stable id for one *logical* execution attempt. Forwarded to
|
|
103
|
+
* RunnerDeps so the runner constructs a disk-backed WriteEffectLedger
|
|
104
|
+
* scoped by `${recipe.name}:${manualRunId}`. Re-using the same id on
|
|
105
|
+
* a retry replays prior dedup records and skips already-completed
|
|
106
|
+
* write tools (resume semantics).
|
|
107
|
+
*/
|
|
108
|
+
manualRunId?: string;
|
|
109
|
+
/**
|
|
110
|
+
* PR5c — directory holding `effect_ledger.jsonl`. Required for the
|
|
111
|
+
* disk-backed ledger; without it the ledger stays in-memory even
|
|
112
|
+
* when manualRunId is set.
|
|
113
|
+
*/
|
|
114
|
+
ledgerDir?: string;
|
|
64
115
|
}
|
|
65
116
|
export interface RunRecipeStepSelection {
|
|
66
117
|
query: string;
|
package/dist/commands/recipe.js
CHANGED
|
@@ -11,10 +11,10 @@ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
|
11
11
|
import "../recipes/tools/index.js";
|
|
12
12
|
import { loadFixtureLibrary } from "../connectors/fixtureLibrary.js";
|
|
13
13
|
import { MockConnector } from "../connectors/mockConnector.js";
|
|
14
|
-
import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "../recipes/
|
|
14
|
+
import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "../recipes/migrations/index.js";
|
|
15
15
|
import { tryResolveRecipePath } from "../recipes/resolveRecipePath.js";
|
|
16
16
|
import { generateSchemaSet, writeSchemas } from "../recipes/schemaGenerator.js";
|
|
17
|
-
import { getTool, isConnectorNamespace, seedToolOutputPreviewContext, } from "../recipes/toolRegistry.js";
|
|
17
|
+
import { getTool, isConnectorNamespace, listConnectorNamespaces, listTools, seedToolOutputPreviewContext, } from "../recipes/toolRegistry.js";
|
|
18
18
|
import { validateRecipeDefinition, } from "../recipes/validation.js";
|
|
19
19
|
import { buildChainedDeps, dispatchRecipe, loadYamlRecipe, render, runYamlRecipe, } from "../recipes/yamlRunner.js";
|
|
20
20
|
import { findYamlRecipePath } from "../recipesHttp.js";
|
|
@@ -113,6 +113,355 @@ export function runNew(options) {
|
|
|
113
113
|
export function listTemplates() {
|
|
114
114
|
return Object.keys(TEMPLATES);
|
|
115
115
|
}
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// patchwork recipe new --interactive
|
|
118
|
+
//
|
|
119
|
+
// Connector-aware prompt tree. Writes a valid recipe YAML based on
|
|
120
|
+
// user choices. Use a `node:readline/promises` adapter in real CLI;
|
|
121
|
+
// tests inject a stub `InteractivePromptDeps`.
|
|
122
|
+
// ============================================================================
|
|
123
|
+
const RECIPE_NAME_RE = /^[a-z0-9][a-z0-9_-]{1,63}$/;
|
|
124
|
+
const CRON_SHAPE_RE = /^(@every\s+\d+\s*(ms|s|m|h)|\S+\s+\S+\s+\S+\s+\S+\s+\S+)$/i;
|
|
125
|
+
async function askWithValidation(deps, question, validate) {
|
|
126
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
127
|
+
const answer = await deps.ask(question);
|
|
128
|
+
const error = validate(answer);
|
|
129
|
+
if (!error)
|
|
130
|
+
return answer;
|
|
131
|
+
// Surface error and re-ask via the question prompt itself.
|
|
132
|
+
question = `${error} ${question}`;
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Too many invalid answers");
|
|
135
|
+
}
|
|
136
|
+
function yamlScalar(value) {
|
|
137
|
+
// Quote unless the string is a simple identifier-ish token. Conservative
|
|
138
|
+
// — when in doubt, JSON-encode (always parses as a YAML string).
|
|
139
|
+
if (/^[A-Za-z0-9_.\-/:]+$/.test(value))
|
|
140
|
+
return value;
|
|
141
|
+
return JSON.stringify(value);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Prompt the user for the tool's required JSON Schema fields. Optional
|
|
145
|
+
* properties are skipped — the user can hand-edit the YAML afterwards
|
|
146
|
+
* if they want to set defaults. `into` is excluded since the generator
|
|
147
|
+
* supplies its own name.
|
|
148
|
+
*
|
|
149
|
+
* Returns an ordered list of `[key, value]` pairs so the YAML output is
|
|
150
|
+
* stable across runs (insertion order of object keys is sufficient in
|
|
151
|
+
* V8 for string keys, but an array makes the contract explicit).
|
|
152
|
+
*/
|
|
153
|
+
async function collectRequiredParams(deps, tool) {
|
|
154
|
+
const schema = tool.paramsSchema;
|
|
155
|
+
if (!schema || typeof schema !== "object")
|
|
156
|
+
return [];
|
|
157
|
+
const required = Array.isArray(schema.required)
|
|
158
|
+
? schema.required.filter((k) => typeof k === "string" && k !== "into")
|
|
159
|
+
: [];
|
|
160
|
+
if (required.length === 0)
|
|
161
|
+
return [];
|
|
162
|
+
const out = [];
|
|
163
|
+
const properties = schema.properties ?? {};
|
|
164
|
+
for (const key of required) {
|
|
165
|
+
const propMeta = properties[key];
|
|
166
|
+
const hint = propMeta?.description ? ` — ${propMeta.description}` : "";
|
|
167
|
+
const value = await askWithValidation(deps, `${tool.id} → ${key}${hint}`, (a) => (a.trim().length > 0 ? null : "required, cannot be empty."));
|
|
168
|
+
out.push([key, value.trim()]);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* AI-suggest path. Discovers the running bridge via lock file, POSTs
|
|
174
|
+
* the user's natural-language goal to `/recipes/generate`, writes the
|
|
175
|
+
* bridge's raw YAML response to disk (no form normalization — per
|
|
176
|
+
* memory project_recipe_audit_2026_05_06_part2), and validates
|
|
177
|
+
* post-hoc as warnings only.
|
|
178
|
+
*
|
|
179
|
+
* Failure modes:
|
|
180
|
+
* - No bridge running → clear actionable error
|
|
181
|
+
* - Bridge returns 503 → "AI generation unavailable — start the
|
|
182
|
+
* bridge with --driver subprocess"
|
|
183
|
+
* - Bridge returns 4xx → surface server error message
|
|
184
|
+
* - Bridge returns 200 but `result.yaml` is empty → write nothing,
|
|
185
|
+
* throw with the raw refusal text
|
|
186
|
+
*/
|
|
187
|
+
async function runNewAiSuggest(options) {
|
|
188
|
+
const { deps } = options;
|
|
189
|
+
const name = await askWithValidation(deps, "Recipe name", (a) => RECIPE_NAME_RE.test(a)
|
|
190
|
+
? null
|
|
191
|
+
: 'must match /^[a-z0-9][a-z0-9_-]{1,63}$/ (e.g. "morning-brief").');
|
|
192
|
+
const goal = await askWithValidation(deps, "Describe what the recipe should do (one-paragraph natural language)", (a) => (a.trim().length > 0 ? null : "goal cannot be empty."));
|
|
193
|
+
// Resolve bridge discovery + fetch — production defaults or test injection.
|
|
194
|
+
const findBridge = options.aiSuggest?.findBridge ??
|
|
195
|
+
(() => {
|
|
196
|
+
// Lazy import so non-AI paths don't pay the cost.
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
198
|
+
const mod = require("../bridgeLockDiscovery.js");
|
|
199
|
+
return mod.findBridgeLock();
|
|
200
|
+
});
|
|
201
|
+
const doFetch = options.aiSuggest?.fetch ?? globalThis.fetch;
|
|
202
|
+
const lock = findBridge();
|
|
203
|
+
if (!lock) {
|
|
204
|
+
throw new Error("AI generation requires a running bridge. Start one with `patchwork start` " +
|
|
205
|
+
"(or `claude-ide-bridge --driver subprocess` for the bridge-only mode), " +
|
|
206
|
+
"then retry.");
|
|
207
|
+
}
|
|
208
|
+
let response;
|
|
209
|
+
try {
|
|
210
|
+
response = await doFetch(`http://127.0.0.1:${lock.port}/recipes/generate`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
Authorization: `Bearer ${lock.authToken}`,
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify({ prompt: goal.trim() }),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
throw new Error(`Could not reach the bridge at port ${lock.port}: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
+
}
|
|
222
|
+
let payload;
|
|
223
|
+
try {
|
|
224
|
+
payload = (await response.json());
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new Error(`Bridge returned non-JSON response (status ${response.status})`);
|
|
228
|
+
}
|
|
229
|
+
if (response.status === 503 || payload.unavailable) {
|
|
230
|
+
throw new Error("AI generation unavailable — start the bridge with `--driver subprocess`.");
|
|
231
|
+
}
|
|
232
|
+
if (!payload.ok) {
|
|
233
|
+
throw new Error(payload.error ?? `Bridge returned ${response.status}`);
|
|
234
|
+
}
|
|
235
|
+
const yamlBody = (payload.yaml ?? "").trim();
|
|
236
|
+
if (!yamlBody) {
|
|
237
|
+
throw new Error("Bridge returned empty YAML — try a more specific goal.");
|
|
238
|
+
}
|
|
239
|
+
// Ensure the SchemaStore pragma is present at line 1 so the file
|
|
240
|
+
// gets autocomplete on first open. If the bridge already prepended
|
|
241
|
+
// one, leave it.
|
|
242
|
+
const hasPragma = /^#\s*yaml-language-server:/.test(yamlBody);
|
|
243
|
+
const content = hasPragma
|
|
244
|
+
? `${yamlBody}\n`
|
|
245
|
+
: `${RECIPE_SCHEMA_HEADER}\n${yamlBody}\n`;
|
|
246
|
+
if (deps.preview)
|
|
247
|
+
deps.preview(content);
|
|
248
|
+
const shouldWrite = await deps.confirm("Write to disk?");
|
|
249
|
+
if (!shouldWrite) {
|
|
250
|
+
throw new Error("Cancelled by user");
|
|
251
|
+
}
|
|
252
|
+
const outputDir = options.outputDir ?? RECIPES_DIR;
|
|
253
|
+
if (!existsSync(outputDir)) {
|
|
254
|
+
mkdirSync(outputDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
const outputPath = join(outputDir, `${name}.yaml`);
|
|
257
|
+
if (existsSync(outputPath)) {
|
|
258
|
+
throw new Error(`Recipe already exists: ${outputPath}`);
|
|
259
|
+
}
|
|
260
|
+
writeFileSync(outputPath, content);
|
|
261
|
+
// Lint post-hoc. Surface as warnings; never block — the user asked
|
|
262
|
+
// for a draft, the bridge produced it, the file is on disk.
|
|
263
|
+
let warnings = [];
|
|
264
|
+
try {
|
|
265
|
+
const parsed = parseYaml(content);
|
|
266
|
+
const lintResult = validateRecipeDefinition(parsed);
|
|
267
|
+
warnings = lintResult.issues ?? [];
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
warnings = [
|
|
271
|
+
{
|
|
272
|
+
level: "warning",
|
|
273
|
+
message: `AI-generated YAML did not parse cleanly: ${err instanceof Error ? err.message : String(err)}`,
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
return { path: outputPath, content, warnings };
|
|
278
|
+
}
|
|
279
|
+
export async function runNewInteractive(options) {
|
|
280
|
+
const { deps } = options;
|
|
281
|
+
// Mode pick — Guided (full prompt tree), Template (existing runNew
|
|
282
|
+
// templates: minimal | daily | inbox), or AI-suggest (asks the running
|
|
283
|
+
// bridge's /recipes/generate endpoint and writes the raw response).
|
|
284
|
+
//
|
|
285
|
+
// AI-suggest skips the form normalizer per memory
|
|
286
|
+
// project_recipe_audit_2026_05_06_part2 — applyAiYaml on the dashboard
|
|
287
|
+
// was lossy. Here we writeFileSync the bridge's `result.yaml` verbatim
|
|
288
|
+
// (only the SchemaStore pragma is prepended if absent) and validate
|
|
289
|
+
// post-hoc as warnings, never blocking the write.
|
|
290
|
+
const templates = listTemplates();
|
|
291
|
+
const modeChoices = [
|
|
292
|
+
"Guided — full prompt tree (recommended)",
|
|
293
|
+
`Template — pick from ${templates.length} starters (${templates.join(", ")})`,
|
|
294
|
+
"AI suggest — describe what you want; the bridge drafts a YAML",
|
|
295
|
+
];
|
|
296
|
+
const modeIdx = await deps.pickFromList("How do you want to start?", modeChoices);
|
|
297
|
+
// AI-suggest mode: route through the running bridge's /recipes/generate.
|
|
298
|
+
if (modeIdx === 3) {
|
|
299
|
+
return runNewAiSuggest(options);
|
|
300
|
+
}
|
|
301
|
+
// Template mode: re-use the existing runNew() flow with the picked template.
|
|
302
|
+
if (modeIdx === 2) {
|
|
303
|
+
const name = await askWithValidation(deps, "Recipe name", (a) => RECIPE_NAME_RE.test(a)
|
|
304
|
+
? null
|
|
305
|
+
: 'must match /^[a-z0-9][a-z0-9_-]{1,63}$/ (e.g. "morning-brief").');
|
|
306
|
+
const description = await askWithValidation(deps, "One-line description", (a) => (a.trim().length > 0 ? null : "cannot be empty."));
|
|
307
|
+
const templateIdx = await deps.pickFromList("Pick a template", templates);
|
|
308
|
+
const template = templates[templateIdx - 1];
|
|
309
|
+
if (!template)
|
|
310
|
+
throw new Error("Invalid template selection");
|
|
311
|
+
const result = runNew({
|
|
312
|
+
name,
|
|
313
|
+
description,
|
|
314
|
+
template,
|
|
315
|
+
...(options.outputDir ? { outputDir: options.outputDir } : {}),
|
|
316
|
+
});
|
|
317
|
+
if (deps.preview)
|
|
318
|
+
deps.preview(result.content);
|
|
319
|
+
const shouldWrite = await deps.confirm("Keep this recipe?");
|
|
320
|
+
if (!shouldWrite) {
|
|
321
|
+
// runNew already wrote the file — clean up to honor "cancel."
|
|
322
|
+
try {
|
|
323
|
+
if (existsSync(result.path)) {
|
|
324
|
+
const { unlinkSync } = await import("node:fs");
|
|
325
|
+
unlinkSync(result.path);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// best effort
|
|
330
|
+
}
|
|
331
|
+
throw new Error("Cancelled by user");
|
|
332
|
+
}
|
|
333
|
+
return { path: result.path, content: result.content, warnings: [] };
|
|
334
|
+
}
|
|
335
|
+
// Guided mode (default — modeIdx === 1).
|
|
336
|
+
const name = await askWithValidation(deps, "Recipe name", (a) => RECIPE_NAME_RE.test(a)
|
|
337
|
+
? null
|
|
338
|
+
: 'must match /^[a-z0-9][a-z0-9_-]{1,63}$/ (e.g. "morning-brief").');
|
|
339
|
+
const description = await askWithValidation(deps, "One-line description", (a) => (a.trim().length > 0 ? null : "cannot be empty."));
|
|
340
|
+
const triggerTypes = ["manual", "cron"];
|
|
341
|
+
const triggerIdx = await deps.pickFromList("Trigger type", triggerTypes.slice());
|
|
342
|
+
const triggerType = triggerTypes[triggerIdx - 1];
|
|
343
|
+
if (!triggerType)
|
|
344
|
+
throw new Error("Invalid trigger selection");
|
|
345
|
+
const triggerLines = [`trigger:`, ` type: ${triggerType}`];
|
|
346
|
+
if (triggerType === "cron") {
|
|
347
|
+
const cron = await askWithValidation(deps, "Cron expression (e.g. '0 8 * * *' for 8am daily)", (a) => CRON_SHAPE_RE.test(a.trim())
|
|
348
|
+
? null
|
|
349
|
+
: 'expected 5-field cron or "@every Ns|Nm|Nh".');
|
|
350
|
+
triggerLines.push(` at: ${yamlScalar(cron.trim())}`);
|
|
351
|
+
}
|
|
352
|
+
// Step loop. The user adds tool / agent steps until they pick "done".
|
|
353
|
+
// Each tool pick reads its paramsSchema and prompts for required fields,
|
|
354
|
+
// so the generated recipe is closer to runnable than the MVP slice.
|
|
355
|
+
const namespaces = listConnectorNamespaces();
|
|
356
|
+
const stepLines = ["steps:"];
|
|
357
|
+
const intoNames = [];
|
|
358
|
+
let lastAgentInto = null;
|
|
359
|
+
let toolStepCount = 0;
|
|
360
|
+
let agentStepCount = 0;
|
|
361
|
+
const stepKindChoices = [
|
|
362
|
+
"Add a tool step (calls a registered tool — gmail.fetch_unread, github.list_issues, …)",
|
|
363
|
+
"Add an agent step (LLM prompt — drafts, classifies, summarizes)",
|
|
364
|
+
"Done — preview and write",
|
|
365
|
+
];
|
|
366
|
+
for (let stepIdx = 0; stepIdx < 20; stepIdx++) {
|
|
367
|
+
const kind = await deps.pickFromList(stepIdx === 0
|
|
368
|
+
? "Build steps — what's next?"
|
|
369
|
+
: "Add another step? (or pick Done)", stepKindChoices);
|
|
370
|
+
if (kind === 3)
|
|
371
|
+
break;
|
|
372
|
+
if (kind === 1) {
|
|
373
|
+
// Tool step
|
|
374
|
+
const nsIdx = await deps.pickFromList("Pick a connector", namespaces.slice());
|
|
375
|
+
const ns = namespaces[nsIdx - 1];
|
|
376
|
+
if (!ns)
|
|
377
|
+
throw new Error("Invalid connector selection");
|
|
378
|
+
const tools = listTools(ns);
|
|
379
|
+
const labels = tools.map((t) => `${t.id} — ${t.description}`);
|
|
380
|
+
const toolIdx = await deps.pickFromList(`Pick a ${ns} tool`, labels);
|
|
381
|
+
const tool = tools[toolIdx - 1];
|
|
382
|
+
if (!tool)
|
|
383
|
+
throw new Error(`Invalid tool selection for ${ns}`);
|
|
384
|
+
toolStepCount += 1;
|
|
385
|
+
const intoName = toolStepCount === 1 ? `${ns}_result` : `${ns}_result_${toolStepCount}`;
|
|
386
|
+
intoNames.push(intoName);
|
|
387
|
+
stepLines.push(` - tool: ${tool.id}`);
|
|
388
|
+
// Prompt for required params from the tool's JSON schema.
|
|
389
|
+
const params = await collectRequiredParams(deps, tool);
|
|
390
|
+
for (const [key, value] of params) {
|
|
391
|
+
stepLines.push(` ${key}: ${yamlScalar(value)}`);
|
|
392
|
+
}
|
|
393
|
+
stepLines.push(` into: ${intoName}`);
|
|
394
|
+
}
|
|
395
|
+
else if (kind === 2) {
|
|
396
|
+
// Agent step
|
|
397
|
+
const prompt = await askWithValidation(deps, "Agent prompt", (a) => a.trim().length > 0 ? null : "cannot be empty.");
|
|
398
|
+
agentStepCount += 1;
|
|
399
|
+
const intoName = agentStepCount === 1
|
|
400
|
+
? "agent_output"
|
|
401
|
+
: `agent_output_${agentStepCount}`;
|
|
402
|
+
lastAgentInto = intoName;
|
|
403
|
+
stepLines.push(` - agent: true`);
|
|
404
|
+
stepLines.push(` prompt: |`);
|
|
405
|
+
for (const line of prompt.split("\n")) {
|
|
406
|
+
stepLines.push(` ${line}`);
|
|
407
|
+
}
|
|
408
|
+
stepLines.push(` into: ${intoName}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Always tail with file.write to inbox so the user sees output somewhere.
|
|
412
|
+
// Reference the most recent agent output when one exists, otherwise fall
|
|
413
|
+
// back to the most recent tool result (or a TODO comment if neither).
|
|
414
|
+
const tailRef = lastAgentInto ?? intoNames[intoNames.length - 1] ?? null;
|
|
415
|
+
stepLines.push(` - tool: file.write`);
|
|
416
|
+
stepLines.push(` path: "~/.patchwork/inbox/${name}-{{date}}.md"`);
|
|
417
|
+
stepLines.push(` content: |`);
|
|
418
|
+
stepLines.push(` # ${name} — {{date}}`);
|
|
419
|
+
stepLines.push(``);
|
|
420
|
+
if (tailRef) {
|
|
421
|
+
stepLines.push(` {{${tailRef}}}`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
stepLines.push(` (no steps configured — fill in)`);
|
|
425
|
+
}
|
|
426
|
+
const content = `${RECIPE_SCHEMA_HEADER}\n` +
|
|
427
|
+
`version: 1.0.0\n` +
|
|
428
|
+
`name: ${name}\n` +
|
|
429
|
+
`description: ${yamlScalar(description)}\n` +
|
|
430
|
+
`${triggerLines.join("\n")}\n` +
|
|
431
|
+
`${stepLines.join("\n")}\n`;
|
|
432
|
+
// Lint pre-write. validateRecipeDefinition expects parsed YAML.
|
|
433
|
+
let warnings = [];
|
|
434
|
+
try {
|
|
435
|
+
const parsed = parseYaml(content);
|
|
436
|
+
const lintResult = validateRecipeDefinition(parsed);
|
|
437
|
+
warnings = lintResult.issues ?? [];
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Parse failure here is a bug in the generator — surface but don't block.
|
|
441
|
+
warnings = [
|
|
442
|
+
{
|
|
443
|
+
level: "error",
|
|
444
|
+
message: "Generated YAML failed to parse — please report this.",
|
|
445
|
+
},
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
if (deps.preview)
|
|
449
|
+
deps.preview(content);
|
|
450
|
+
const shouldWrite = await deps.confirm("Write to disk?");
|
|
451
|
+
if (!shouldWrite) {
|
|
452
|
+
throw new Error("Cancelled by user");
|
|
453
|
+
}
|
|
454
|
+
const outputDir = options.outputDir ?? RECIPES_DIR;
|
|
455
|
+
if (!existsSync(outputDir)) {
|
|
456
|
+
mkdirSync(outputDir, { recursive: true });
|
|
457
|
+
}
|
|
458
|
+
const outputPath = join(outputDir, `${name}.yaml`);
|
|
459
|
+
if (existsSync(outputPath)) {
|
|
460
|
+
throw new Error(`Recipe already exists: ${outputPath}`);
|
|
461
|
+
}
|
|
462
|
+
writeFileSync(outputPath, content);
|
|
463
|
+
return { path: outputPath, content, warnings };
|
|
464
|
+
}
|
|
116
465
|
export async function runSchema(outputDir) {
|
|
117
466
|
const resolvedOutputDir = resolve(outputDir);
|
|
118
467
|
const schemas = generateSchemaSet();
|
|
@@ -420,6 +769,8 @@ export async function runRecipe(recipeRef, options = {}) {
|
|
|
420
769
|
const runnerDeps = {
|
|
421
770
|
...options.deps,
|
|
422
771
|
workdir: options.workdir ?? options.deps?.workdir ?? process.cwd(),
|
|
772
|
+
...(options.manualRunId && { manualRunId: options.manualRunId }),
|
|
773
|
+
...(options.ledgerDir && { ledgerDir: options.ledgerDir }),
|
|
423
774
|
};
|
|
424
775
|
if (options.dryRun) {
|
|
425
776
|
throw new Error("runRecipeDryPlan must be used for dry-run execution");
|
|
@@ -832,7 +1183,16 @@ export async function runPreflight(recipeRef, options = {}) {
|
|
|
832
1183
|
}
|
|
833
1184
|
const plan = await runRecipeDryPlan(recipeRef, options);
|
|
834
1185
|
const requireWriteAck = options.requireWriteAck ?? true;
|
|
835
|
-
|
|
1186
|
+
// Merge the recipe's declared allowWrites (YAML) with any caller-supplied
|
|
1187
|
+
// entries (e.g. --allow-write CLI flag). Either source is sufficient.
|
|
1188
|
+
const recipeForWrites = loadYamlRecipe(recipePath);
|
|
1189
|
+
const recipeAllowWrites = Array.isArray(recipeForWrites.allowWrites)
|
|
1190
|
+
? recipeForWrites.allowWrites.filter((entry) => typeof entry === "string")
|
|
1191
|
+
: [];
|
|
1192
|
+
const allowlist = new Set([
|
|
1193
|
+
...recipeAllowWrites,
|
|
1194
|
+
...(options.allowWrites ?? []),
|
|
1195
|
+
]);
|
|
836
1196
|
for (const step of plan.steps) {
|
|
837
1197
|
if (step.type === "tool" && step.tool && step.resolved === false) {
|
|
838
1198
|
issues.push({
|