patchwork-os 0.2.0-beta.1 → 0.2.0-beta.10.canary.95
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 +14 -14
- package/README.md +201 -34
- package/deploy/README.md +1 -1
- package/deploy/bootstrap-vps.sh +14 -9
- 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 +63 -26
- package/dist/activityLog.js.map +1 -1
- package/dist/adapters/claude.js +22 -16
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.js +15 -11
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.js +9 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/ajv2020.d.ts +25 -0
- package/dist/ajv2020.js +33 -0
- package/dist/ajv2020.js.map +1 -0
- package/dist/analyticsAggregator.js +1 -1
- package/dist/analyticsAggregator.js.map +1 -1
- package/dist/analyticsConfig.d.ts +29 -0
- package/dist/analyticsConfig.js +102 -0
- package/dist/analyticsConfig.js.map +1 -0
- package/dist/analyticsPrefs.d.ts +38 -2
- package/dist/analyticsPrefs.js +148 -23
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/analyticsSend.d.ts +17 -1
- package/dist/analyticsSend.js +67 -5
- package/dist/analyticsSend.js.map +1 -1
- package/dist/approvalHttp.d.ts +14 -0
- package/dist/approvalHttp.js +212 -9
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalInsights.js +13 -5
- package/dist/approvalInsights.js.map +1 -1
- package/dist/approvalQueue.d.ts +97 -3
- package/dist/approvalQueue.js +193 -19
- package/dist/approvalQueue.js.map +1 -1
- package/dist/approvalSignals.js +19 -11
- package/dist/approvalSignals.js.map +1 -1
- package/dist/automation.d.ts +35 -10
- package/dist/automation.js +133 -37
- package/dist/automation.js.map +1 -1
- package/dist/automationSuggestions.js +10 -8
- package/dist/automationSuggestions.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +202 -25
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeLockDiscovery.d.ts +27 -1
- package/dist/bridgeLockDiscovery.js +38 -11
- package/dist/bridgeLockDiscovery.js.map +1 -1
- package/dist/bridgeToken.js +3 -2
- package/dist/bridgeToken.js.map +1 -1
- package/dist/claudeDriver.js +23 -8
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +87 -44
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/analytics.d.ts +8 -0
- package/dist/commands/analytics.js +134 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/auditEnv.d.ts +36 -0
- package/dist/commands/auditEnv.js +202 -0
- package/dist/commands/auditEnv.js.map +1 -0
- package/dist/commands/dashboard.js +8 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +35 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/install.js +3 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/launchd.js +15 -16
- package/dist/commands/launchd.js.map +1 -1
- package/dist/commands/patchworkInit.d.ts +5 -0
- package/dist/commands/patchworkInit.js +89 -7
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +110 -0
- package/dist/commands/recipe.js +512 -9
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +11 -4
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/commands/shadowScan.d.ts +34 -0
- package/dist/commands/shadowScan.js +142 -0
- package/dist/commands/shadowScan.js.map +1 -0
- package/dist/commands/task.js +2 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/tools.d.ts +20 -1
- package/dist/commands/tools.js +112 -3
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/tracesImport.js +21 -4
- package/dist/commands/tracesImport.js.map +1 -1
- package/dist/commitIssueLinkLog.d.ts +16 -0
- package/dist/commitIssueLinkLog.js +110 -24
- package/dist/commitIssueLinkLog.js.map +1 -1
- package/dist/companions/registry.js +15 -7
- package/dist/companions/registry.js.map +1 -1
- package/dist/config.d.ts +47 -3
- package/dist/config.js +111 -21
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.js +866 -57
- package/dist/connectorRoutes.js.map +1 -1
- package/dist/connectors/airtable.d.ts +230 -0
- package/dist/connectors/airtable.js +700 -0
- package/dist/connectors/airtable.js.map +1 -0
- package/dist/connectors/asana.js +13 -9
- package/dist/connectors/asana.js.map +1 -1
- package/dist/connectors/baseConnector.js +25 -3
- package/dist/connectors/baseConnector.js.map +1 -1
- package/dist/connectors/caldiy.d.ts +137 -0
- package/dist/connectors/caldiy.js +424 -0
- package/dist/connectors/caldiy.js.map +1 -0
- package/dist/connectors/circleci.d.ts +162 -0
- package/dist/connectors/circleci.js +576 -0
- package/dist/connectors/circleci.js.map +1 -0
- package/dist/connectors/cloudflare.d.ts +132 -0
- package/dist/connectors/cloudflare.js +622 -0
- package/dist/connectors/cloudflare.js.map +1 -0
- package/dist/connectors/confluence.js +35 -0
- package/dist/connectors/confluence.js.map +1 -1
- package/dist/connectors/connectorRedirectUri.d.ts +30 -0
- package/dist/connectors/connectorRedirectUri.js +38 -0
- package/dist/connectors/connectorRedirectUri.js.map +1 -0
- package/dist/connectors/connectorRegistry.d.ts +63 -0
- package/dist/connectors/connectorRegistry.js +354 -0
- package/dist/connectors/connectorRegistry.js.map +1 -0
- package/dist/connectors/datadog.js +33 -4
- package/dist/connectors/datadog.js.map +1 -1
- package/dist/connectors/discord.js +14 -10
- package/dist/connectors/discord.js.map +1 -1
- package/dist/connectors/elasticsearch.d.ts +141 -0
- package/dist/connectors/elasticsearch.js +601 -0
- package/dist/connectors/elasticsearch.js.map +1 -0
- package/dist/connectors/figma.d.ts +130 -0
- package/dist/connectors/figma.js +387 -0
- package/dist/connectors/figma.js.map +1 -0
- package/dist/connectors/github.d.ts +17 -0
- package/dist/connectors/github.js +53 -2
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gitlab.js +12 -6
- package/dist/connectors/gitlab.js.map +1 -1
- package/dist/connectors/gmail.js +72 -21
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.js +8 -8
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/googleDocs.d.ts +103 -0
- package/dist/connectors/googleDocs.js +409 -0
- package/dist/connectors/googleDocs.js.map +1 -0
- package/dist/connectors/googleDrive.d.ts +12 -0
- package/dist/connectors/googleDrive.js +35 -8
- package/dist/connectors/googleDrive.js.map +1 -1
- package/dist/connectors/grafana.d.ts +133 -0
- package/dist/connectors/grafana.js +478 -0
- package/dist/connectors/grafana.js.map +1 -0
- package/dist/connectors/jira.d.ts +25 -0
- package/dist/connectors/jira.js +227 -1
- package/dist/connectors/jira.js.map +1 -1
- package/dist/connectors/linear.js +1 -1
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.js +78 -16
- package/dist/connectors/mcpOAuth.js.map +1 -1
- package/dist/connectors/monday.d.ts +217 -0
- package/dist/connectors/monday.js +655 -0
- package/dist/connectors/monday.js.map +1 -0
- package/dist/connectors/mongodb.d.ts +139 -0
- package/dist/connectors/mongodb.js +455 -0
- package/dist/connectors/mongodb.js.map +1 -0
- package/dist/connectors/oauthStateStore.d.ts +20 -0
- package/dist/connectors/oauthStateStore.js +94 -4
- package/dist/connectors/oauthStateStore.js.map +1 -1
- package/dist/connectors/obsidian.d.ts +83 -0
- package/dist/connectors/obsidian.js +441 -0
- package/dist/connectors/obsidian.js.map +1 -0
- package/dist/connectors/paystack.d.ts +159 -0
- package/dist/connectors/paystack.js +607 -0
- package/dist/connectors/paystack.js.map +1 -0
- package/dist/connectors/pipedrive.d.ts +189 -0
- package/dist/connectors/pipedrive.js +559 -0
- package/dist/connectors/pipedrive.js.map +1 -0
- package/dist/connectors/postgres.d.ts +127 -0
- package/dist/connectors/postgres.js +512 -0
- package/dist/connectors/postgres.js.map +1 -0
- package/dist/connectors/posthog.d.ts +119 -0
- package/dist/connectors/posthog.js +479 -0
- package/dist/connectors/posthog.js.map +1 -0
- package/dist/connectors/redis.d.ts +140 -0
- package/dist/connectors/redis.js +571 -0
- package/dist/connectors/redis.js.map +1 -0
- package/dist/connectors/resend.d.ts +131 -0
- package/dist/connectors/resend.js +529 -0
- package/dist/connectors/resend.js.map +1 -0
- package/dist/connectors/salesforce.d.ts +136 -0
- package/dist/connectors/salesforce.js +603 -0
- package/dist/connectors/salesforce.js.map +1 -0
- package/dist/connectors/secrets.d.ts +51 -0
- package/dist/connectors/secrets.js +102 -0
- package/dist/connectors/secrets.js.map +1 -0
- package/dist/connectors/sendgrid.d.ts +102 -0
- package/dist/connectors/sendgrid.js +423 -0
- package/dist/connectors/sendgrid.js.map +1 -0
- package/dist/connectors/shopify.d.ts +145 -0
- package/dist/connectors/shopify.js +502 -0
- package/dist/connectors/shopify.js.map +1 -0
- package/dist/connectors/slack.d.ts +1 -1
- package/dist/connectors/slack.js +61 -9
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/snowflake.d.ts +119 -0
- package/dist/connectors/snowflake.js +615 -0
- package/dist/connectors/snowflake.js.map +1 -0
- package/dist/connectors/supabase.d.ts +92 -0
- package/dist/connectors/supabase.js +630 -0
- package/dist/connectors/supabase.js.map +1 -0
- package/dist/connectors/todoist.d.ts +117 -0
- package/dist/connectors/todoist.js +485 -0
- package/dist/connectors/todoist.js.map +1 -0
- package/dist/connectors/tokenStorage.js +56 -14
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/connectors/twilio.d.ts +118 -0
- package/dist/connectors/twilio.js +475 -0
- package/dist/connectors/twilio.js.map +1 -0
- package/dist/connectors/vercel.d.ts +110 -0
- package/dist/connectors/vercel.js +479 -0
- package/dist/connectors/vercel.js.map +1 -0
- package/dist/connectors/webflow.d.ts +118 -0
- package/dist/connectors/webflow.js +393 -0
- package/dist/connectors/webflow.js.map +1 -0
- package/dist/connectors/woocommerce.d.ts +220 -0
- package/dist/connectors/woocommerce.js +464 -0
- package/dist/connectors/woocommerce.js.map +1 -0
- package/dist/crypto.js +7 -2
- package/dist/crypto.js.map +1 -1
- package/dist/decisionReplay.js +15 -6
- package/dist/decisionReplay.js.map +1 -1
- package/dist/decisionTraceLog.d.ts +28 -0
- package/dist/decisionTraceLog.js +156 -29
- package/dist/decisionTraceLog.js.map +1 -1
- package/dist/drivers/claude/api.js +5 -4
- package/dist/drivers/claude/api.js.map +1 -1
- package/dist/drivers/claude/envSanitizer.d.ts +0 -6
- package/dist/drivers/claude/envSanitizer.js +17 -2
- package/dist/drivers/claude/envSanitizer.js.map +1 -1
- package/dist/drivers/claude/streamParser.js +5 -4
- package/dist/drivers/claude/streamParser.js.map +1 -1
- package/dist/drivers/claude/subprocess.js +22 -3
- package/dist/drivers/claude/subprocess.js.map +1 -1
- package/dist/drivers/gemini/index.d.ts +22 -0
- package/dist/drivers/gemini/index.js +256 -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 +37 -4
- package/dist/extensionClient.js +58 -16
- package/dist/extensionClient.js.map +1 -1
- package/dist/featureFlags.d.ts +91 -0
- package/dist/featureFlags.js +174 -3
- package/dist/featureFlags.js.map +1 -1
- package/dist/fileLock.js +21 -12
- package/dist/fileLock.js.map +1 -1
- package/dist/fileLockSync.d.ts +67 -0
- package/dist/fileLockSync.js +126 -0
- package/dist/fileLockSync.js.map +1 -0
- package/dist/fp/activityAnalytics.js +26 -12
- package/dist/fp/activityAnalytics.js.map +1 -1
- package/dist/fp/automationInterpreter.d.ts +15 -1
- package/dist/fp/automationInterpreter.js +217 -82
- 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 +24 -5
- package/dist/fp/automationState.js +56 -9
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/automationUtils.js +1 -1
- package/dist/fp/automationUtils.js.map +1 -1
- package/dist/fp/commandDescription.js +7 -1
- package/dist/fp/commandDescription.js.map +1 -1
- package/dist/fp/extensionSnapshot.js +8 -2
- package/dist/fp/extensionSnapshot.js.map +1 -1
- package/dist/fp/interpreterContext.d.ts +66 -1
- package/dist/fp/interpreterContext.js +91 -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/fsWatchWithFallback.d.ts +36 -0
- package/dist/fsWatchWithFallback.js +128 -0
- package/dist/fsWatchWithFallback.js.map +1 -0
- package/dist/haltPushDispatch.d.ts +33 -0
- package/dist/haltPushDispatch.js +103 -0
- package/dist/haltPushDispatch.js.map +1 -0
- package/dist/httpBodyValidation.d.ts +41 -0
- package/dist/httpBodyValidation.js +45 -0
- package/dist/httpBodyValidation.js.map +1 -0
- package/dist/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +61 -1
- package/dist/inboxRoutes.js.map +1 -1
- package/dist/index.js +1053 -76
- package/dist/index.js.map +1 -1
- package/dist/installGuard.js +6 -2
- package/dist/installGuard.js.map +1 -1
- package/dist/lockfile.js +31 -4
- 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/oauthRoutes.d.ts +1 -1
- package/dist/oauthRoutes.js +5 -3
- package/dist/oauthRoutes.js.map +1 -1
- package/dist/orchestrator/childBridgeRegistry.js +29 -11
- package/dist/orchestrator/childBridgeRegistry.js.map +1 -1
- package/dist/patchworkConfig.d.ts +44 -1
- package/dist/patchworkConfig.js +28 -3
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.js +10 -1
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +12 -14
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.js +3 -2
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/processTree.d.ts +34 -0
- package/dist/processTree.js +105 -0
- package/dist/processTree.js.map +1 -0
- package/dist/prompts.js +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +9 -0
- package/dist/recipeOrchestration.js +341 -24
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +58 -2
- package/dist/recipeRoutes.js +744 -115
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/RecipeOrchestrator.d.ts +2 -0
- package/dist/recipes/RecipeOrchestrator.js +6 -1
- package/dist/recipes/RecipeOrchestrator.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +25 -5
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +2 -0
- package/dist/recipes/chainedRunner.js +142 -5
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.d.ts +66 -0
- package/dist/recipes/connectorPreflight.js +169 -0
- package/dist/recipes/connectorPreflight.js.map +1 -0
- package/dist/recipes/dependencyGraph.js +13 -5
- package/dist/recipes/dependencyGraph.js.map +1 -1
- package/dist/recipes/githubInstallSource.d.ts +128 -0
- package/dist/recipes/githubInstallSource.js +206 -0
- package/dist/recipes/githubInstallSource.js.map +1 -0
- package/dist/recipes/haltCategory.d.ts +119 -0
- package/dist/recipes/haltCategory.js +188 -0
- package/dist/recipes/haltCategory.js.map +1 -0
- package/dist/recipes/idempotencyKey.d.ts +134 -0
- package/dist/recipes/idempotencyKey.js +322 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/installer.js +48 -2
- package/dist/recipes/installer.js.map +1 -1
- 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/names.d.ts +20 -0
- package/dist/recipes/names.js +25 -0
- package/dist/recipes/names.js.map +1 -1
- package/dist/recipes/parser.js +88 -5
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/replayRun.js +1 -1
- package/dist/recipes/replayRun.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 +30 -0
- package/dist/recipes/scheduler.js +69 -19
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +36 -0
- package/dist/recipes/schemaGenerator.js +103 -5
- package/dist/recipes/schemaGenerator.js.map +1 -1
- package/dist/recipes/stepObservation.js +9 -0
- package/dist/recipes/stepObservation.js.map +1 -1
- package/dist/recipes/toolRegistry.js +20 -3
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/fanOut.d.ts +20 -0
- package/dist/recipes/tools/fanOut.js +199 -0
- package/dist/recipes/tools/fanOut.js.map +1 -0
- package/dist/recipes/tools/file.js +5 -2
- package/dist/recipes/tools/file.js.map +1 -1
- package/dist/recipes/tools/github.d.ts +1 -1
- package/dist/recipes/tools/github.js +75 -1
- package/dist/recipes/tools/github.js.map +1 -1
- package/dist/recipes/tools/gmail.js +27 -5
- package/dist/recipes/tools/gmail.js.map +1 -1
- package/dist/recipes/tools/googleDrive.js +64 -0
- package/dist/recipes/tools/googleDrive.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 +2 -0
- package/dist/recipes/tools/index.js +2 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/tools/slack.js +1 -1
- package/dist/recipes/validation.d.ts +17 -0
- package/dist/recipes/validation.js +29 -11
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/workspaceRoot.d.ts +37 -0
- package/dist/recipes/workspaceRoot.js +73 -0
- package/dist/recipes/workspaceRoot.js.map +1 -0
- package/dist/recipes/yamlPositions.d.ts +56 -0
- package/dist/recipes/yamlPositions.js +183 -0
- package/dist/recipes/yamlPositions.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +182 -8
- package/dist/recipes/yamlRunner.js +877 -217
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +19 -3
- package/dist/recipesHttp.js +67 -11
- package/dist/recipesHttp.js.map +1 -1
- package/dist/resources.js +21 -13
- package/dist/resources.js.map +1 -1
- package/dist/runLog.d.ts +58 -5
- package/dist/runLog.js +98 -21
- package/dist/runLog.js.map +1 -1
- package/dist/sanitizeParsedJson.d.ts +39 -0
- package/dist/sanitizeParsedJson.js +55 -0
- package/dist/sanitizeParsedJson.js.map +1 -0
- package/dist/schemas/recipe.v1.json +885 -196
- package/dist/server.d.ts +160 -3
- package/dist/server.js +1099 -122
- package/dist/server.js.map +1 -1
- package/dist/sessionCheckpoint.d.ts +8 -0
- package/dist/sessionCheckpoint.js +33 -3
- package/dist/sessionCheckpoint.js.map +1 -1
- package/dist/ssrfGuard.d.ts +16 -0
- package/dist/ssrfGuard.js +87 -4
- package/dist/ssrfGuard.js.map +1 -1
- package/dist/streamableHttp.d.ts +9 -4
- package/dist/streamableHttp.js +76 -20
- package/dist/streamableHttp.js.map +1 -1
- package/dist/telemetry.js +19 -12
- package/dist/telemetry.js.map +1 -1
- package/dist/testing/shadowRun.d.ts +52 -0
- package/dist/testing/shadowRun.js +71 -0
- package/dist/testing/shadowRun.js.map +1 -0
- package/dist/tools/batchLsp.d.ts +3 -0
- package/dist/tools/bridgeDoctor.d.ts +11 -0
- package/dist/tools/bridgeDoctor.js +48 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/bridgeStatus.d.ts +0 -12
- package/dist/tools/bridgeStatus.js +2 -28
- package/dist/tools/bridgeStatus.js.map +1 -1
- package/dist/tools/cancelClaudeTask.d.ts +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/closeTabs.d.ts +1 -0
- package/dist/tools/codeLens.d.ts +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +9 -0
- package/dist/tools/ctxGetTaskContext.js +50 -2
- package/dist/tools/ctxGetTaskContext.js.map +1 -1
- package/dist/tools/ctxQueryTraces.js +32 -24
- package/dist/tools/ctxQueryTraces.js.map +1 -1
- package/dist/tools/ctxSaveTrace.d.ts +1 -0
- package/dist/tools/debug.d.ts +4 -0
- package/dist/tools/decorations.d.ts +2 -0
- package/dist/tools/detectUnusedCode.js +9 -7
- package/dist/tools/detectUnusedCode.js.map +1 -1
- package/dist/tools/documentLinks.d.ts +1 -0
- package/dist/tools/editText.d.ts +1 -0
- package/dist/tools/editText.js +2 -1
- package/dist/tools/editText.js.map +1 -1
- package/dist/tools/enrichCommit.d.ts +1 -0
- package/dist/tools/enrichStackTrace.js +11 -5
- package/dist/tools/enrichStackTrace.js.map +1 -1
- package/dist/tools/explainDiagnostic.d.ts +1 -0
- package/dist/tools/explainSymbol.d.ts +1 -0
- package/dist/tools/fileOperations.d.ts +3 -0
- package/dist/tools/fileOperations.js +2 -1
- package/dist/tools/fileOperations.js.map +1 -1
- package/dist/tools/fileWatcher.d.ts +2 -0
- package/dist/tools/fileWatcher.js +8 -2
- package/dist/tools/fileWatcher.js.map +1 -1
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.js +10 -5
- package/dist/tools/fixAllLintErrors.js.map +1 -1
- package/dist/tools/foldingRanges.d.ts +1 -0
- package/dist/tools/formatDocument.d.ts +1 -0
- package/dist/tools/formatDocument.js +10 -5
- package/dist/tools/formatDocument.js.map +1 -1
- package/dist/tools/generateTests.d.ts +1 -0
- package/dist/tools/getAIComments.d.ts +1 -0
- package/dist/tools/getAnalyticsReport.js +5 -1
- package/dist/tools/getAnalyticsReport.js.map +1 -1
- package/dist/tools/getBufferContent.d.ts +1 -0
- package/dist/tools/getChangeImpact.d.ts +1 -0
- package/dist/tools/getChangeImpact.js +23 -14
- package/dist/tools/getChangeImpact.js.map +1 -1
- package/dist/tools/getClaudeTaskStatus.d.ts +1 -0
- package/dist/tools/getCodeCoverage.d.ts +1 -0
- package/dist/tools/getCodeCoverage.js +32 -14
- package/dist/tools/getCodeCoverage.js.map +1 -1
- package/dist/tools/getCommitsForIssue.d.ts +1 -0
- package/dist/tools/getDebugState.d.ts +1 -0
- package/dist/tools/getDiagnostics.js +32 -29
- package/dist/tools/getDiagnostics.js.map +1 -1
- package/dist/tools/getDiffFromHandoff.js +8 -2
- package/dist/tools/getDiffFromHandoff.js.map +1 -1
- package/dist/tools/getDocumentSymbols.d.ts +1 -0
- package/dist/tools/getGitHotspots.d.ts +1 -0
- package/dist/tools/getImportedSignatures.d.ts +1 -0
- package/dist/tools/getImportedSignatures.js +18 -14
- package/dist/tools/getImportedSignatures.js.map +1 -1
- package/dist/tools/getPRTemplate.d.ts +1 -0
- package/dist/tools/getProjectContext.js +23 -10
- package/dist/tools/getProjectContext.js.map +1 -1
- package/dist/tools/getSymbolHistory.d.ts +1 -0
- package/dist/tools/getToolCapabilities.d.ts +10 -5
- package/dist/tools/getToolCapabilities.js +79 -202
- package/dist/tools/getToolCapabilities.js.map +1 -1
- package/dist/tools/getTypeSignature.d.ts +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +1 -0
- package/dist/tools/gitWrite.d.ts +11 -0
- package/dist/tools/github/actions.d.ts +2 -0
- package/dist/tools/github/composite.d.ts +3 -0
- package/dist/tools/github/issues.d.ts +4 -0
- package/dist/tools/github/pr.d.ts +7 -0
- package/dist/tools/handoffNote.d.ts +1 -0
- package/dist/tools/handoffNote.js +2 -1
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/headless/lspClient.js +3 -0
- package/dist/tools/headless/lspClient.js.map +1 -1
- package/dist/tools/hoverAtCursor.d.ts +1 -0
- package/dist/tools/httpClient.d.ts +2 -0
- package/dist/tools/httpClient.js +38 -71
- package/dist/tools/httpClient.js.map +1 -1
- package/dist/tools/inlayHints.d.ts +1 -0
- package/dist/tools/launchQuickTask.d.ts +1 -0
- package/dist/tools/listClaudeTasks.d.ts +1 -0
- package/dist/tools/listClaudeTasks.js +10 -8
- package/dist/tools/listClaudeTasks.js.map +1 -1
- package/dist/tools/listTerminals.d.ts +1 -0
- package/dist/tools/lsp.d.ts +15 -0
- package/dist/tools/lsp.js +17 -0
- package/dist/tools/lsp.js.map +1 -1
- package/dist/tools/navigateToSymbolByName.d.ts +1 -0
- package/dist/tools/openDiff.d.ts +1 -0
- package/dist/tools/openDiff.js +4 -1
- package/dist/tools/openDiff.js.map +1 -1
- package/dist/tools/openFile.d.ts +1 -0
- package/dist/tools/openFile.js +4 -1
- package/dist/tools/openFile.js.map +1 -1
- package/dist/tools/openInBrowser.js +6 -1
- package/dist/tools/openInBrowser.js.map +1 -1
- package/dist/tools/organizeImports.d.ts +1 -0
- package/dist/tools/organizeImports.js +5 -3
- package/dist/tools/organizeImports.js.map +1 -1
- package/dist/tools/performanceReport.js +5 -3
- package/dist/tools/performanceReport.js.map +1 -1
- package/dist/tools/planPersistence.d.ts +3 -0
- package/dist/tools/planPersistence.js +4 -1
- package/dist/tools/planPersistence.js.map +1 -1
- package/dist/tools/previewEdit.d.ts +1 -0
- package/dist/tools/previewEdit.js +15 -4
- package/dist/tools/previewEdit.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +54 -11
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/dist/tools/refactorAnalyze.d.ts +1 -0
- package/dist/tools/refactorExtractFunction.js +4 -1
- package/dist/tools/refactorExtractFunction.js.map +1 -1
- package/dist/tools/refactorPreview.d.ts +1 -0
- package/dist/tools/refactorPreview.js +10 -2
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.d.ts +1 -0
- package/dist/tools/replaceBlock.js +2 -1
- package/dist/tools/replaceBlock.js.map +1 -1
- package/dist/tools/resumeClaudeTask.d.ts +1 -0
- package/dist/tools/runClaudeTask.d.ts +1 -0
- package/dist/tools/runTests.js +15 -4
- package/dist/tools/runTests.js.map +1 -1
- package/dist/tools/screenshot.d.ts +1 -0
- package/dist/tools/screenshotAndAnnotate.js +6 -2
- package/dist/tools/screenshotAndAnnotate.js.map +1 -1
- package/dist/tools/searchAndReplace.d.ts +1 -0
- package/dist/tools/searchAndReplace.js +17 -7
- package/dist/tools/searchAndReplace.js.map +1 -1
- package/dist/tools/searchTools.js +20 -19
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/searchWorkspace.d.ts +1 -0
- package/dist/tools/selectionRanges.d.ts +1 -0
- package/dist/tools/semanticTokens.d.ts +1 -0
- package/dist/tools/signatureHelp.d.ts +1 -0
- package/dist/tools/spawnWorkspace.js +15 -7
- package/dist/tools/spawnWorkspace.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/testRunners/pytest.js +6 -2
- package/dist/tools/testRunners/pytest.js.map +1 -1
- package/dist/tools/testRunners/vitestJest.js +3 -1
- package/dist/tools/testRunners/vitestJest.js.map +1 -1
- package/dist/tools/testTraceToSource.d.ts +1 -0
- package/dist/tools/transaction.d.ts +4 -0
- package/dist/tools/transaction.js +4 -1
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/utils.d.ts +18 -0
- package/dist/tools/utils.js +103 -18
- package/dist/tools/utils.js.map +1 -1
- package/dist/tools/vscodeCommands.d.ts +2 -0
- package/dist/tools/vscodeTasks.d.ts +2 -0
- package/dist/tools/workspaceSettings.d.ts +1 -0
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +103 -52
- package/dist/transport.js.map +1 -1
- package/dist/winShim.d.ts +34 -0
- package/dist/winShim.js +94 -0
- package/dist/winShim.js.map +1 -0
- package/dist/wireHaltPushDispatch.d.ts +38 -0
- package/dist/wireHaltPushDispatch.js +71 -0
- package/dist/wireHaltPushDispatch.js.map +1 -0
- package/dist/writeFileAtomic.d.ts +23 -0
- package/dist/writeFileAtomic.js +121 -0
- package/dist/writeFileAtomic.js.map +1 -0
- package/package.json +23 -7
- package/scripts/postinstall.mjs +42 -2
- package/scripts/smoke/run-all.mjs +213 -0
- package/scripts/start-all.mjs +572 -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 +2 -2
- package/templates/recipes/approval-queue-ui-test.yaml +1 -1
- package/templates/recipes/ctx-loop-test.yaml +1 -1
- package/templates/recipes/fix-errors-on-save.yaml +71 -0
- package/templates/recipes/morning-brief.yaml +5 -2
- package/templates/recipes/project-health-check.yaml +4 -1
- package/templates/recipes/sentry-to-linear.yaml +72 -38
- package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
- package/templates/recipes/webhook/customer-escalation.yaml +8 -9
- package/templates/recipes/webhook/meeting-prep.yaml +11 -5
- 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
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* file.read — read file into `into` variable (optional: true ok)
|
|
15
15
|
* git.log_since — run git log --oneline --since=<since> (injected for tests)
|
|
16
16
|
* git.stale_branches — list branches with no activity in N days
|
|
17
|
-
* diagnostics.get —
|
|
17
|
+
* diagnostics.get — returns the bridge's LSP diagnostics; in CLI runs
|
|
18
|
+
* (no bridge available) returns a JSON {ok:false,error}
|
|
19
|
+
* payload that the step-error detector flags so the
|
|
20
|
+
* recipe halts rather than silently succeeding.
|
|
18
21
|
*
|
|
19
22
|
* Supported trigger types (for `patchwork recipe run`):
|
|
20
23
|
* manual, cron — both run immediately via CLI
|
|
@@ -27,14 +30,29 @@ import path from "node:path";
|
|
|
27
30
|
import { fileURLToPath } from "node:url";
|
|
28
31
|
import { parse as parseYaml } from "yaml";
|
|
29
32
|
import { captureFixture } from "../connectors/fixtureRecorder.js";
|
|
33
|
+
import { sanitizeEnv } from "../drivers/claude/envSanitizer.js";
|
|
30
34
|
import { loadConfig as loadPatchworkConfigSync } from "../patchworkConfig.js";
|
|
31
35
|
import { findYamlRecipePath } from "../recipesHttp.js";
|
|
36
|
+
/**
|
|
37
|
+
* Local alias for `sanitizeParsedJson` from `src/sanitizeParsedJson.ts`.
|
|
38
|
+
* Kept under the old name so the existing callsites in this file don't
|
|
39
|
+
* need to be renamed. The shared module is the canonical home for the
|
|
40
|
+
* prototype-pollution scrub — see PR #568 + audit 2026-05-17 + the
|
|
41
|
+
* comment in that file for full rationale.
|
|
42
|
+
*/
|
|
43
|
+
import { sanitizeParsedJson as sanitizeParsed } from "../sanitizeParsedJson.js";
|
|
44
|
+
import { ensureCmdShim } from "../winShim.js";
|
|
32
45
|
import { executeAgent as _executeAgent, } from "./agentExecutor.js";
|
|
33
|
-
import {
|
|
46
|
+
import { categoriseHaltReason } from "./haltCategory.js";
|
|
47
|
+
import { assertValidManualRunId, deriveScopeKey, WriteEffectLedger, } from "./idempotencyKey.js";
|
|
48
|
+
import { buildJudgeArtefactBlock, JUDGE_PROMPT_SUFFIX, parseJudgeVerdict, } from "./judgeVerdict.js";
|
|
49
|
+
import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "./migrations/index.js";
|
|
34
50
|
import { resolveRecipePath } from "./resolveRecipePath.js";
|
|
51
|
+
import { RunBudget } from "./runBudget.js";
|
|
35
52
|
import { detectSilentFail } from "./stepObservation.js";
|
|
36
53
|
// Import tool registry and trigger tool self-registration
|
|
37
54
|
import { applyToolOutputContext, executeTool, getTool, hasTool, registerPluginTools, } from "./toolRegistry.js";
|
|
55
|
+
import { resolveWorkspaceRoot } from "./workspaceRoot.js";
|
|
38
56
|
import "./tools/index.js";
|
|
39
57
|
/**
|
|
40
58
|
* Bundled-templates directory used as a third allowed root for nested-recipe
|
|
@@ -111,7 +129,128 @@ export function evaluateExpect(result, expect) {
|
|
|
111
129
|
}
|
|
112
130
|
return failures;
|
|
113
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Lazy AJV for `step.expect.schema`. Initialised on first use so recipes
|
|
134
|
+
* without schema assertions don't pay the import/compile cost.
|
|
135
|
+
*/
|
|
136
|
+
let _stepExpectAjv;
|
|
137
|
+
async function getStepExpectAjv() {
|
|
138
|
+
if (!_stepExpectAjv) {
|
|
139
|
+
const { createAjv2020 } = await import("../ajv2020.js");
|
|
140
|
+
_stepExpectAjv = createAjv2020({ strict: false, allErrors: true });
|
|
141
|
+
}
|
|
142
|
+
return _stepExpectAjv;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Stringify a step value for assertion purposes. Strings pass through;
|
|
146
|
+
* other values JSON.stringify so `matches`/`contains` see something stable.
|
|
147
|
+
*/
|
|
148
|
+
function stringifyForAssert(value) {
|
|
149
|
+
if (typeof value === "string")
|
|
150
|
+
return value;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.stringify(value);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return String(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Evaluate a per-step `expect` block against the step's output value.
|
|
160
|
+
* Returns the list of failure messages (empty = all assertions passed).
|
|
161
|
+
*
|
|
162
|
+
* Slice 2 of the agentic-workflow primitives. v1 supports
|
|
163
|
+
* schema/equals/matches/contains; `on_fail: judge` deliberately omitted —
|
|
164
|
+
* see comment on `StepExpect`.
|
|
165
|
+
*/
|
|
166
|
+
export async function evaluateStepExpect(expect, value) {
|
|
167
|
+
const failures = [];
|
|
168
|
+
const asString = stringifyForAssert(value);
|
|
169
|
+
if (expect.equals !== undefined) {
|
|
170
|
+
const expected = expect.equals;
|
|
171
|
+
const expectedStr = typeof expected === "string" ? expected : stringifyForAssert(expected);
|
|
172
|
+
if (asString !== expectedStr) {
|
|
173
|
+
failures.push(`equals: expected ${JSON.stringify(expectedStr)}, got ${JSON.stringify(asString)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (expect.contains !== undefined) {
|
|
177
|
+
const needles = Array.isArray(expect.contains)
|
|
178
|
+
? expect.contains
|
|
179
|
+
: [expect.contains];
|
|
180
|
+
for (const needle of needles) {
|
|
181
|
+
if (!asString.includes(needle)) {
|
|
182
|
+
failures.push(`contains: missing ${JSON.stringify(needle)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (expect.matches !== undefined) {
|
|
187
|
+
// Guard against ReDoS: limit pattern and input string length before
|
|
188
|
+
// compiling / executing user-supplied regex.
|
|
189
|
+
const MAX_PATTERN = 500;
|
|
190
|
+
const MAX_INPUT = 65_536; // 64 KB
|
|
191
|
+
if (expect.matches.length > MAX_PATTERN) {
|
|
192
|
+
failures.push(`matches: regex pattern too long (${expect.matches.length} chars, max ${MAX_PATTERN})`);
|
|
193
|
+
return failures;
|
|
194
|
+
}
|
|
195
|
+
let re;
|
|
196
|
+
try {
|
|
197
|
+
re = new RegExp(expect.matches);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
failures.push(`matches: invalid regex ${JSON.stringify(expect.matches)} (${err instanceof Error ? err.message : String(err)})`);
|
|
201
|
+
return failures;
|
|
202
|
+
}
|
|
203
|
+
const testInput = asString.length > MAX_INPUT ? asString.slice(0, MAX_INPUT) : asString;
|
|
204
|
+
if (!re.test(testInput)) {
|
|
205
|
+
failures.push(`matches: ${JSON.stringify(expect.matches)} did not match output`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (expect.schema !== undefined) {
|
|
209
|
+
let parsed;
|
|
210
|
+
try {
|
|
211
|
+
parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
failures.push(`schema: output is not valid JSON`);
|
|
215
|
+
return failures;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const ajv = await getStepExpectAjv();
|
|
219
|
+
const validate = ajv.compile(expect.schema);
|
|
220
|
+
if (!validate(parsed)) {
|
|
221
|
+
const errs = (validate.errors ?? [])
|
|
222
|
+
.map((e) => `${e.instancePath || "/"} ${e.message ?? "invalid"}`)
|
|
223
|
+
.join("; ");
|
|
224
|
+
failures.push(`schema: ${errs || "validation failed"}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
failures.push(`schema: compile error (${err instanceof Error ? err.message : String(err)})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return failures;
|
|
232
|
+
}
|
|
114
233
|
// Strip tool-call narration some models (e.g. Gemini) prepend before the markdown block.
|
|
234
|
+
/**
|
|
235
|
+
* Phase 0β — separator-agnostic inbox-path detector. Extracted so the
|
|
236
|
+
* Windows path-separator behaviour can be unit-tested by injecting
|
|
237
|
+
* `path.win32` / `path.posix` without booting a real recipe runner.
|
|
238
|
+
*
|
|
239
|
+
* Returns true when `candidate` resolves to a direct child of
|
|
240
|
+
* `inboxDirAbs`, isn't a dotfile, and lives in (not above) the inbox
|
|
241
|
+
* dir. Both arguments must already be platform-appropriate absolute
|
|
242
|
+
* paths (resolve them with the same path module before calling).
|
|
243
|
+
*/
|
|
244
|
+
export function isInboxPathFor(candidate, inboxDirAbs, pathMod) {
|
|
245
|
+
const target = pathMod.resolve(candidate);
|
|
246
|
+
const rel = pathMod.relative(inboxDirAbs, target);
|
|
247
|
+
if (!rel || rel.startsWith("..") || pathMod.isAbsolute(rel))
|
|
248
|
+
return false;
|
|
249
|
+
if (pathMod.basename(target).startsWith("."))
|
|
250
|
+
return false;
|
|
251
|
+
// Only direct children — `~/.patchwork/inbox/foo.md`, not nested.
|
|
252
|
+
return !rel.includes(pathMod.sep);
|
|
253
|
+
}
|
|
115
254
|
function stripLeadingNarration(text) {
|
|
116
255
|
const lines = text.split("\n");
|
|
117
256
|
const firstMarkdown = lines.findIndex((l) => /^(#|>|`|\||[-*+] |\d+\. |\*\*)/.test(l.trimStart()));
|
|
@@ -222,7 +361,88 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
222
361
|
...envCtx,
|
|
223
362
|
...seedContext,
|
|
224
363
|
};
|
|
225
|
-
const stepDeps = resolveStepDeps(deps);
|
|
364
|
+
const stepDeps = resolveStepDeps(deps, { recipeName: recipe.name });
|
|
365
|
+
// Phase 0β — inbox provenance. When a recipe `file.write` / `file.append`
|
|
366
|
+
// step targets `~/.patchwork/inbox/`, prepend a YAML frontmatter block
|
|
367
|
+
// (first write only) recording recipe + run + trigger, and accumulate the
|
|
368
|
+
// delivered filename onto the run record's `inboxOutputs`. Old recipes /
|
|
369
|
+
// non-inbox paths pass through unchanged.
|
|
370
|
+
//
|
|
371
|
+
// Windows path-separator fix (CI repro 2026-05-20): the original
|
|
372
|
+
// implementation built the prefix as `${os.homedir()}/.patchwork/inbox/`
|
|
373
|
+
// and compared with `startsWith`, which failed on Windows where
|
|
374
|
+
// resolved absolute paths use `\` separators and `os.homedir()` returns
|
|
375
|
+
// `C:\Users\...`. Now we resolve both sides through `path.resolve()`
|
|
376
|
+
// and use `path.relative()` to detect containment so the comparison is
|
|
377
|
+
// separator-agnostic. Also case-insensitive on Win32 (NTFS).
|
|
378
|
+
const inboxDirAbs = path.resolve(path.join(os.homedir(), ".patchwork", "inbox"));
|
|
379
|
+
const inboxOutputs = [];
|
|
380
|
+
const isInboxPath = (abs) => isInboxPathFor(abs, inboxDirAbs, path);
|
|
381
|
+
const buildFrontmatter = () => {
|
|
382
|
+
const triggerKindAtWrite = yamlTriggerKind;
|
|
383
|
+
const lines = ["---", `recipe: ${recipe.name}`];
|
|
384
|
+
if (runSeq !== undefined)
|
|
385
|
+
lines.push(`runSeq: ${runSeq}`);
|
|
386
|
+
lines.push(`trigger: ${triggerKindAtWrite}`, `deliveredAt: ${new Date().toISOString()}`, "---", "", "");
|
|
387
|
+
return lines.join("\n");
|
|
388
|
+
};
|
|
389
|
+
const recordInboxDelivery = (abs) => {
|
|
390
|
+
inboxOutputs.push({
|
|
391
|
+
filename: path.basename(abs),
|
|
392
|
+
deliveredAt: Date.now(),
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
// Atomic read-or-default: a single `readFileSync` in a try/catch. No
|
|
396
|
+
// `existsSync`/`statSync` probe around the write — on Windows a stat
|
|
397
|
+
// immediately before write can race a concurrent fd holder and surface
|
|
398
|
+
// `EBUSY`/`EPERM`. The read either succeeds (file present) or throws
|
|
399
|
+
// ENOENT (treated as new file). Either way we never stat the same path
|
|
400
|
+
// we're about to write.
|
|
401
|
+
const readExistingOrEmpty = (abs) => {
|
|
402
|
+
try {
|
|
403
|
+
return readFileSync(abs, "utf-8");
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
const originalWrite = stepDeps.writeFile;
|
|
410
|
+
const originalAppend = stepDeps.appendFile;
|
|
411
|
+
stepDeps.writeFile = (p, content) => {
|
|
412
|
+
if (isInboxPath(p)) {
|
|
413
|
+
// First-write detection by content shape, not by stat. Empty string
|
|
414
|
+
// (ENOENT) and any file that does NOT already begin with `---\n`
|
|
415
|
+
// gets frontmatter; pre-frontmattered files are overwritten as-is
|
|
416
|
+
// so consumers can replay a recipe without doubling the header.
|
|
417
|
+
const existing = readExistingOrEmpty(p);
|
|
418
|
+
const hasFm = existing.startsWith("---\n");
|
|
419
|
+
const final = hasFm ? content : buildFrontmatter() + content;
|
|
420
|
+
originalWrite(p, final);
|
|
421
|
+
recordInboxDelivery(p);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
originalWrite(p, content);
|
|
425
|
+
};
|
|
426
|
+
stepDeps.appendFile = (p, content) => {
|
|
427
|
+
if (isInboxPath(p)) {
|
|
428
|
+
// file.append: never re-prepend. If file is brand-new, seed one
|
|
429
|
+
// frontmatter block so an append-only recipe still gets
|
|
430
|
+
// provenance. Same atomic read-or-default — no stat probe.
|
|
431
|
+
const existing = readExistingOrEmpty(p);
|
|
432
|
+
if (existing.length === 0) {
|
|
433
|
+
originalWrite(p, buildFrontmatter() + content);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
originalAppend(p, content);
|
|
437
|
+
}
|
|
438
|
+
recordInboxDelivery(p);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
originalAppend(p, content);
|
|
442
|
+
};
|
|
443
|
+
// PR2b: one per-run budget shared across all agent steps. Absent
|
|
444
|
+
// `recipe.budget` → no enforcement, no overhead.
|
|
445
|
+
const runBudget = new RunBudget(recipe.budget);
|
|
226
446
|
// Open a `running`-state run-log entry so the dashboard sees the recipe
|
|
227
447
|
// as in flight. Only when a long-lived `runLog` is provided (bridge path);
|
|
228
448
|
// CLI runs fall back to `appendDirect` at end via the existing logDir
|
|
@@ -241,6 +461,9 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
241
461
|
trigger: yamlTriggerKind,
|
|
242
462
|
createdAt: recipeStartedAt,
|
|
243
463
|
startedAt: recipeStartedAt,
|
|
464
|
+
...(deps.manualRunId !== undefined && {
|
|
465
|
+
manualRunId: deps.manualRunId,
|
|
466
|
+
}),
|
|
244
467
|
});
|
|
245
468
|
}
|
|
246
469
|
catch {
|
|
@@ -251,221 +474,512 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
251
474
|
const stepResults = [];
|
|
252
475
|
let stepsRun = 0;
|
|
253
476
|
let runError;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
477
|
+
// Live-tail SSE broadcaster. Wrapped in a try/catch on every call so a
|
|
478
|
+
// misbehaving listener can never break the run (mirrors chainedRunner).
|
|
479
|
+
// No-ops when `activityLog` isn't wired (CLI runs, tests, mocks).
|
|
480
|
+
const broadcast = deps.activityLog;
|
|
481
|
+
const emit = (event, metadata) => {
|
|
482
|
+
if (!broadcast || runSeq === undefined || stepDeps.testMode)
|
|
483
|
+
return;
|
|
484
|
+
try {
|
|
485
|
+
broadcast.recordEvent(event, metadata);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
/* live-tail must not break a recipe run */
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
// Emit recipe_started as soon as we have a runSeq. The dashboard
|
|
492
|
+
// RecipeRunInline component watches for this event to flip a row
|
|
493
|
+
// from "queued" to "running" without waiting for the first step.
|
|
494
|
+
emit("recipe_started", {
|
|
495
|
+
runSeq,
|
|
496
|
+
recipeName: recipe.name,
|
|
497
|
+
trigger: yamlTriggerKind,
|
|
498
|
+
totalSteps: recipe.steps.length,
|
|
499
|
+
ts: recipeStartedAt,
|
|
500
|
+
});
|
|
501
|
+
// Push live step results into the run-log ring so the dashboard's
|
|
502
|
+
// `/runs/[seq]` page surfaces verdicts + haltReasons mid-flight,
|
|
503
|
+
// instead of waiting for the whole recipe to finish via
|
|
504
|
+
// `completeRun`. The runLog ignores non-running entries; cron/webhook
|
|
505
|
+
// runs through the orchestrator path (where `runSeq` is undefined)
|
|
506
|
+
// skip this entirely.
|
|
507
|
+
const persistLiveStepResults = () => {
|
|
508
|
+
if (!deps.runLog || runSeq === undefined || stepDeps.testMode)
|
|
509
|
+
return;
|
|
510
|
+
try {
|
|
511
|
+
deps.runLog.updateRunSteps(runSeq, stepResults);
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
/* live-tail is best-effort; never break a recipe run for it */
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
// Track per-step start timestamps so done events carry durationMs
|
|
518
|
+
// without a second roundtrip.
|
|
519
|
+
const stepStartTs = new Map();
|
|
520
|
+
// Emit recipe_step_done for the step result just pushed onto
|
|
521
|
+
// `stepResults`. Every loop branch (skip / budget / agent / tool)
|
|
522
|
+
// pushes exactly one result before it ends, so the last element is
|
|
523
|
+
// always the current step. `stepId` mirrors recipe_step_start's
|
|
524
|
+
// `stepIdForEmit` so live consumers can correlate start↔done — the
|
|
525
|
+
// pushed result's own id can diverge for agent steps without `into`.
|
|
526
|
+
const emitStepDone = (stepIdForEmit) => {
|
|
527
|
+
const justPushed = stepResults[stepResults.length - 1];
|
|
528
|
+
if (!justPushed)
|
|
529
|
+
return;
|
|
530
|
+
const haltReason = justPushed.haltReason;
|
|
531
|
+
emit("recipe_step_done", {
|
|
532
|
+
runSeq,
|
|
533
|
+
recipeName: recipe.name,
|
|
534
|
+
stepId: stepIdForEmit,
|
|
535
|
+
tool: justPushed.tool,
|
|
536
|
+
status: justPushed.status,
|
|
537
|
+
durationMs: justPushed.durationMs,
|
|
538
|
+
...(justPushed.error !== undefined && { error: justPushed.error }),
|
|
539
|
+
...(haltReason !== undefined && {
|
|
540
|
+
haltReason,
|
|
541
|
+
haltCategory: justPushed.haltCategory ?? categoriseHaltReason(haltReason),
|
|
542
|
+
}),
|
|
543
|
+
ts: Date.now(),
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
// The step loop is wrapped so an uncaught throw from any unguarded
|
|
547
|
+
// call site (a `when`/prompt render on a malformed step, a path-jail
|
|
548
|
+
// re-check, etc.) cannot escape `runYamlRecipe` and strand the
|
|
549
|
+
// run-log entry at "running" forever. On throw we capture the
|
|
550
|
+
// message into `runError` and fall through to the normal
|
|
551
|
+
// finalization path, which marks the run "error".
|
|
552
|
+
try {
|
|
553
|
+
for (const step of recipe.steps) {
|
|
554
|
+
const stepIdForEmit = step.into ?? step.agent?.into ?? `step_${stepsRun}`;
|
|
555
|
+
const stepTs = Date.now();
|
|
556
|
+
stepStartTs.set(stepIdForEmit, stepTs);
|
|
557
|
+
emit("recipe_step_start", {
|
|
558
|
+
runSeq,
|
|
559
|
+
recipeName: recipe.name,
|
|
560
|
+
stepId: stepIdForEmit,
|
|
561
|
+
tool: step.agent ? "agent" : step.tool,
|
|
562
|
+
ts: stepTs,
|
|
563
|
+
});
|
|
564
|
+
// Evaluate `when` guard before running anything. Mirrors
|
|
565
|
+
// chainedRunner.ts:248-266 — render the template, then truthy-check the
|
|
566
|
+
// result (empty string, "0", "false", "null", "undefined" are falsy).
|
|
567
|
+
// A falsy guard records the step as `skipped`, increments stepsRun, and
|
|
568
|
+
// continues — it is NOT a failure. Bridge-dev iMessage recipes rely on
|
|
569
|
+
// this to suppress the iMessage agent step when phone is empty.
|
|
570
|
+
if (typeof step.when === "string" && step.when.length > 0) {
|
|
571
|
+
const rendered = render(step.when, ctx).trim().toLowerCase();
|
|
572
|
+
const truthy = !!rendered &&
|
|
573
|
+
rendered !== "0" &&
|
|
574
|
+
rendered !== "false" &&
|
|
575
|
+
rendered !== "null" &&
|
|
576
|
+
rendered !== "undefined";
|
|
577
|
+
if (!truthy) {
|
|
578
|
+
const skipId = step.into ?? step.agent?.into ?? `step_${stepsRun}`;
|
|
579
|
+
stepResults.push({
|
|
580
|
+
id: skipId,
|
|
581
|
+
tool: step.agent ? "agent" : step.tool,
|
|
582
|
+
status: "skipped",
|
|
583
|
+
durationMs: 0,
|
|
584
|
+
});
|
|
585
|
+
stepsRun++;
|
|
586
|
+
persistLiveStepResults();
|
|
587
|
+
emit("recipe_step_done", {
|
|
588
|
+
runSeq,
|
|
589
|
+
recipeName: recipe.name,
|
|
590
|
+
stepId: skipId,
|
|
591
|
+
tool: step.agent ? "agent" : step.tool,
|
|
592
|
+
status: "skipped",
|
|
593
|
+
durationMs: 0,
|
|
594
|
+
ts: Date.now(),
|
|
595
|
+
});
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Handle agent steps separately
|
|
600
|
+
if (step.agent) {
|
|
601
|
+
const agentCfg = step.agent;
|
|
602
|
+
const isJudge = agentCfg.kind === "judge";
|
|
603
|
+
// PR3a: judge prompt convention. Append the structured-verdict
|
|
604
|
+
// suffix and, when `reviews: <stepId>` is set, inject the
|
|
605
|
+
// upstream step's output as an <artefact> block.
|
|
606
|
+
let renderedPrompt = render(agentCfg.prompt, ctx);
|
|
607
|
+
if (isJudge) {
|
|
608
|
+
if (agentCfg.reviews) {
|
|
609
|
+
renderedPrompt += buildJudgeArtefactBlock(ctx[agentCfg.reviews]);
|
|
610
|
+
}
|
|
611
|
+
renderedPrompt += JUDGE_PROMPT_SUFFIX;
|
|
612
|
+
}
|
|
613
|
+
const intoKey = agentCfg.into ?? "agent_output";
|
|
614
|
+
const stepId = intoKey;
|
|
615
|
+
const stepStart = Date.now();
|
|
616
|
+
let agentResult;
|
|
617
|
+
// PR2b: per-recipe token budget. Admission check before dispatch;
|
|
618
|
+
// reconcile actual consumption after. Subscription drivers
|
|
619
|
+
// (Claude CLI, provider subprocess) report `usage === undefined`
|
|
620
|
+
// — `RunBudget.reconcile` records a fail-open warning per driver
|
|
621
|
+
// per run and continues.
|
|
622
|
+
const admission = runBudget.admit();
|
|
623
|
+
if (!admission.admitted) {
|
|
624
|
+
const reason = admission.reason ??
|
|
625
|
+
"Run exceeded its token budget — budget_exceeded.";
|
|
282
626
|
runError = runError ?? reason;
|
|
283
627
|
stepResults.push({
|
|
284
628
|
id: stepId,
|
|
285
629
|
tool: "agent",
|
|
286
630
|
status: "error",
|
|
287
631
|
error: reason,
|
|
288
|
-
|
|
632
|
+
haltReason: reason,
|
|
633
|
+
haltCategory: "budget_exceeded",
|
|
634
|
+
durationMs: 0,
|
|
289
635
|
});
|
|
636
|
+
stepsRun++;
|
|
637
|
+
persistLiveStepResults();
|
|
638
|
+
emitStepDone(stepIdForEmit);
|
|
639
|
+
continue;
|
|
290
640
|
}
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
641
|
+
try {
|
|
642
|
+
const agentReturn = await _executeAgent({
|
|
643
|
+
prompt: renderedPrompt,
|
|
644
|
+
driver: agentCfg.driver === "api" ? "anthropic" : agentCfg.driver,
|
|
645
|
+
model: agentCfg.model,
|
|
646
|
+
...(agentCfg.mcpAccess !== undefined && {
|
|
647
|
+
mcpAccess: agentCfg.mcpAccess,
|
|
648
|
+
}),
|
|
649
|
+
}, buildAgentExecutorDeps(stepDeps, deps));
|
|
650
|
+
agentResult = agentReturn.text;
|
|
651
|
+
runBudget.reconcile(agentCfg.driver === "api"
|
|
652
|
+
? "anthropic"
|
|
653
|
+
: (agentCfg.driver ?? "auto"), agentReturn.usage);
|
|
654
|
+
// Catch both `[agent step failed: ...]` (existing) and the
|
|
655
|
+
// silent-fail patterns `[agent step skipped: ...]` etc. via the
|
|
656
|
+
// shared detector. Per-step opt-out via `silentFailDetection: false`.
|
|
657
|
+
const agentSilentFail = step.silentFailDetection !== false
|
|
658
|
+
? detectSilentFail(agentResult)
|
|
659
|
+
: null;
|
|
660
|
+
if (agentResult.startsWith("[agent step failed:") ||
|
|
661
|
+
agentSilentFail) {
|
|
662
|
+
const reason = agentSilentFail
|
|
663
|
+
? `silent-fail detected (${agentSilentFail.reason}): ${agentSilentFail.matched}`
|
|
664
|
+
: agentResult;
|
|
665
|
+
runError = runError ?? reason;
|
|
296
666
|
stepResults.push({
|
|
297
667
|
id: stepId,
|
|
298
668
|
tool: "agent",
|
|
299
669
|
status: "error",
|
|
300
|
-
error:
|
|
670
|
+
error: reason,
|
|
671
|
+
haltReason: agentSilentFail
|
|
672
|
+
? `Agent step "${stepId}" returned no usable output (silent-fail: ${agentSilentFail.reason}).`
|
|
673
|
+
: `Agent step "${stepId}" reported failure.`,
|
|
674
|
+
haltCategory: "agent_silent_fail",
|
|
301
675
|
durationMs: Date.now() - stepStart,
|
|
302
676
|
});
|
|
303
677
|
}
|
|
304
678
|
else {
|
|
305
|
-
|
|
679
|
+
const stripped = stripLeadingNarration(agentResult);
|
|
680
|
+
if (!stripped.trim()) {
|
|
681
|
+
const errMsg = `[agent step failed: ${agentCfg.driver ?? "agent"} returned only narration or whitespace — no content]`;
|
|
682
|
+
runError = runError ?? errMsg;
|
|
683
|
+
stepResults.push({
|
|
684
|
+
id: stepId,
|
|
685
|
+
tool: "agent",
|
|
686
|
+
status: "error",
|
|
687
|
+
error: errMsg,
|
|
688
|
+
haltReason: `Agent step "${stepId}" returned only narration or whitespace — no content.`,
|
|
689
|
+
haltCategory: "agent_narration_only",
|
|
690
|
+
durationMs: Date.now() - stepStart,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
// Try to parse as JSON so dot-notation ({{meeting.field}}) works
|
|
695
|
+
try {
|
|
696
|
+
const jsonMatch = /```(?:json)?\s*([\s\S]*?)```/.exec(stripped) ?? [null, stripped];
|
|
697
|
+
const parsed = sanitizeParsed(JSON.parse((jsonMatch[1] ?? "").trim()));
|
|
698
|
+
ctx[intoKey] = parsed;
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
ctx[intoKey] = stripped;
|
|
702
|
+
}
|
|
703
|
+
outputs.push(intoKey);
|
|
704
|
+
// PR3a: parse + stash the judge verdict on the step result.
|
|
705
|
+
// Augment-only: a `request_changes` verdict still yields
|
|
706
|
+
// `status: "ok"`. The verdict surfaces via the runlog +
|
|
707
|
+
// future PR3b dashboard panel, but never gates the run.
|
|
708
|
+
const judgeVerdict = isJudge
|
|
709
|
+
? parseJudgeVerdict(stripped)
|
|
710
|
+
: undefined;
|
|
711
|
+
stepResults.push({
|
|
712
|
+
id: stepId,
|
|
713
|
+
tool: "agent",
|
|
714
|
+
status: "ok",
|
|
715
|
+
...(judgeVerdict !== undefined && { judgeVerdict }),
|
|
716
|
+
durationMs: Date.now() - stepStart,
|
|
717
|
+
});
|
|
718
|
+
// Slice 2 — per-step expect eval. Runs on the value just
|
|
719
|
+
// committed to ctx[intoKey]. Halt failure flips the just-pushed
|
|
720
|
+
// result to error and rolls back the ctx commit so downstream
|
|
721
|
+
// steps don't see a value the recipe author rejected.
|
|
722
|
+
if (step.expect) {
|
|
723
|
+
const failures = await evaluateStepExpect(step.expect, ctx[intoKey]);
|
|
724
|
+
if (failures.length > 0) {
|
|
725
|
+
const onFail = step.expect.on_fail ?? "halt";
|
|
726
|
+
const last = stepResults[stepResults.length - 1];
|
|
727
|
+
if (last) {
|
|
728
|
+
if (onFail === "halt") {
|
|
729
|
+
last.status = "error";
|
|
730
|
+
last.error = `expect_failed: ${failures.join("; ")}`;
|
|
731
|
+
last.haltReason = `expect_failed in step "${stepId}": ${failures.join("; ")}`;
|
|
732
|
+
last.haltCategory = "expect_failed";
|
|
733
|
+
const fbk = recipe.on_error?.fallback;
|
|
734
|
+
const fbkOpen = fbk === "log_only" || fbk === "deliver_original";
|
|
735
|
+
const failOpenAgent = step.optional === true || fbkOpen;
|
|
736
|
+
if (!failOpenAgent) {
|
|
737
|
+
runError = runError ?? last.haltReason;
|
|
738
|
+
}
|
|
739
|
+
delete ctx[intoKey];
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
last.expectWarnings = failures;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
752
|
+
runError = runError ?? `agent step "${stepId}" failed: ${msg}`;
|
|
753
|
+
stepResults.push({
|
|
754
|
+
id: stepId,
|
|
755
|
+
tool: "agent",
|
|
756
|
+
status: "error",
|
|
757
|
+
error: msg,
|
|
758
|
+
haltReason: `Agent step "${stepId}" threw before completing: ${msg}`,
|
|
759
|
+
haltCategory: "agent_threw",
|
|
760
|
+
durationMs: Date.now() - stepStart,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
stepsRun++;
|
|
764
|
+
persistLiveStepResults();
|
|
765
|
+
emitStepDone(stepIdForEmit);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const stepStart = Date.now();
|
|
769
|
+
const stepId = step.into ?? `step_${stepsRun}`;
|
|
770
|
+
// Resolve retry policy: step-level overrides recipe-level.
|
|
771
|
+
const retryCount = step.retry ?? recipe.on_error?.retry ?? 0;
|
|
772
|
+
const retryDelayMs = step.retryDelay ?? recipe.on_error?.retryDelay ?? 1000;
|
|
773
|
+
let result = null;
|
|
774
|
+
let stepError;
|
|
775
|
+
let thrownError;
|
|
776
|
+
let thrownErrorCode;
|
|
777
|
+
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
778
|
+
if (attempt > 0) {
|
|
779
|
+
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
780
|
+
}
|
|
781
|
+
stepError = undefined;
|
|
782
|
+
thrownError = undefined;
|
|
783
|
+
thrownErrorCode = undefined;
|
|
784
|
+
try {
|
|
785
|
+
// Slice (sandbox-alternative): per-step wall-clock timeout via
|
|
786
|
+
// Promise.race. The underlying tool keeps running in the
|
|
787
|
+
// background — this is a halt signal for the runner, not a
|
|
788
|
+
// process kill. The thrown error carries a `step_timeout`
|
|
789
|
+
// prefix so categoriseHaltReason maps it correctly.
|
|
790
|
+
const timeoutMs = typeof step.timeout_ms === "number" && step.timeout_ms > 0
|
|
791
|
+
? step.timeout_ms
|
|
792
|
+
: 0;
|
|
793
|
+
if (timeoutMs > 0) {
|
|
794
|
+
let timer;
|
|
795
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
796
|
+
timer = setTimeout(() => {
|
|
797
|
+
reject(new Error(`step_timeout: exceeded ${timeoutMs}ms in step "${step.into ?? step.tool ?? "?"}"`));
|
|
798
|
+
}, timeoutMs);
|
|
799
|
+
});
|
|
800
|
+
try {
|
|
801
|
+
result = await Promise.race([
|
|
802
|
+
executeStep(step, ctx, stepDeps),
|
|
803
|
+
timeoutPromise,
|
|
804
|
+
]);
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
if (timer)
|
|
808
|
+
clearTimeout(timer);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
result = await executeStep(step, ctx, stepDeps);
|
|
813
|
+
}
|
|
814
|
+
// Detect tool-level errors reported as JSON {ok: false, error: ...}
|
|
815
|
+
if (result !== null) {
|
|
306
816
|
try {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
817
|
+
const parsed = JSON.parse(result);
|
|
818
|
+
if (parsed.ok === false && typeof parsed.error === "string") {
|
|
819
|
+
stepError = parsed.error;
|
|
820
|
+
}
|
|
310
821
|
}
|
|
311
822
|
catch {
|
|
312
|
-
|
|
823
|
+
/* non-JSON result is fine */
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Silent-fail detection: tools that return string placeholders
|
|
827
|
+
// (`(git branches unavailable)`, `[agent step skipped: ...]`)
|
|
828
|
+
// or empty list-tool error shapes (`{count:0,error:"..."}`)
|
|
829
|
+
// succeed with bad data — flag them as `error` so the runner
|
|
830
|
+
// doesn't quietly hand garbage to a downstream agent. Per-step
|
|
831
|
+
// opt-out via `silentFailDetection: false`.
|
|
832
|
+
if (!stepError &&
|
|
833
|
+
result !== null &&
|
|
834
|
+
step.silentFailDetection !== false) {
|
|
835
|
+
const detected = detectSilentFail(result);
|
|
836
|
+
if (detected) {
|
|
837
|
+
stepError = `silent-fail detected (${detected.reason}): ${detected.matched}`;
|
|
313
838
|
}
|
|
314
|
-
outputs.push(intoKey);
|
|
315
|
-
stepResults.push({
|
|
316
|
-
id: stepId,
|
|
317
|
-
tool: "agent",
|
|
318
|
-
status: "ok",
|
|
319
|
-
durationMs: Date.now() - stepStart,
|
|
320
|
-
});
|
|
321
839
|
}
|
|
322
840
|
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
thrownError = err instanceof Error ? err.message : String(err);
|
|
843
|
+
// Preserve structured error codes (e.g. recipe_path_jail_escape)
|
|
844
|
+
// so callers and tests can branch on `err.code` per R2 M-4
|
|
845
|
+
// without scraping the message string.
|
|
846
|
+
const code = err?.code;
|
|
847
|
+
if (typeof code === "string")
|
|
848
|
+
thrownErrorCode = code;
|
|
849
|
+
result = null;
|
|
850
|
+
}
|
|
851
|
+
if (!stepError && !thrownError)
|
|
852
|
+
break;
|
|
323
853
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
854
|
+
// Recipe-level fallback: log_only / deliver_original treat step failure
|
|
855
|
+
// as non-fatal (fail-open) — same semantics as step-level optional: true.
|
|
856
|
+
const fallback = recipe.on_error?.fallback;
|
|
857
|
+
const fallbackFailOpen = fallback === "log_only" || fallback === "deliver_original";
|
|
858
|
+
const failOpen = step.optional === true || fallbackFailOpen;
|
|
859
|
+
if (thrownError) {
|
|
860
|
+
const retryNote = retryCount > 0 ? ` after ${retryCount + 1} attempts` : "";
|
|
327
861
|
stepResults.push({
|
|
328
862
|
id: stepId,
|
|
329
|
-
tool:
|
|
863
|
+
tool: step.tool,
|
|
330
864
|
status: "error",
|
|
331
|
-
error:
|
|
865
|
+
error: thrownError,
|
|
866
|
+
...(thrownErrorCode ? { errorCode: thrownErrorCode } : {}),
|
|
867
|
+
haltReason: `Tool "${step.tool ?? "?"}" in step "${stepId}" threw${retryNote}: ${thrownError}`,
|
|
868
|
+
haltCategory: thrownErrorCode === "kill_switch_blocked"
|
|
869
|
+
? "kill_switch"
|
|
870
|
+
: "tool_threw",
|
|
332
871
|
durationMs: Date.now() - stepStart,
|
|
333
872
|
});
|
|
873
|
+
if (!failOpen) {
|
|
874
|
+
runError = runError ?? `${step.tool} failed: ${thrownError}`;
|
|
875
|
+
}
|
|
876
|
+
else if (fallbackFailOpen && !step.optional) {
|
|
877
|
+
console.warn(`step ${stepId} failed but on_error.fallback=${fallback} — treating as non-fatal: ${thrownError}`);
|
|
878
|
+
}
|
|
334
879
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
880
|
+
else {
|
|
881
|
+
const finalStatus = result === null ? "skipped" : stepError ? "error" : "ok";
|
|
882
|
+
const retryNote = retryCount > 0 ? ` after ${retryCount + 1} attempts` : "";
|
|
883
|
+
stepResults.push({
|
|
884
|
+
id: stepId,
|
|
885
|
+
tool: step.tool,
|
|
886
|
+
status: finalStatus,
|
|
887
|
+
error: stepError,
|
|
888
|
+
...(finalStatus === "error" && stepError
|
|
889
|
+
? {
|
|
890
|
+
haltReason: `Tool "${step.tool ?? "?"}" in step "${stepId}" reported an error${retryNote}: ${stepError}`,
|
|
891
|
+
haltCategory: "tool_error",
|
|
892
|
+
}
|
|
893
|
+
: {}),
|
|
894
|
+
durationMs: Date.now() - stepStart,
|
|
895
|
+
});
|
|
896
|
+
if (stepError) {
|
|
897
|
+
if (!failOpen) {
|
|
898
|
+
runError = runError ?? `${step.tool} failed: ${stepError}`;
|
|
899
|
+
}
|
|
900
|
+
else if (fallbackFailOpen && !step.optional) {
|
|
901
|
+
console.warn(`step ${stepId} failed but on_error.fallback=${fallback} — treating as non-fatal: ${stepError}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
350
904
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
result = await executeStep(step, ctx, stepDeps);
|
|
356
|
-
// Detect tool-level errors reported as JSON {ok: false, error: ...}
|
|
357
|
-
if (result !== null) {
|
|
905
|
+
stepsRun++;
|
|
906
|
+
if (result !== null) {
|
|
907
|
+
// Apply transform if present — render template with $result injected
|
|
908
|
+
if (step.transform) {
|
|
358
909
|
try {
|
|
359
|
-
|
|
360
|
-
if (parsed.ok === false && typeof parsed.error === "string") {
|
|
361
|
-
stepError = parsed.error;
|
|
362
|
-
}
|
|
910
|
+
result = render(step.transform, { ...ctx, $result: result });
|
|
363
911
|
}
|
|
364
|
-
catch {
|
|
365
|
-
|
|
912
|
+
catch (err) {
|
|
913
|
+
// warn but fall through with original result
|
|
914
|
+
console.warn(`transform failed for step ${step.into ?? step.tool ?? "?"}: ${err}`);
|
|
366
915
|
}
|
|
367
916
|
}
|
|
368
|
-
//
|
|
369
|
-
// (
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
917
|
+
// Slice 2 — per-step expect eval. Runs on the post-transform value
|
|
918
|
+
// (what would land in ctx) and only when the step otherwise succeeded.
|
|
919
|
+
// Halt failure flips the just-pushed result to error and suppresses
|
|
920
|
+
// the ctx commit by nulling `result` so the downstream `if (step.into)`
|
|
921
|
+
// block skips. Composes with `optional: true` / `on_error.fallback`.
|
|
922
|
+
if (step.expect && !thrownError && !stepError && result !== null) {
|
|
923
|
+
const failures = await evaluateStepExpect(step.expect, result);
|
|
924
|
+
if (failures.length > 0) {
|
|
925
|
+
const onFail = step.expect.on_fail ?? "halt";
|
|
926
|
+
const last = stepResults[stepResults.length - 1];
|
|
927
|
+
if (last) {
|
|
928
|
+
if (onFail === "halt") {
|
|
929
|
+
last.status = "error";
|
|
930
|
+
last.error = `expect_failed: ${failures.join("; ")}`;
|
|
931
|
+
last.haltReason = `expect_failed in step "${stepId}": ${failures.join("; ")}`;
|
|
932
|
+
last.haltCategory = "expect_failed";
|
|
933
|
+
if (!failOpen) {
|
|
934
|
+
runError = runError ?? last.haltReason;
|
|
935
|
+
}
|
|
936
|
+
result = null;
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
last.expectWarnings = failures;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
380
942
|
}
|
|
381
943
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// without scraping the message string.
|
|
388
|
-
const code = err?.code;
|
|
389
|
-
if (typeof code === "string")
|
|
390
|
-
thrownErrorCode = code;
|
|
391
|
-
result = null;
|
|
392
|
-
}
|
|
393
|
-
if (!stepError && !thrownError)
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
// Recipe-level fallback: log_only / deliver_original treat step failure
|
|
397
|
-
// as non-fatal (fail-open) — same semantics as step-level optional: true.
|
|
398
|
-
const fallback = recipe.on_error?.fallback;
|
|
399
|
-
const fallbackFailOpen = fallback === "log_only" || fallback === "deliver_original";
|
|
400
|
-
const failOpen = step.optional === true || fallbackFailOpen;
|
|
401
|
-
if (thrownError) {
|
|
402
|
-
stepResults.push({
|
|
403
|
-
id: stepId,
|
|
404
|
-
tool: step.tool,
|
|
405
|
-
status: "error",
|
|
406
|
-
error: thrownError,
|
|
407
|
-
...(thrownErrorCode ? { errorCode: thrownErrorCode } : {}),
|
|
408
|
-
durationMs: Date.now() - stepStart,
|
|
409
|
-
});
|
|
410
|
-
if (!failOpen) {
|
|
411
|
-
runError = runError ?? `${step.tool} failed: ${thrownError}`;
|
|
412
|
-
}
|
|
413
|
-
else if (fallbackFailOpen && !step.optional) {
|
|
414
|
-
console.warn(`step ${stepId} failed but on_error.fallback=${fallback} — treating as non-fatal: ${thrownError}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
stepResults.push({
|
|
419
|
-
id: stepId,
|
|
420
|
-
tool: step.tool,
|
|
421
|
-
status: result === null ? "skipped" : stepError ? "error" : "ok",
|
|
422
|
-
error: stepError,
|
|
423
|
-
durationMs: Date.now() - stepStart,
|
|
424
|
-
});
|
|
425
|
-
if (stepError) {
|
|
426
|
-
if (!failOpen) {
|
|
427
|
-
runError = runError ?? `${step.tool} failed: ${stepError}`;
|
|
944
|
+
if (result !== null && step.into) {
|
|
945
|
+
ctx[step.into] = result;
|
|
946
|
+
if (step.tool) {
|
|
947
|
+
applyToolOutputContext(step.tool, step.into, result, ctx);
|
|
948
|
+
}
|
|
428
949
|
}
|
|
429
|
-
|
|
430
|
-
|
|
950
|
+
if (step.tool === "file.write" || step.tool === "file.append") {
|
|
951
|
+
// R2 C-1 / F-02: re-validate the rendered path against the jail so a
|
|
952
|
+
// template substitution that survived earlier checks (e.g. via a
|
|
953
|
+
// chained sub-recipe deps override) cannot smuggle an out-of-jail
|
|
954
|
+
// path into the run log / dashboard outputs list.
|
|
955
|
+
const renderedPath = render(step.path, ctx);
|
|
956
|
+
outputs.push(resolveRecipePath(renderedPath, {
|
|
957
|
+
workspace: stepDeps.workdir,
|
|
958
|
+
write: true,
|
|
959
|
+
}));
|
|
431
960
|
}
|
|
432
961
|
}
|
|
962
|
+
persistLiveStepResults();
|
|
963
|
+
emitStepDone(stepIdForEmit);
|
|
433
964
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (step.tool === "file.write" || step.tool === "file.append") {
|
|
453
|
-
// R2 C-1 / F-02: re-validate the rendered path against the jail so a
|
|
454
|
-
// template substitution that survived earlier checks (e.g. via a
|
|
455
|
-
// chained sub-recipe deps override) cannot smuggle an out-of-jail
|
|
456
|
-
// path into the run log / dashboard outputs list.
|
|
457
|
-
const renderedPath = render(step.path, ctx);
|
|
458
|
-
outputs.push(resolveRecipePath(renderedPath, {
|
|
459
|
-
workspace: stepDeps.workdir,
|
|
460
|
-
write: true,
|
|
461
|
-
}));
|
|
462
|
-
}
|
|
965
|
+
}
|
|
966
|
+
catch (err) {
|
|
967
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
968
|
+
runError = runError ?? `recipe run aborted: ${msg}`;
|
|
969
|
+
}
|
|
970
|
+
// Evaluate expect block before persisting so failures are stored in the
|
|
971
|
+
// run log. Guarded: a throw here must not skip finalization and strand
|
|
972
|
+
// the run at "running".
|
|
973
|
+
let assertionFailures = [];
|
|
974
|
+
if (recipe.expect) {
|
|
975
|
+
try {
|
|
976
|
+
assertionFailures = evaluateExpect({ stepsRun, outputs, context: ctx, errorMessage: runError }, recipe.expect);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
980
|
+
runError = runError ?? `expect evaluation failed: ${msg}`;
|
|
463
981
|
}
|
|
464
982
|
}
|
|
465
|
-
// Evaluate expect block before persisting so failures are stored in the run log
|
|
466
|
-
const assertionFailures = recipe.expect
|
|
467
|
-
? evaluateExpect({ stepsRun, outputs, context: ctx, errorMessage: runError }, recipe.expect)
|
|
468
|
-
: [];
|
|
469
983
|
// Write to RecipeRunLog so the dashboard Runs page shows this execution.
|
|
470
984
|
// Bridge path: completeRun on the running entry opened above (live-tail).
|
|
471
985
|
// CLI path: construct a local log + appendDirect (no live-tail).
|
|
@@ -481,6 +995,9 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
481
995
|
tool: s.tool,
|
|
482
996
|
status: s.status,
|
|
483
997
|
error: s.error,
|
|
998
|
+
...(s.haltReason ? { haltReason: s.haltReason } : {}),
|
|
999
|
+
...(s.haltCategory ? { haltCategory: s.haltCategory } : {}),
|
|
1000
|
+
...(s.judgeVerdict ? { judgeVerdict: s.judgeVerdict } : {}),
|
|
484
1001
|
durationMs: s.durationMs,
|
|
485
1002
|
}));
|
|
486
1003
|
if (deps.runLog && runSeq !== undefined) {
|
|
@@ -492,6 +1009,23 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
492
1009
|
outputTail,
|
|
493
1010
|
...(runError !== undefined && { errorMessage: runError }),
|
|
494
1011
|
...(assertionFailures.length > 0 ? { assertionFailures } : {}),
|
|
1012
|
+
...(inboxOutputs.length > 0 ? { inboxOutputs } : {}),
|
|
1013
|
+
});
|
|
1014
|
+
emit("recipe_done", {
|
|
1015
|
+
runSeq,
|
|
1016
|
+
recipeName: recipe.name,
|
|
1017
|
+
status: runError ? "error" : "done",
|
|
1018
|
+
durationMs: doneAt - recipeStartedAt,
|
|
1019
|
+
stepCount: finalStepResults.length,
|
|
1020
|
+
// A `done` run can still carry step errors — the runner
|
|
1021
|
+
// continues past a non-fatal step failure. Surface it so
|
|
1022
|
+
// live consumers can show "completed with errors".
|
|
1023
|
+
hadStepErrors: finalStepResults.some((s) => s.status === "error"),
|
|
1024
|
+
...(runError !== undefined && { errorMessage: runError }),
|
|
1025
|
+
...(assertionFailures.length > 0 && {
|
|
1026
|
+
assertionFailureCount: assertionFailures.length,
|
|
1027
|
+
}),
|
|
1028
|
+
ts: doneAt,
|
|
495
1029
|
});
|
|
496
1030
|
}
|
|
497
1031
|
else {
|
|
@@ -512,6 +1046,7 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
512
1046
|
errorMessage: runError,
|
|
513
1047
|
stepResults: finalStepResults,
|
|
514
1048
|
...(assertionFailures.length > 0 ? { assertionFailures } : {}),
|
|
1049
|
+
...(inboxOutputs.length > 0 ? { inboxOutputs } : {}),
|
|
515
1050
|
});
|
|
516
1051
|
}
|
|
517
1052
|
}
|
|
@@ -519,13 +1054,13 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
519
1054
|
// Non-fatal — run log write failure should never break recipe execution
|
|
520
1055
|
}
|
|
521
1056
|
}
|
|
522
|
-
// Notify via Slack if any step failed
|
|
523
|
-
if (runError && !stepDeps.testMode) {
|
|
1057
|
+
// Notify via Slack if any step failed and on_error.notify is not explicitly disabled
|
|
1058
|
+
if (runError && !stepDeps.testMode && recipe.on_error?.notify !== false) {
|
|
524
1059
|
try {
|
|
525
1060
|
const { isConnected, postMessage } = await import("../connectors/slack.js");
|
|
526
1061
|
if (isConnected()) {
|
|
527
|
-
// Read notification channel from ~/.patchwork/config.json
|
|
528
|
-
let notifyChannel = "
|
|
1062
|
+
// Read notification channel from ~/.patchwork/config.json
|
|
1063
|
+
let notifyChannel = "";
|
|
529
1064
|
try {
|
|
530
1065
|
const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
|
|
531
1066
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
@@ -535,13 +1070,15 @@ export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
|
535
1070
|
}
|
|
536
1071
|
}
|
|
537
1072
|
catch {
|
|
538
|
-
/*
|
|
1073
|
+
/* config unreadable — skip notification */
|
|
1074
|
+
}
|
|
1075
|
+
if (notifyChannel) {
|
|
1076
|
+
const failedSteps = stepResults
|
|
1077
|
+
.filter((s) => s.status === "error")
|
|
1078
|
+
.map((s) => `• ${s.tool ?? s.id}: ${s.error ?? "unknown error"}`)
|
|
1079
|
+
.join("\n");
|
|
1080
|
+
await postMessage(notifyChannel, `⚠️ *Recipe failed: ${recipe.name}*\n\n${failedSteps}\n\n_${new Date().toISOString()}_`);
|
|
539
1081
|
}
|
|
540
|
-
const failedSteps = stepResults
|
|
541
|
-
.filter((s) => s.status === "error")
|
|
542
|
-
.map((s) => `• ${s.tool ?? s.id}: ${s.error ?? "unknown error"}`)
|
|
543
|
-
.join("\n");
|
|
544
|
-
await postMessage(notifyChannel, `⚠️ *Recipe failed: ${recipe.name}*\n\n${failedSteps}\n\n_${new Date().toISOString()}_`);
|
|
545
1082
|
}
|
|
546
1083
|
}
|
|
547
1084
|
catch {
|
|
@@ -566,11 +1103,19 @@ export async function executeStep(step, ctx, deps) {
|
|
|
566
1103
|
// Check if tool is registered in the new registry
|
|
567
1104
|
if (hasTool(toolId)) {
|
|
568
1105
|
const tool = getTool(toolId);
|
|
569
|
-
// Build params with template rendering for string values
|
|
1106
|
+
// Build params with template rendering for string values.
|
|
1107
|
+
// `do` is left raw: it carries a nested sub-step template (used by
|
|
1108
|
+
// `fan_out`) whose `{{item.*}}` placeholders must be rendered per-iter
|
|
1109
|
+
// with the loop variable in scope, not pre-rendered against the outer
|
|
1110
|
+
// ctx (which would resolve them to empty strings).
|
|
570
1111
|
const params = {};
|
|
571
1112
|
for (const [key, value] of Object.entries(step)) {
|
|
572
1113
|
if (key === "tool" || key === "agent" || key === "into")
|
|
573
1114
|
continue;
|
|
1115
|
+
if (key === "do") {
|
|
1116
|
+
params[key] = value;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
574
1119
|
params[key] = deepRender(value, ctx);
|
|
575
1120
|
}
|
|
576
1121
|
// Check if mock connector is available for this tool
|
|
@@ -612,7 +1157,7 @@ export function render(template, ctx) {
|
|
|
612
1157
|
return "";
|
|
613
1158
|
if (typeof val === "string") {
|
|
614
1159
|
try {
|
|
615
|
-
val = JSON.parse(val);
|
|
1160
|
+
val = sanitizeParsed(JSON.parse(val));
|
|
616
1161
|
}
|
|
617
1162
|
catch {
|
|
618
1163
|
return "";
|
|
@@ -752,7 +1297,7 @@ export function defaultGitStaleBranches(days, workdir) {
|
|
|
752
1297
|
}
|
|
753
1298
|
}
|
|
754
1299
|
/** Resolve all RunnerDeps to concrete StepDeps with production defaults filled in. */
|
|
755
|
-
function resolveStepDeps(deps) {
|
|
1300
|
+
function resolveStepDeps(deps, scope) {
|
|
756
1301
|
const workdir = deps.workdir ?? process.cwd();
|
|
757
1302
|
// Defense-in-depth: even if a file.* tool somehow forgets to call
|
|
758
1303
|
// resolveRecipePath in its execute(), the default StepDeps file ops will
|
|
@@ -810,19 +1355,44 @@ function resolveStepDeps(deps) {
|
|
|
810
1355
|
}),
|
|
811
1356
|
logDir: deps.logDir,
|
|
812
1357
|
testMode: deps.testMode ?? false,
|
|
1358
|
+
// PR5a/b: per-attempt idempotency ledger. Disk-backed when
|
|
1359
|
+
// `ledgerDir` + `manualRunId` + recipe name are all available so a
|
|
1360
|
+
// retry of the same logical attempt re-uses prior records (resume
|
|
1361
|
+
// semantics). Falls back to pure in-memory dedup otherwise.
|
|
1362
|
+
writeEffectLedger: deps.ledgerDir && deps.manualRunId && scope?.recipeName
|
|
1363
|
+
? new WriteEffectLedger({
|
|
1364
|
+
dir: deps.ledgerDir,
|
|
1365
|
+
// Hash both fields together — `${recipeName}:${manualRunId}`
|
|
1366
|
+
// is ambiguous when recipe names contain colons (which
|
|
1367
|
+
// RecipeRunLog.parseTrigger explicitly allows).
|
|
1368
|
+
scopeKey: deriveScopeKey(scope.recipeName, assertValidManualRunId(deps.manualRunId)),
|
|
1369
|
+
})
|
|
1370
|
+
: new WriteEffectLedger(),
|
|
813
1371
|
};
|
|
814
1372
|
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Normalise the union return of a RunnerDeps caller into an `AgentResult`.
|
|
1375
|
+
* Test mocks / CLI overrides typically return a plain string; bridge
|
|
1376
|
+
* wrappers + real adapter paths return `{text, usage}` so PR2b's token
|
|
1377
|
+
* budget enforcer can read usage. Both shapes converge here.
|
|
1378
|
+
*/
|
|
1379
|
+
function toAgentResult(v) {
|
|
1380
|
+
return typeof v === "string" ? { text: v } : v;
|
|
1381
|
+
}
|
|
815
1382
|
function buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride) {
|
|
816
1383
|
const claudeCliFn = claudeCodeFnOverride ?? stepDeps.claudeCodeFn;
|
|
817
1384
|
return {
|
|
818
|
-
anthropicFn: (prompt, model) => stepDeps.claudeFn(prompt, model),
|
|
819
|
-
providerDriverFn: (driver, prompt, model) => stepDeps.providerDriverFn(driver, prompt, model),
|
|
820
|
-
claudeCliFn: (prompt, opts) => claudeCliFn(prompt, opts),
|
|
821
|
-
localFn: (prompt, model) => stepDeps.localFn(prompt, model),
|
|
1385
|
+
anthropicFn: async (prompt, model) => toAgentResult(await stepDeps.claudeFn(prompt, model)),
|
|
1386
|
+
providerDriverFn: async (driver, prompt, model) => toAgentResult(await stepDeps.providerDriverFn(driver, prompt, model)),
|
|
1387
|
+
claudeCliFn: async (prompt, opts) => toAgentResult(await claudeCliFn(prompt, opts)),
|
|
1388
|
+
localFn: async (prompt, model) => toAgentResult(await stepDeps.localFn(prompt, model)),
|
|
822
1389
|
probeClaudeCli: () => {
|
|
823
1390
|
if (runnerDeps.claudeFn !== undefined)
|
|
824
1391
|
return false;
|
|
825
|
-
|
|
1392
|
+
// Use the same resolution as defaultClaudeCodeFn so the auto-detect
|
|
1393
|
+
// branch in agentExecutor.ts doesn't probe "claude" via PATH and
|
|
1394
|
+
// then later fail to spawn the configured override (or vice versa).
|
|
1395
|
+
const probe = spawnSync(resolveClaudeBinary(), ["--version"], {
|
|
826
1396
|
encoding: "utf-8",
|
|
827
1397
|
timeout: 5000,
|
|
828
1398
|
});
|
|
@@ -841,21 +1411,78 @@ function buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride) {
|
|
|
841
1411
|
},
|
|
842
1412
|
};
|
|
843
1413
|
}
|
|
844
|
-
|
|
1414
|
+
/**
|
|
1415
|
+
* Resolve the `claude` binary path with override precedence:
|
|
1416
|
+
* 1. PATCHWORK_CLAUDE_BINARY env var (set by the bridge LaunchAgent
|
|
1417
|
+
* or any wrapper script)
|
|
1418
|
+
* 2. `~/.patchwork/config.json` `claudeBinary` field
|
|
1419
|
+
* 3. plain `"claude"` (PATH lookup — pre-existing default)
|
|
1420
|
+
*
|
|
1421
|
+
* Resolved per-call, not memoised, so config edits + env-var changes
|
|
1422
|
+
* take effect on the next agent step without a bridge restart.
|
|
1423
|
+
*/
|
|
1424
|
+
export function resolveClaudeBinary() {
|
|
1425
|
+
const envOverride = process.env.PATCHWORK_CLAUDE_BINARY;
|
|
1426
|
+
if (envOverride && envOverride.length > 0)
|
|
1427
|
+
return ensureCmdShim(envOverride);
|
|
845
1428
|
try {
|
|
846
|
-
const
|
|
1429
|
+
const cfg = loadPatchworkConfigSync();
|
|
1430
|
+
if (cfg.claudeBinary && cfg.claudeBinary.length > 0)
|
|
1431
|
+
return ensureCmdShim(cfg.claudeBinary);
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
// ignore — fall through to the "claude" default
|
|
1435
|
+
}
|
|
1436
|
+
return ensureCmdShim("claude");
|
|
1437
|
+
}
|
|
1438
|
+
export function defaultClaudeCodeFn(prompt, opts) {
|
|
1439
|
+
const binary = resolveClaudeBinary();
|
|
1440
|
+
// Resolve a workspace cwd so the spawned `claude -p` doesn't inherit the
|
|
1441
|
+
// bridge LaunchAgent's `$HOME` (P2 from the 2026-05-20 research run).
|
|
1442
|
+
// When nothing resolves, surface a typed reason instead of silently
|
|
1443
|
+
// shelling out from the wrong directory.
|
|
1444
|
+
const workspace = resolveWorkspaceRoot();
|
|
1445
|
+
if (!workspace) {
|
|
1446
|
+
return Promise.resolve(`[agent step failed: recipe_no_workspace — no .git ancestor of "${process.cwd()}" and PATCHWORK_WORKSPACE not set. Set PATCHWORK_WORKSPACE in the bridge environment or add a 'workspace:' field to the recipe.]`);
|
|
1447
|
+
}
|
|
1448
|
+
// mcpAccess is plumbed through executeAgent → buildChainedDeps → here.
|
|
1449
|
+
// The default fn has no bridge MCP endpoint resolver (SubprocessDriver
|
|
1450
|
+
// owns that). Surface mcpAccess=true as a typed error rather than
|
|
1451
|
+
// silently falling back to no-MCP spawn — the recipe explicitly asked
|
|
1452
|
+
// for bridge tools and should be routed through SubprocessDriver via
|
|
1453
|
+
// the runtime injector instead.
|
|
1454
|
+
if (opts?.mcpAccess === true) {
|
|
1455
|
+
return Promise.resolve("[agent step failed: recipe_mcp_unsupported — defaultClaudeCodeFn does not support mcpAccess:true; route via SubprocessDriver or unset the mcpAccess flag on this step]");
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
const result = spawnSync(binary, [
|
|
847
1459
|
"-p",
|
|
848
1460
|
prompt,
|
|
1461
|
+
// --strict-mcp-config: never load ~/.claude.json or .mcp.json. Recipes
|
|
1462
|
+
// are sandboxed by default (mcpAccess defaults to false above). This
|
|
1463
|
+
// also prevents accidental session attachment when the parent process
|
|
1464
|
+
// had a bridge MCP entry in ~/.claude.json.
|
|
1465
|
+
"--strict-mcp-config",
|
|
849
1466
|
"--system-prompt",
|
|
850
1467
|
"You are a helpful assistant processing a recipe task. Use ONLY the data explicitly provided in the user message — treat it as ground truth. Do not call tools to look up git history, emails, or any other information; all necessary data is already included.",
|
|
851
1468
|
"--no-session-persistence",
|
|
852
1469
|
], {
|
|
1470
|
+
cwd: workspace.path,
|
|
1471
|
+
// sanitizeEnv strips CLAUDECODE / CLAUDE_CODE_* / MCP_* from the
|
|
1472
|
+
// child so the spawn doesn't re-authenticate as, or nest under,
|
|
1473
|
+
// the parent Claude Code session. Mirrors SubprocessDriver.run
|
|
1474
|
+
// hygiene. Preserves CLAUDE_CODE_OAUTH_TOKEN (subscription auth).
|
|
1475
|
+
env: sanitizeEnv(process.env),
|
|
853
1476
|
encoding: "utf-8",
|
|
854
|
-
timeout:
|
|
1477
|
+
timeout: 600_000,
|
|
855
1478
|
maxBuffer: 10 * 1024 * 1024,
|
|
856
1479
|
});
|
|
857
1480
|
if (result.error) {
|
|
858
|
-
|
|
1481
|
+
// Surface the configured binary path in the error so users diagnosing
|
|
1482
|
+
// ENOENT can see whether resolveClaudeBinary picked up their override.
|
|
1483
|
+
// Hint includes the env var + config field names so the fix is one
|
|
1484
|
+
// click away.
|
|
1485
|
+
return Promise.resolve(`[agent step failed: claude CLI not found at "${binary}" — install Claude Code, set PATCHWORK_CLAUDE_BINARY, or set ANTHROPIC_API_KEY]`);
|
|
859
1486
|
}
|
|
860
1487
|
if (result.status !== 0) {
|
|
861
1488
|
return Promise.resolve(`[agent step failed: claude exited ${result.status}: ${result.stderr?.slice(0, 200) ?? ""}]`);
|
|
@@ -884,10 +1511,11 @@ function makeProviderDriverFn() {
|
|
|
884
1511
|
const timeoutMs = 300_000;
|
|
885
1512
|
const startupTimeoutMs = 30_000;
|
|
886
1513
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1514
|
+
const resolvedWorkspace = process.cwd();
|
|
887
1515
|
try {
|
|
888
1516
|
const result = await driver.run({
|
|
889
1517
|
prompt,
|
|
890
|
-
workspace:
|
|
1518
|
+
workspace: resolvedWorkspace,
|
|
891
1519
|
timeoutMs,
|
|
892
1520
|
startupTimeoutMs,
|
|
893
1521
|
signal: controller.signal,
|
|
@@ -914,7 +1542,7 @@ function makeProviderDriverFn() {
|
|
|
914
1542
|
async function defaultClaudeFn(prompt, model) {
|
|
915
1543
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
916
1544
|
if (!apiKey)
|
|
917
|
-
return "[agent step skipped: ANTHROPIC_API_KEY not set]";
|
|
1545
|
+
return { text: "[agent step skipped: ANTHROPIC_API_KEY not set]" };
|
|
918
1546
|
try {
|
|
919
1547
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
920
1548
|
method: "POST",
|
|
@@ -936,13 +1564,25 @@ async function defaultClaudeFn(prompt, model) {
|
|
|
936
1564
|
});
|
|
937
1565
|
if (!res.ok) {
|
|
938
1566
|
const text = await res.text().catch(() => res.statusText);
|
|
939
|
-
return `[agent step failed: ${text}]
|
|
1567
|
+
return { text: `[agent step failed: ${text}]` };
|
|
940
1568
|
}
|
|
1569
|
+
// PR2a: forward Anthropic API token counts so PR2b's RunBudget can
|
|
1570
|
+
// reconcile actual consumption. Optional both upstream (older API
|
|
1571
|
+
// versions) and downstream (subscription/CLI driver returns
|
|
1572
|
+
// undefined here).
|
|
941
1573
|
const data = (await res.json());
|
|
942
|
-
|
|
1574
|
+
const text = data.content?.[0]?.text ?? "[agent step failed: empty response]";
|
|
1575
|
+
const inputTokens = data.usage?.input_tokens;
|
|
1576
|
+
const outputTokens = data.usage?.output_tokens;
|
|
1577
|
+
if (typeof inputTokens === "number" && typeof outputTokens === "number") {
|
|
1578
|
+
return { text, usage: { inputTokens, outputTokens } };
|
|
1579
|
+
}
|
|
1580
|
+
return { text };
|
|
943
1581
|
}
|
|
944
1582
|
catch (err) {
|
|
945
|
-
return
|
|
1583
|
+
return {
|
|
1584
|
+
text: `[agent step failed: ${err instanceof Error ? err.message : String(err)}]`,
|
|
1585
|
+
};
|
|
946
1586
|
}
|
|
947
1587
|
}
|
|
948
1588
|
async function defaultLocalFn(prompt, model) {
|
|
@@ -958,10 +1598,18 @@ async function defaultLocalFn(prompt, model) {
|
|
|
958
1598
|
systemPrompt: "",
|
|
959
1599
|
messages: [{ role: "user", content: prompt }],
|
|
960
1600
|
});
|
|
961
|
-
|
|
1601
|
+
const text = result.text ?? "[agent step failed: empty response from local LLM]";
|
|
1602
|
+
// PR2a: local adapters carry usage when the backing API (Ollama / LM
|
|
1603
|
+
// Studio) surfaces it; otherwise undefined.
|
|
1604
|
+
if (result.usage) {
|
|
1605
|
+
return { text, usage: result.usage };
|
|
1606
|
+
}
|
|
1607
|
+
return { text };
|
|
962
1608
|
}
|
|
963
1609
|
catch (err) {
|
|
964
|
-
return
|
|
1610
|
+
return {
|
|
1611
|
+
text: `[agent step failed: ${err instanceof Error ? err.message : String(err)}]`,
|
|
1612
|
+
};
|
|
965
1613
|
}
|
|
966
1614
|
}
|
|
967
1615
|
/**
|
|
@@ -1017,12 +1665,18 @@ export function buildChainedDeps(runnerDeps, claudeCodeFnOverride) {
|
|
|
1017
1665
|
const result = await executeStep(step, {}, stepDeps);
|
|
1018
1666
|
return result ?? "";
|
|
1019
1667
|
};
|
|
1020
|
-
const executeAgent = async (prompt, model, driver, mcpAccess) =>
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1668
|
+
const executeAgent = async (prompt, model, driver, mcpAccess) => {
|
|
1669
|
+
// chainedRunner's AgentExecutor contract still returns a plain string —
|
|
1670
|
+
// PR2b's token-budget consumer will plug in here as well, but for now
|
|
1671
|
+
// we discard `.usage`.
|
|
1672
|
+
const result = await _executeAgent({
|
|
1673
|
+
prompt,
|
|
1674
|
+
model,
|
|
1675
|
+
driver,
|
|
1676
|
+
...(mcpAccess !== undefined && { mcpAccess }),
|
|
1677
|
+
}, buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride));
|
|
1678
|
+
return result.text;
|
|
1679
|
+
};
|
|
1026
1680
|
// ---------------------------------------------------------------------
|
|
1027
1681
|
// BEGIN A-PR2 EDIT BLOCK — `loadNestedRecipe` jail (dogfood F-04).
|
|
1028
1682
|
//
|
|
@@ -1139,7 +1793,7 @@ export async function dispatchRecipe(recipe, deps, seedContext = {}) {
|
|
|
1139
1793
|
TIME: now.toTimeString().slice(0, 5),
|
|
1140
1794
|
...seedContext,
|
|
1141
1795
|
},
|
|
1142
|
-
maxConcurrency: chainedRecipe.maxConcurrency ?? 4,
|
|
1796
|
+
maxConcurrency: Math.max(1, chainedRecipe.maxConcurrency ?? 4),
|
|
1143
1797
|
maxDepth: chainedRecipe.maxDepth ?? 3,
|
|
1144
1798
|
dryRun: deps.chainedOptions?.dryRun ?? false,
|
|
1145
1799
|
sourcePath: deps.chainedOptions?.sourcePath,
|
|
@@ -1156,11 +1810,17 @@ export async function dispatchRecipe(recipe, deps, seedContext = {}) {
|
|
|
1156
1810
|
}
|
|
1157
1811
|
return runChainedRecipe(chainedRecipe, options, deps.chainedDeps);
|
|
1158
1812
|
}
|
|
1159
|
-
// For non-chained recipes, lift `runLog`
|
|
1160
|
-
// RunnerDeps so runYamlRecipe gets the
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1813
|
+
// For non-chained recipes, lift `runLog` AND `activityLog` from
|
|
1814
|
+
// chainedOptions onto the RunnerDeps so runYamlRecipe gets the
|
|
1815
|
+
// bridge's singletons too. The activityLog is what powers
|
|
1816
|
+
// recipe_started / recipe_step_start / recipe_step_done /
|
|
1817
|
+
// recipe_done SSE emission to dashboard subscribers.
|
|
1818
|
+
const lifted = { ...deps };
|
|
1819
|
+
if (deps.chainedOptions?.runLog)
|
|
1820
|
+
lifted.runLog = deps.chainedOptions.runLog;
|
|
1821
|
+
if (deps.chainedOptions?.activityLog)
|
|
1822
|
+
lifted.activityLog = deps.chainedOptions.activityLog;
|
|
1823
|
+
return runYamlRecipe(recipe, lifted, seedContext);
|
|
1164
1824
|
}
|
|
1165
1825
|
/** List all YAML recipes in a directory. Returns names. */
|
|
1166
1826
|
export function listYamlRecipes(recipesDir) {
|