patchwork-os 0.2.0-alpha.8 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.bridge.md +6 -0
- package/README.md +315 -35
- package/deploy/bootstrap-new-vps.sh +12 -12
- package/deploy/bootstrap-vps.sh +187 -0
- package/deploy/deploy-dashboard.sh +174 -0
- package/deploy/deploy-landing.sh +136 -0
- package/dist/activationMetrics.d.ts +67 -0
- package/dist/activationMetrics.js +255 -0
- package/dist/activationMetrics.js.map +1 -0
- package/dist/activityLog.d.ts +49 -0
- package/dist/activityLog.js +78 -0
- package/dist/activityLog.js.map +1 -1
- package/dist/approvalHttp.d.ts +49 -2
- package/dist/approvalHttp.js +217 -21
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalInsights.d.ts +49 -0
- package/dist/approvalInsights.js +97 -0
- package/dist/approvalInsights.js.map +1 -0
- package/dist/approvalQueue.d.ts +27 -1
- package/dist/approvalQueue.js +123 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/approvalSignals.d.ts +124 -0
- package/dist/approvalSignals.js +512 -0
- package/dist/approvalSignals.js.map +1 -0
- package/dist/automation.d.ts +57 -0
- package/dist/automation.js +156 -59
- package/dist/automation.js.map +1 -1
- package/dist/automationSuggestions.d.ts +79 -0
- package/dist/automationSuggestions.js +150 -0
- package/dist/automationSuggestions.js.map +1 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/bridge.js +174 -143
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/ccPermissions.d.ts +15 -0
- package/dist/ccPermissions.js +21 -4
- package/dist/ccPermissions.js.map +1 -1
- package/dist/claudeDriver.js +74 -16
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +14 -8
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/launchd.d.ts +2 -0
- package/dist/commands/launchd.js +94 -0
- package/dist/commands/launchd.js.map +1 -0
- package/dist/commands/patchworkInit.d.ts +8 -0
- package/dist/commands/patchworkInit.js +77 -11
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +289 -0
- package/dist/commands/recipe.js +1359 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipeInstall.d.ts +150 -0
- package/dist/commands/recipeInstall.js +647 -0
- package/dist/commands/recipeInstall.js.map +1 -0
- package/dist/commands/tracesExport.d.ts +83 -0
- package/dist/commands/tracesExport.js +269 -0
- package/dist/commands/tracesExport.js.map +1 -0
- package/dist/commands/tracesImport.d.ts +56 -0
- package/dist/commands/tracesImport.js +161 -0
- package/dist/commands/tracesImport.js.map +1 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +108 -9
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.d.ts +43 -0
- package/dist/connectorRoutes.js +1609 -0
- package/dist/connectorRoutes.js.map +1 -0
- package/dist/connectors/asana.d.ts +198 -0
- package/dist/connectors/asana.js +679 -0
- package/dist/connectors/asana.js.map +1 -0
- package/dist/connectors/baseConnector.d.ts +153 -0
- package/dist/connectors/baseConnector.js +336 -0
- package/dist/connectors/baseConnector.js.map +1 -0
- package/dist/connectors/confluence.d.ts +111 -0
- package/dist/connectors/confluence.js +406 -0
- package/dist/connectors/confluence.js.map +1 -0
- package/dist/connectors/datadog.d.ts +116 -0
- package/dist/connectors/datadog.js +385 -0
- package/dist/connectors/datadog.js.map +1 -0
- package/dist/connectors/discord.d.ts +150 -0
- package/dist/connectors/discord.js +543 -0
- package/dist/connectors/discord.js.map +1 -0
- package/dist/connectors/fixtureLibrary.d.ts +21 -0
- package/dist/connectors/fixtureLibrary.js +70 -0
- package/dist/connectors/fixtureLibrary.js.map +1 -0
- package/dist/connectors/fixtureRecorder.d.ts +1 -0
- package/dist/connectors/fixtureRecorder.js +35 -0
- package/dist/connectors/fixtureRecorder.js.map +1 -0
- package/dist/connectors/github.js +17 -18
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gitlab.d.ts +180 -0
- package/dist/connectors/gitlab.js +582 -0
- package/dist/connectors/gitlab.js.map +1 -0
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +157 -27
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +4 -1
- package/dist/connectors/googleCalendar.js +88 -25
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/googleDrive.d.ts +34 -0
- package/dist/connectors/googleDrive.js +321 -0
- package/dist/connectors/googleDrive.js.map +1 -0
- package/dist/connectors/htmlEscape.d.ts +5 -0
- package/dist/connectors/htmlEscape.js +13 -0
- package/dist/connectors/htmlEscape.js.map +1 -0
- package/dist/connectors/hubspot.d.ts +112 -0
- package/dist/connectors/hubspot.js +408 -0
- package/dist/connectors/hubspot.js.map +1 -0
- package/dist/connectors/intercom.d.ts +102 -0
- package/dist/connectors/intercom.js +402 -0
- package/dist/connectors/intercom.js.map +1 -0
- package/dist/connectors/jira.d.ts +98 -0
- package/dist/connectors/jira.js +379 -0
- package/dist/connectors/jira.js.map +1 -0
- package/dist/connectors/linear.js +30 -19
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.d.ts +3 -0
- package/dist/connectors/mcpOAuth.js +64 -10
- package/dist/connectors/mcpOAuth.js.map +1 -1
- package/dist/connectors/mockConnector.d.ts +28 -0
- package/dist/connectors/mockConnector.js +81 -0
- package/dist/connectors/mockConnector.js.map +1 -0
- package/dist/connectors/notion.d.ts +143 -0
- package/dist/connectors/notion.js +424 -0
- package/dist/connectors/notion.js.map +1 -0
- package/dist/connectors/oauthStateStore.d.ts +31 -0
- package/dist/connectors/oauthStateStore.js +52 -0
- package/dist/connectors/oauthStateStore.js.map +1 -0
- package/dist/connectors/pagerduty.d.ts +160 -0
- package/dist/connectors/pagerduty.js +464 -0
- package/dist/connectors/pagerduty.js.map +1 -0
- package/dist/connectors/sentry.js +5 -13
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/connectors/slack.d.ts +16 -1
- package/dist/connectors/slack.js +155 -32
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/stripe.d.ts +116 -0
- package/dist/connectors/stripe.js +379 -0
- package/dist/connectors/stripe.js.map +1 -0
- package/dist/connectors/tokenStorage.d.ts +35 -0
- package/dist/connectors/tokenStorage.js +484 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +442 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/cors.d.ts +10 -0
- package/dist/cors.js +29 -0
- package/dist/cors.js.map +1 -0
- package/dist/decisionReplay.d.ts +72 -0
- package/dist/decisionReplay.js +92 -0
- package/dist/decisionReplay.js.map +1 -0
- package/dist/decisionTraceLog.d.ts +6 -0
- package/dist/decisionTraceLog.js +54 -2
- package/dist/decisionTraceLog.js.map +1 -1
- package/dist/drivers/gemini/index.d.ts +5 -1
- package/dist/drivers/gemini/index.js +39 -5
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/featureFlags.d.ts +79 -0
- package/dist/featureFlags.js +208 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +26 -21
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +1 -1
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/automationState.js +4 -1
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/policyParser.js +21 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +114 -0
- package/dist/inboxRoutes.js.map +1 -0
- package/dist/index.js +1400 -201
- package/dist/index.js.map +1 -1
- package/dist/installGuard.d.ts +25 -0
- package/dist/installGuard.js +48 -0
- package/dist/installGuard.js.map +1 -0
- package/dist/mcpRoutes.d.ts +37 -0
- package/dist/mcpRoutes.js +76 -0
- package/dist/mcpRoutes.js.map +1 -0
- package/dist/oauth.d.ts +7 -1
- package/dist/oauth.js +201 -39
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.d.ts +32 -0
- package/dist/oauthRoutes.js +124 -0
- package/dist/oauthRoutes.js.map +1 -0
- package/dist/orchestrator/orchestratorBridge.js +2 -2
- package/dist/orchestrator/orchestratorBridge.js.map +1 -1
- package/dist/patchworkConfig.d.ts +16 -0
- package/dist/patchworkConfig.js +1 -1
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.d.ts +28 -0
- package/dist/pluginLoader.js +77 -11
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +8 -3
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.d.ts +12 -0
- package/dist/preToolUseHook.js +23 -0
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +121 -0
- package/dist/recipeOrchestration.js +955 -0
- package/dist/recipeOrchestration.js.map +1 -0
- package/dist/recipeRoutes.d.ts +180 -0
- package/dist/recipeRoutes.js +1345 -0
- package/dist/recipeRoutes.js.map +1 -0
- package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
- package/dist/recipes/RecipeOrchestrator.js +51 -0
- package/dist/recipes/RecipeOrchestrator.js.map +1 -0
- package/dist/recipes/agentExecutor.d.ts +29 -0
- package/dist/recipes/agentExecutor.js +49 -0
- package/dist/recipes/agentExecutor.js.map +1 -0
- package/dist/recipes/chainedRunner.d.ts +191 -0
- package/dist/recipes/chainedRunner.js +759 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/compiler.js +3 -3
- package/dist/recipes/compiler.js.map +1 -1
- package/dist/recipes/dependencyGraph.d.ts +39 -0
- package/dist/recipes/dependencyGraph.js +199 -0
- package/dist/recipes/dependencyGraph.js.map +1 -0
- package/dist/recipes/disabledMarkers.d.ts +48 -0
- package/dist/recipes/disabledMarkers.js +52 -0
- package/dist/recipes/disabledMarkers.js.map +1 -0
- package/dist/recipes/installer.js +3 -3
- package/dist/recipes/installer.js.map +1 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
- package/dist/recipes/legacyRecipeCompat.js +131 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/manifest.d.ts +47 -0
- package/dist/recipes/manifest.js +156 -0
- package/dist/recipes/manifest.js.map +1 -0
- package/dist/recipes/migrationWarnings.d.ts +12 -0
- package/dist/recipes/migrationWarnings.js +44 -0
- package/dist/recipes/migrationWarnings.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +24 -0
- package/dist/recipes/migrations/index.js +55 -0
- package/dist/recipes/migrations/index.js.map +1 -0
- package/dist/recipes/migrations/types.d.ts +28 -0
- package/dist/recipes/migrations/types.js +2 -0
- package/dist/recipes/migrations/types.js.map +1 -0
- package/dist/recipes/migrations/v1.d.ts +11 -0
- package/dist/recipes/migrations/v1.js +18 -0
- package/dist/recipes/migrations/v1.js.map +1 -0
- package/dist/recipes/names.d.ts +40 -0
- package/dist/recipes/names.js +66 -0
- package/dist/recipes/names.js.map +1 -0
- package/dist/recipes/nestedRecipeStep.d.ts +58 -0
- package/dist/recipes/nestedRecipeStep.js +95 -0
- package/dist/recipes/nestedRecipeStep.js.map +1 -0
- package/dist/recipes/outputRegistry.d.ts +28 -0
- package/dist/recipes/outputRegistry.js +52 -0
- package/dist/recipes/outputRegistry.js.map +1 -0
- package/dist/recipes/parser.js +4 -1
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/replayRun.d.ts +62 -0
- package/dist/recipes/replayRun.js +97 -0
- package/dist/recipes/replayRun.js.map +1 -0
- package/dist/recipes/resolveRecipePath.d.ts +69 -0
- package/dist/recipes/resolveRecipePath.js +202 -0
- package/dist/recipes/resolveRecipePath.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +225 -45
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +17 -2
- package/dist/recipes/schemaGenerator.d.ts +28 -0
- package/dist/recipes/schemaGenerator.js +565 -0
- package/dist/recipes/schemaGenerator.js.map +1 -0
- package/dist/recipes/stepObservation.d.ts +44 -0
- package/dist/recipes/stepObservation.js +232 -0
- package/dist/recipes/stepObservation.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +201 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +186 -0
- package/dist/recipes/toolRegistry.js +309 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/asana.d.ts +16 -0
- package/dist/recipes/tools/asana.js +524 -0
- package/dist/recipes/tools/asana.js.map +1 -0
- package/dist/recipes/tools/calendar.d.ts +6 -0
- package/dist/recipes/tools/calendar.js +61 -0
- package/dist/recipes/tools/calendar.js.map +1 -0
- package/dist/recipes/tools/confluence.d.ts +6 -0
- package/dist/recipes/tools/confluence.js +254 -0
- package/dist/recipes/tools/confluence.js.map +1 -0
- package/dist/recipes/tools/datadog.d.ts +6 -0
- package/dist/recipes/tools/datadog.js +239 -0
- package/dist/recipes/tools/datadog.js.map +1 -0
- package/dist/recipes/tools/diagnostics.d.ts +6 -0
- package/dist/recipes/tools/diagnostics.js +36 -0
- package/dist/recipes/tools/diagnostics.js.map +1 -0
- package/dist/recipes/tools/discord.d.ts +18 -0
- package/dist/recipes/tools/discord.js +254 -0
- package/dist/recipes/tools/discord.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +12 -0
- package/dist/recipes/tools/file.js +174 -0
- package/dist/recipes/tools/file.js.map +1 -0
- package/dist/recipes/tools/git.d.ts +6 -0
- package/dist/recipes/tools/git.js +63 -0
- package/dist/recipes/tools/git.js.map +1 -0
- package/dist/recipes/tools/github.d.ts +6 -0
- package/dist/recipes/tools/github.js +116 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gitlab.d.ts +11 -0
- package/dist/recipes/tools/gitlab.js +285 -0
- package/dist/recipes/tools/gitlab.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +434 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/googleDrive.d.ts +1 -0
- package/dist/recipes/tools/googleDrive.js +55 -0
- package/dist/recipes/tools/googleDrive.js.map +1 -0
- package/dist/recipes/tools/hubspot.d.ts +6 -0
- package/dist/recipes/tools/hubspot.js +232 -0
- package/dist/recipes/tools/hubspot.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +30 -0
- package/dist/recipes/tools/index.js +33 -0
- package/dist/recipes/tools/index.js.map +1 -0
- package/dist/recipes/tools/intercom.d.ts +6 -0
- package/dist/recipes/tools/intercom.js +226 -0
- package/dist/recipes/tools/intercom.js.map +1 -0
- package/dist/recipes/tools/jira.d.ts +14 -0
- package/dist/recipes/tools/jira.js +369 -0
- package/dist/recipes/tools/jira.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +7 -0
- package/dist/recipes/tools/linear.js +307 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/meetingNotes.d.ts +21 -0
- package/dist/recipes/tools/meetingNotes.js +701 -0
- package/dist/recipes/tools/meetingNotes.js.map +1 -0
- package/dist/recipes/tools/notion.d.ts +6 -0
- package/dist/recipes/tools/notion.js +278 -0
- package/dist/recipes/tools/notion.js.map +1 -0
- package/dist/recipes/tools/pagerduty.d.ts +15 -0
- package/dist/recipes/tools/pagerduty.js +451 -0
- package/dist/recipes/tools/pagerduty.js.map +1 -0
- package/dist/recipes/tools/sentry.d.ts +12 -0
- package/dist/recipes/tools/sentry.js +73 -0
- package/dist/recipes/tools/sentry.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +82 -0
- package/dist/recipes/tools/slack.js.map +1 -0
- package/dist/recipes/tools/stripe.d.ts +6 -0
- package/dist/recipes/tools/stripe.js +265 -0
- package/dist/recipes/tools/stripe.js.map +1 -0
- package/dist/recipes/tools/zendesk.d.ts +6 -0
- package/dist/recipes/tools/zendesk.js +245 -0
- package/dist/recipes/tools/zendesk.js.map +1 -0
- package/dist/recipes/validation.d.ts +13 -0
- package/dist/recipes/validation.js +617 -0
- package/dist/recipes/validation.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +116 -1
- package/dist/recipes/yamlRunner.js +1000 -401
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +137 -6
- package/dist/recipesHttp.js +941 -29
- package/dist/recipesHttp.js.map +1 -1
- package/dist/riskTier.js +7 -1
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +100 -1
- package/dist/runLog.js +258 -5
- package/dist/runLog.js.map +1 -1
- package/dist/schemas/dry-run-plan.v1.json +139 -0
- package/dist/schemas/recipe.v1.json +684 -0
- package/dist/server.d.ts +121 -8
- package/dist/server.js +538 -735
- package/dist/server.js.map +1 -1
- package/dist/ssrfGuard.d.ts +54 -0
- package/dist/ssrfGuard.js +122 -0
- package/dist/ssrfGuard.js.map +1 -0
- package/dist/streamableHttp.d.ts +39 -1
- package/dist/streamableHttp.js +128 -17
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tokenUsageTracker.d.ts +33 -0
- package/dist/tokenUsageTracker.js +146 -0
- package/dist/tokenUsageTracker.js.map +1 -0
- package/dist/tools/activityLog.d.ts +2 -0
- package/dist/tools/addLinearComment.d.ts +1 -0
- package/dist/tools/addLinearComment.js +4 -2
- package/dist/tools/addLinearComment.js.map +1 -1
- package/dist/tools/batchLsp.d.ts +3 -0
- package/dist/tools/bridgeDoctor.d.ts +1 -0
- package/dist/tools/bridgeDoctor.js +2 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/bridgeStatus.d.ts +1 -0
- package/dist/tools/cancelClaudeTask.d.ts +2 -0
- package/dist/tools/cancelClaudeTask.js +1 -0
- package/dist/tools/cancelClaudeTask.js.map +1 -1
- package/dist/tools/checkDocumentDirty.d.ts +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/closeTabs.d.ts +2 -0
- package/dist/tools/codeLens.d.ts +1 -0
- package/dist/tools/contextBundle.d.ts +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +1 -0
- package/dist/tools/createLinearIssue.d.ts +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +1 -0
- package/dist/tools/ctxQueryTraces.d.ts +1 -0
- 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/documentLinks.d.ts +1 -0
- package/dist/tools/editText.d.ts +1 -0
- package/dist/tools/enrichCommit.d.ts +1 -0
- package/dist/tools/enrichStackTrace.d.ts +1 -0
- package/dist/tools/explainDiagnostic.d.ts +1 -0
- package/dist/tools/explainSymbol.d.ts +1 -0
- package/dist/tools/fetchCalendarEvents.d.ts +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +1 -0
- package/dist/tools/fetchGithubPR.d.ts +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.js +4 -1
- package/dist/tools/fetchSlackProfile.js.map +1 -1
- package/dist/tools/fileOperations.d.ts +3 -0
- package/dist/tools/fileWatcher.d.ts +2 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findRelatedTests.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +1 -0
- package/dist/tools/foldingRanges.d.ts +1 -0
- package/dist/tools/formatDocument.d.ts +1 -0
- package/dist/tools/generateTests.d.ts +1 -0
- package/dist/tools/getAIComments.d.ts +1 -0
- package/dist/tools/getAnalyticsReport.d.ts +1 -0
- package/dist/tools/getArchitectureContext.d.ts +1 -0
- package/dist/tools/getBufferContent.d.ts +1 -0
- package/dist/tools/getChangeImpact.d.ts +1 -0
- package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
- package/dist/tools/getClaudeTaskStatus.js +1 -0
- package/dist/tools/getClaudeTaskStatus.js.map +1 -1
- package/dist/tools/getCodeCoverage.d.ts +1 -0
- package/dist/tools/getCommitsForIssue.d.ts +1 -0
- package/dist/tools/getConnectorStatus.d.ts +1 -0
- package/dist/tools/getCurrentSelection.d.ts +2 -0
- package/dist/tools/getDebugState.d.ts +1 -0
- package/dist/tools/getDependencyTree.d.ts +1 -0
- package/dist/tools/getDiagnostics.d.ts +1 -0
- package/dist/tools/getDiffFromHandoff.d.ts +1 -0
- package/dist/tools/getDocumentSymbols.d.ts +25 -0
- package/dist/tools/getDocumentSymbols.js +74 -8
- package/dist/tools/getDocumentSymbols.js.map +1 -1
- package/dist/tools/getFileTree.d.ts +1 -0
- package/dist/tools/getGitDiff.d.ts +1 -0
- package/dist/tools/getGitHotspots.d.ts +1 -0
- package/dist/tools/getGitLog.d.ts +1 -0
- package/dist/tools/getGitStatus.d.ts +1 -0
- package/dist/tools/getImportTree.d.ts +1 -0
- package/dist/tools/getImportedSignatures.d.ts +1 -0
- package/dist/tools/getOpenEditors.d.ts +1 -0
- package/dist/tools/getPRTemplate.d.ts +1 -0
- package/dist/tools/getProjectContext.d.ts +1 -0
- package/dist/tools/getProjectInfo.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.js +10 -1
- package/dist/tools/getSecurityAdvisories.js.map +1 -1
- package/dist/tools/getSessionUsage.d.ts +4 -0
- package/dist/tools/getSessionUsage.js +3 -0
- package/dist/tools/getSessionUsage.js.map +1 -1
- package/dist/tools/getSymbolHistory.d.ts +1 -0
- package/dist/tools/getToolCapabilities.d.ts +1 -0
- package/dist/tools/getTypeSignature.d.ts +1 -0
- package/dist/tools/getWorkspaceFolders.d.ts +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +1 -0
- package/dist/tools/gitHistory.d.ts +2 -0
- package/dist/tools/gitWrite.d.ts +11 -0
- package/dist/tools/github/actions.d.ts +2 -0
- package/dist/tools/github/actions.js +4 -2
- package/dist/tools/github/actions.js.map +1 -1
- package/dist/tools/github/composite.d.ts +342 -0
- package/dist/tools/github/composite.js +343 -0
- package/dist/tools/github/composite.js.map +1 -0
- package/dist/tools/github/index.d.ts +1 -0
- package/dist/tools/github/index.js +1 -0
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/issues.d.ts +4 -0
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.d.ts +7 -0
- package/dist/tools/github/pr.js +50 -12
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/handoffNote.d.ts +4 -0
- package/dist/tools/handoffNote.js +2 -0
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/hoverAtCursor.d.ts +1 -0
- package/dist/tools/httpClient.d.ts +2 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +47 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inlayHints.d.ts +1 -0
- package/dist/tools/launchQuickTask.d.ts +2 -0
- package/dist/tools/launchQuickTask.js +1 -0
- package/dist/tools/launchQuickTask.js.map +1 -1
- package/dist/tools/listClaudeTasks.d.ts +2 -0
- package/dist/tools/listClaudeTasks.js +1 -0
- package/dist/tools/listClaudeTasks.js.map +1 -1
- package/dist/tools/listTerminals.d.ts +1 -0
- package/dist/tools/lsp.d.ts +14 -0
- package/dist/tools/navigateToSymbolByName.d.ts +1 -0
- package/dist/tools/openDiff.d.ts +1 -0
- package/dist/tools/openFile.d.ts +1 -0
- package/dist/tools/openInBrowser.d.ts +1 -0
- package/dist/tools/organizeImports.d.ts +1 -0
- package/dist/tools/performanceReport.d.ts +1 -0
- package/dist/tools/planPersistence.d.ts +5 -0
- package/dist/tools/previewEdit.d.ts +1 -0
- package/dist/tools/refactorAnalyze.d.ts +1 -0
- package/dist/tools/refactorPreview.d.ts +2 -0
- package/dist/tools/refactorPreview.js +1 -0
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.d.ts +1 -0
- package/dist/tools/resumeClaudeTask.d.ts +2 -0
- package/dist/tools/resumeClaudeTask.js +1 -0
- package/dist/tools/resumeClaudeTask.js.map +1 -1
- package/dist/tools/runClaudeTask.d.ts +2 -0
- package/dist/tools/runClaudeTask.js +1 -0
- package/dist/tools/runClaudeTask.js.map +1 -1
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runTests.d.ts +1 -0
- package/dist/tools/saveDocument.d.ts +1 -0
- package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
- package/dist/tools/searchAndReplace.d.ts +1 -0
- package/dist/tools/searchTools.d.ts +1 -0
- package/dist/tools/searchTools.js +1 -1
- 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/setActiveWorkspaceFolder.d.ts +1 -0
- package/dist/tools/signatureHelp.d.ts +1 -0
- package/dist/tools/slackListChannels.d.ts +1 -0
- package/dist/tools/slackListChannels.js.map +1 -1
- package/dist/tools/slackPostMessage.d.ts +1 -0
- package/dist/tools/slackPostMessage.js +11 -6
- package/dist/tools/slackPostMessage.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/testTraceToSource.d.ts +1 -0
- package/dist/tools/testTraceToSource.js +2 -2
- package/dist/tools/testTraceToSource.js.map +1 -1
- package/dist/tools/transaction.d.ts +23 -0
- package/dist/tools/transaction.js +29 -0
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/updateLinearIssue.d.ts +1 -0
- package/dist/tools/updateLinearIssue.js +20 -6
- package/dist/tools/updateLinearIssue.js.map +1 -1
- package/dist/tools/utils.d.ts +2 -0
- 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/traceEncryption.d.ts +46 -0
- package/dist/traceEncryption.js +124 -0
- package/dist/traceEncryption.js.map +1 -0
- package/dist/transport.d.ts +46 -1
- package/dist/transport.js +173 -19
- package/dist/transport.js.map +1 -1
- package/package.json +30 -8
- package/scripts/mcp-stdio-shim.cjs +19 -3
- package/scripts/start-all.sh +30 -1
- package/templates/automation-policies/recipe-authoring.json +25 -0
- package/templates/automation-policy.example.json +6 -0
- package/templates/co.patchwork-os.bridge.plist +34 -0
- package/templates/policies/README.md +72 -0
- package/templates/policies/conservative.json +14 -0
- package/templates/policies/developer.json +14 -0
- package/templates/policies/headless-ci.json +24 -0
- package/templates/policies/personal-assistant.json +15 -0
- package/templates/policies/regulated-industry.json +18 -0
- package/templates/recipes/lint-on-save.yaml +1 -2
- package/templates/recipes/morning-brief-slack.yaml +57 -0
- package/templates/recipes/morning-brief.yaml +2 -2
- package/templates/recipes/project-health-check.yaml +50 -0
- package/templates/recipes/webhook/README.md +70 -0
- package/templates/recipes/webhook/capture-thought.yaml +26 -0
- package/templates/recipes/webhook/customer-escalation.yaml +49 -0
- package/templates/recipes/webhook/incident-intake.yaml +46 -0
- package/templates/recipes/webhook/meeting-prep.yaml +48 -0
- package/templates/recipes/webhook/morning-brief.yaml +57 -0
|
@@ -24,17 +24,110 @@ import { spawnSync } from "node:child_process";
|
|
|
24
24
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
27
28
|
import { parse as parseYaml } from "yaml";
|
|
29
|
+
import { captureFixture } from "../connectors/fixtureRecorder.js";
|
|
30
|
+
import { loadConfig as loadPatchworkConfigSync } from "../patchworkConfig.js";
|
|
31
|
+
import { findYamlRecipePath } from "../recipesHttp.js";
|
|
32
|
+
import { executeAgent as _executeAgent, } from "./agentExecutor.js";
|
|
33
|
+
import { defaultDeprecationWarn, normalizeRecipeForRuntime, } from "./legacyRecipeCompat.js";
|
|
34
|
+
import { resolveRecipePath } from "./resolveRecipePath.js";
|
|
35
|
+
import { detectSilentFail } from "./stepObservation.js";
|
|
36
|
+
// Import tool registry and trigger tool self-registration
|
|
37
|
+
import { applyToolOutputContext, executeTool, getTool, hasTool, registerPluginTools, } from "./toolRegistry.js";
|
|
38
|
+
import "./tools/index.js";
|
|
39
|
+
/**
|
|
40
|
+
* Bundled-templates directory used as a third allowed root for nested-recipe
|
|
41
|
+
* lookups (`recipe:` references with explicit paths). Resolved once at module
|
|
42
|
+
* load — `__dirname` equivalent points at `dist/recipes/` in the npm tarball
|
|
43
|
+
* (or `src/recipes/` in dev) so the relative `../../templates/recipes` lifts
|
|
44
|
+
* out of the source tree to the package root regardless of build layout.
|
|
45
|
+
*
|
|
46
|
+
* See dogfood A-PR2 / R2 M-5 — the third jail root is captured here, not at
|
|
47
|
+
* call time, so a runtime CWD change cannot relocate it.
|
|
48
|
+
*/
|
|
49
|
+
const BUNDLED_TEMPLATES_DIR = (() => {
|
|
50
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
// dist/recipes/yamlRunner.js → ../../templates/recipes
|
|
52
|
+
// src/recipes/yamlRunner.ts → ../../templates/recipes
|
|
53
|
+
return path.resolve(here, "..", "..", "templates", "recipes");
|
|
54
|
+
})();
|
|
55
|
+
export function evaluateExpect(result, expect) {
|
|
56
|
+
const failures = [];
|
|
57
|
+
if (expect.stepsRun !== undefined && result.stepsRun !== expect.stepsRun) {
|
|
58
|
+
failures.push({
|
|
59
|
+
assertion: "stepsRun",
|
|
60
|
+
expected: expect.stepsRun,
|
|
61
|
+
actual: result.stepsRun,
|
|
62
|
+
message: `Expected stepsRun=${expect.stepsRun}, got ${result.stepsRun}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (expect.errorMessage !== undefined) {
|
|
66
|
+
const expected = expect.errorMessage ?? null;
|
|
67
|
+
const actual = result.errorMessage ?? null;
|
|
68
|
+
if (expected !== actual) {
|
|
69
|
+
failures.push({
|
|
70
|
+
assertion: "errorMessage",
|
|
71
|
+
expected,
|
|
72
|
+
actual,
|
|
73
|
+
message: expected === null
|
|
74
|
+
? `Expected clean run (no error), got: ${actual}`
|
|
75
|
+
: `Expected error "${expected}", got: ${actual === null ? "(none)" : actual}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (expect.outputs !== undefined) {
|
|
80
|
+
for (const key of expect.outputs) {
|
|
81
|
+
if (!result.outputs.includes(key)) {
|
|
82
|
+
failures.push({
|
|
83
|
+
assertion: "outputs",
|
|
84
|
+
expected: key,
|
|
85
|
+
actual: result.outputs,
|
|
86
|
+
message: `Expected output key "${key}" not found in [${result.outputs.join(", ")}]`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (expect.context !== undefined) {
|
|
92
|
+
for (const [key, expectedVal] of Object.entries(expect.context)) {
|
|
93
|
+
const actual = result.context[key];
|
|
94
|
+
if (actual === undefined) {
|
|
95
|
+
failures.push({
|
|
96
|
+
assertion: `context.${key}`,
|
|
97
|
+
expected: expectedVal,
|
|
98
|
+
actual: undefined,
|
|
99
|
+
message: `Expected context key "${key}" to equal "${expectedVal}", but key is missing`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (!actual.includes(expectedVal)) {
|
|
103
|
+
failures.push({
|
|
104
|
+
assertion: `context.${key}`,
|
|
105
|
+
expected: expectedVal,
|
|
106
|
+
actual,
|
|
107
|
+
message: `Expected context["${key}"] to contain "${expectedVal}", got "${actual}"`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return failures;
|
|
113
|
+
}
|
|
114
|
+
// Strip tool-call narration some models (e.g. Gemini) prepend before the markdown block.
|
|
115
|
+
function stripLeadingNarration(text) {
|
|
116
|
+
const lines = text.split("\n");
|
|
117
|
+
const firstMarkdown = lines.findIndex((l) => /^(#|>|`|\||[-*+] |\d+\. |\*\*)/.test(l.trimStart()));
|
|
118
|
+
return firstMarkdown > 0 ? lines.slice(firstMarkdown).join("\n") : text;
|
|
119
|
+
}
|
|
28
120
|
export function loadYamlRecipe(filePath) {
|
|
29
121
|
const text = readFileSync(filePath, "utf-8");
|
|
30
122
|
const raw = parseYaml(text);
|
|
31
123
|
return validateYamlRecipe(raw);
|
|
32
124
|
}
|
|
33
125
|
export function validateYamlRecipe(raw) {
|
|
34
|
-
|
|
126
|
+
const normalized = normalizeRecipeForRuntime(raw, defaultDeprecationWarn);
|
|
127
|
+
if (typeof normalized !== "object" || normalized === null) {
|
|
35
128
|
throw new Error("recipe must be an object");
|
|
36
129
|
}
|
|
37
|
-
const r =
|
|
130
|
+
const r = normalized;
|
|
38
131
|
if (typeof r.name !== "string" || !r.name) {
|
|
39
132
|
throw new Error("recipe.name required");
|
|
40
133
|
}
|
|
@@ -44,124 +137,706 @@ export function validateYamlRecipe(raw) {
|
|
|
44
137
|
if (!Array.isArray(r.steps) || r.steps.length === 0) {
|
|
45
138
|
throw new Error("recipe.steps must be a non-empty array");
|
|
46
139
|
}
|
|
140
|
+
if (r.servers !== undefined &&
|
|
141
|
+
(!Array.isArray(r.servers) ||
|
|
142
|
+
r.servers.some((s) => typeof s !== "string"))) {
|
|
143
|
+
throw new Error("recipe.servers must be an array of strings if present");
|
|
144
|
+
}
|
|
47
145
|
return r;
|
|
48
146
|
}
|
|
147
|
+
/** Track already-loaded plugin specs to avoid double-loading within a process. */
|
|
148
|
+
const loadedPluginSpecs = new Set();
|
|
149
|
+
/**
|
|
150
|
+
* Load plugin specs declared in `recipe.servers` and register their tools into
|
|
151
|
+
* the recipe tool registry. Errors per-spec are logged as warnings — never fatal.
|
|
152
|
+
*/
|
|
153
|
+
export async function loadRecipeServers(specs) {
|
|
154
|
+
const toLoad = specs.filter((s) => !loadedPluginSpecs.has(s));
|
|
155
|
+
if (toLoad.length === 0)
|
|
156
|
+
return;
|
|
157
|
+
let loadPluginsFull;
|
|
158
|
+
try {
|
|
159
|
+
({ loadPluginsFull } = await import("../pluginLoader.js"));
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.warn(`[recipe servers] failed to import pluginLoader: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const minimalConfig = {
|
|
166
|
+
workspace: process.cwd(),
|
|
167
|
+
workspaceFolders: [process.cwd()],
|
|
168
|
+
commandTimeout: 30_000,
|
|
169
|
+
maxResultSize: 1_048_576,
|
|
170
|
+
};
|
|
171
|
+
const minimalLogger = {
|
|
172
|
+
info: (msg) => console.info(`[recipe servers] ${msg}`),
|
|
173
|
+
warn: (msg) => console.warn(`[recipe servers] ${msg}`),
|
|
174
|
+
error: (msg) => console.error(`[recipe servers] ${msg}`),
|
|
175
|
+
debug: (_msg) => { },
|
|
176
|
+
};
|
|
177
|
+
for (const spec of toLoad) {
|
|
178
|
+
try {
|
|
179
|
+
const loaded = await loadPluginsFull([spec], minimalConfig, minimalLogger);
|
|
180
|
+
let toolCount = 0;
|
|
181
|
+
for (const plugin of loaded) {
|
|
182
|
+
const pluginTools = plugin.tools.map((t) => ({
|
|
183
|
+
name: t.schema.name,
|
|
184
|
+
handler: t.handler,
|
|
185
|
+
schema: t.schema,
|
|
186
|
+
}));
|
|
187
|
+
toolCount += registerPluginTools(pluginTools);
|
|
188
|
+
}
|
|
189
|
+
loadedPluginSpecs.add(spec);
|
|
190
|
+
if (toolCount > 0) {
|
|
191
|
+
console.info(`[recipe servers] loaded "${spec}" — ${toolCount} tool(s) registered`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.warn(`[recipe servers] failed to load "${spec}": ${err instanceof Error ? err.message : String(err)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
49
199
|
export async function runYamlRecipe(recipe, deps = {}, seedContext = {}) {
|
|
200
|
+
if (recipe.servers?.length) {
|
|
201
|
+
await loadRecipeServers(recipe.servers);
|
|
202
|
+
}
|
|
50
203
|
const now = deps.now ? deps.now() : new Date();
|
|
204
|
+
// Resolve recipe-level context blocks (type: env) into seed context
|
|
205
|
+
const envCtx = {};
|
|
206
|
+
if (Array.isArray(recipe.context)) {
|
|
207
|
+
for (const block of recipe
|
|
208
|
+
.context ?? []) {
|
|
209
|
+
const b = block;
|
|
210
|
+
if (b.type === "env" && Array.isArray(b.keys)) {
|
|
211
|
+
for (const key of b.keys) {
|
|
212
|
+
const v = process.env[key];
|
|
213
|
+
if (v !== undefined)
|
|
214
|
+
envCtx[key] = v;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
51
219
|
const ctx = {
|
|
52
220
|
date: now.toISOString().slice(0, 10),
|
|
53
221
|
time: now.toTimeString().slice(0, 5),
|
|
222
|
+
...envCtx,
|
|
54
223
|
...seedContext,
|
|
55
224
|
};
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
225
|
+
const stepDeps = resolveStepDeps(deps);
|
|
226
|
+
// Open a `running`-state run-log entry so the dashboard sees the recipe
|
|
227
|
+
// as in flight. Only when a long-lived `runLog` is provided (bridge path);
|
|
228
|
+
// CLI runs fall back to `appendDirect` at end via the existing logDir
|
|
229
|
+
// path. Skip in test mode.
|
|
230
|
+
const recipeStartedAt = now.getTime();
|
|
231
|
+
const recipeTriggerKind = recipe.trigger?.type ?? "manual";
|
|
232
|
+
const yamlTriggerKind = (["cron", "webhook", "recipe"].includes(recipeTriggerKind)
|
|
233
|
+
? recipeTriggerKind
|
|
234
|
+
: "recipe");
|
|
235
|
+
let runSeq;
|
|
236
|
+
if (deps.runLog && !stepDeps.testMode) {
|
|
237
|
+
try {
|
|
238
|
+
runSeq = deps.runLog.startRun({
|
|
239
|
+
taskId: `yaml:${recipe.name}:${recipeStartedAt}`,
|
|
240
|
+
recipeName: recipe.name,
|
|
241
|
+
trigger: yamlTriggerKind,
|
|
242
|
+
createdAt: recipeStartedAt,
|
|
243
|
+
startedAt: recipeStartedAt,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Non-fatal — run-log failures must never break recipe execution.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
71
250
|
const outputs = [];
|
|
251
|
+
const stepResults = [];
|
|
72
252
|
let stepsRun = 0;
|
|
73
|
-
|
|
74
|
-
const stepDeps = {
|
|
75
|
-
readFile,
|
|
76
|
-
writeFile,
|
|
77
|
-
appendFile,
|
|
78
|
-
mkdir,
|
|
79
|
-
workdir,
|
|
80
|
-
gitLogSince: deps.gitLogSince ?? defaultGitLogSince,
|
|
81
|
-
gitStaleBranches: deps.gitStaleBranches ?? defaultGitStaleBranches,
|
|
82
|
-
getDiagnostics: deps.getDiagnostics ?? (() => ""),
|
|
83
|
-
fetchFn: deps.fetchFn ?? globalThis.fetch,
|
|
84
|
-
claudeFn: deps.claudeFn ?? defaultClaudeFn,
|
|
85
|
-
claudeCodeFn: deps.claudeCodeFn ?? defaultClaudeCodeFn,
|
|
86
|
-
getGmailToken: deps.getGmailToken ??
|
|
87
|
-
(async () => {
|
|
88
|
-
const { getValidAccessToken } = await import("../connectors/gmail.js");
|
|
89
|
-
return getValidAccessToken();
|
|
90
|
-
}),
|
|
91
|
-
};
|
|
253
|
+
let runError;
|
|
92
254
|
for (const step of recipe.steps) {
|
|
93
255
|
// Handle agent steps separately
|
|
94
256
|
if (step.agent) {
|
|
95
257
|
const agentCfg = step.agent;
|
|
96
258
|
const renderedPrompt = render(agentCfg.prompt, ctx);
|
|
97
|
-
const model = agentCfg.model ?? "claude-haiku-4-5-20251001";
|
|
98
259
|
const intoKey = agentCfg.into ?? "agent_output";
|
|
260
|
+
const stepId = intoKey;
|
|
261
|
+
const stepStart = Date.now();
|
|
99
262
|
let agentResult;
|
|
100
|
-
|
|
101
|
-
agentResult = await
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
263
|
+
try {
|
|
264
|
+
agentResult = await _executeAgent({
|
|
265
|
+
prompt: renderedPrompt,
|
|
266
|
+
driver: agentCfg.driver === "api" ? "anthropic" : agentCfg.driver,
|
|
267
|
+
model: agentCfg.model,
|
|
268
|
+
}, buildAgentExecutorDeps(stepDeps, deps));
|
|
269
|
+
// Catch both `[agent step failed: ...]` (existing) and the
|
|
270
|
+
// silent-fail patterns `[agent step skipped: ...]` etc. via the
|
|
271
|
+
// shared detector. Per-step opt-out via `silentFailDetection: false`.
|
|
272
|
+
const agentSilentFail = step.silentFailDetection !== false
|
|
273
|
+
? detectSilentFail(agentResult)
|
|
274
|
+
: null;
|
|
275
|
+
if (agentResult.startsWith("[agent step failed:") || agentSilentFail) {
|
|
276
|
+
const reason = agentSilentFail
|
|
277
|
+
? `silent-fail detected (${agentSilentFail.reason}): ${agentSilentFail.matched}`
|
|
278
|
+
: agentResult;
|
|
279
|
+
runError = runError ?? reason;
|
|
280
|
+
stepResults.push({
|
|
281
|
+
id: stepId,
|
|
282
|
+
tool: "agent",
|
|
283
|
+
status: "error",
|
|
284
|
+
error: reason,
|
|
285
|
+
durationMs: Date.now() - stepStart,
|
|
115
286
|
});
|
|
116
|
-
|
|
117
|
-
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const stripped = stripLeadingNarration(agentResult);
|
|
290
|
+
if (!stripped.trim()) {
|
|
291
|
+
const errMsg = `[agent step failed: ${agentCfg.driver ?? "agent"} returned only narration or whitespace — no content]`;
|
|
292
|
+
runError = runError ?? errMsg;
|
|
293
|
+
stepResults.push({
|
|
294
|
+
id: stepId,
|
|
295
|
+
tool: "agent",
|
|
296
|
+
status: "error",
|
|
297
|
+
error: errMsg,
|
|
298
|
+
durationMs: Date.now() - stepStart,
|
|
299
|
+
});
|
|
118
300
|
}
|
|
119
301
|
else {
|
|
120
|
-
|
|
302
|
+
// Try to parse as JSON so dot-notation ({{meeting.field}}) works
|
|
303
|
+
try {
|
|
304
|
+
const jsonMatch = /```(?:json)?\s*([\s\S]*?)```/.exec(stripped) ?? [null, stripped];
|
|
305
|
+
const parsed = JSON.parse((jsonMatch[1] ?? "").trim());
|
|
306
|
+
ctx[intoKey] = parsed;
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
ctx[intoKey] = stripped;
|
|
310
|
+
}
|
|
311
|
+
outputs.push(intoKey);
|
|
312
|
+
stepResults.push({
|
|
313
|
+
id: stepId,
|
|
314
|
+
tool: "agent",
|
|
315
|
+
status: "ok",
|
|
316
|
+
durationMs: Date.now() - stepStart,
|
|
317
|
+
});
|
|
121
318
|
}
|
|
122
319
|
}
|
|
123
|
-
else {
|
|
124
|
-
agentResult = await stepDeps.claudeFn(renderedPrompt, model);
|
|
125
|
-
}
|
|
126
320
|
}
|
|
127
|
-
|
|
128
|
-
|
|
321
|
+
catch (err) {
|
|
322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
323
|
+
runError = runError ?? `agent step "${stepId}" failed: ${msg}`;
|
|
324
|
+
stepResults.push({
|
|
325
|
+
id: stepId,
|
|
326
|
+
tool: "agent",
|
|
327
|
+
status: "error",
|
|
328
|
+
error: msg,
|
|
329
|
+
durationMs: Date.now() - stepStart,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
129
332
|
stepsRun++;
|
|
130
333
|
continue;
|
|
131
334
|
}
|
|
132
|
-
const
|
|
133
|
-
stepsRun
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
335
|
+
const stepStart = Date.now();
|
|
336
|
+
const stepId = step.into ?? `step_${stepsRun}`;
|
|
337
|
+
// Resolve retry policy: step-level overrides recipe-level.
|
|
338
|
+
const retryCount = step.retry ?? recipe.on_error?.retry ?? 0;
|
|
339
|
+
const retryDelayMs = step.retryDelay ?? recipe.on_error?.retryDelay ?? 1000;
|
|
340
|
+
let result = null;
|
|
341
|
+
let stepError;
|
|
342
|
+
let thrownError;
|
|
343
|
+
let thrownErrorCode;
|
|
344
|
+
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
345
|
+
if (attempt > 0) {
|
|
346
|
+
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
347
|
+
}
|
|
348
|
+
stepError = undefined;
|
|
349
|
+
thrownError = undefined;
|
|
350
|
+
thrownErrorCode = undefined;
|
|
351
|
+
try {
|
|
352
|
+
result = await executeStep(step, ctx, stepDeps);
|
|
353
|
+
// Detect tool-level errors reported as JSON {ok: false, error: ...}
|
|
354
|
+
if (result !== null) {
|
|
142
355
|
try {
|
|
143
356
|
const parsed = JSON.parse(result);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ctx[`${step.into}.${k}`] = String(v);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// Also expose messages array as JSON string for agent prompts
|
|
150
|
-
if (Array.isArray(parsed.messages)) {
|
|
151
|
-
ctx[`${step.into}.json`] = JSON.stringify(parsed.messages);
|
|
357
|
+
if (parsed.ok === false && typeof parsed.error === "string") {
|
|
358
|
+
stepError = parsed.error;
|
|
152
359
|
}
|
|
153
360
|
}
|
|
154
361
|
catch {
|
|
155
|
-
|
|
362
|
+
/* non-JSON result is fine */
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Silent-fail detection: tools that return string placeholders
|
|
366
|
+
// (`(git branches unavailable)`, `[agent step skipped: ...]`)
|
|
367
|
+
// or empty list-tool error shapes (`{count:0,error:"..."}`)
|
|
368
|
+
// succeed with bad data — flag them as `error` so the runner
|
|
369
|
+
// doesn't quietly hand garbage to a downstream agent. Per-step
|
|
370
|
+
// opt-out via `silentFailDetection: false`.
|
|
371
|
+
if (!stepError &&
|
|
372
|
+
result !== null &&
|
|
373
|
+
step.silentFailDetection !== false) {
|
|
374
|
+
const detected = detectSilentFail(result);
|
|
375
|
+
if (detected) {
|
|
376
|
+
stepError = `silent-fail detected (${detected.reason}): ${detected.matched}`;
|
|
156
377
|
}
|
|
157
378
|
}
|
|
158
379
|
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
thrownError = err instanceof Error ? err.message : String(err);
|
|
382
|
+
// Preserve structured error codes (e.g. recipe_path_jail_escape)
|
|
383
|
+
// so callers and tests can branch on `err.code` per R2 M-4
|
|
384
|
+
// without scraping the message string.
|
|
385
|
+
const code = err?.code;
|
|
386
|
+
if (typeof code === "string")
|
|
387
|
+
thrownErrorCode = code;
|
|
388
|
+
result = null;
|
|
389
|
+
}
|
|
390
|
+
if (!stepError && !thrownError)
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
// Recipe-level fallback: log_only / deliver_original treat step failure
|
|
394
|
+
// as non-fatal (fail-open) — same semantics as step-level optional: true.
|
|
395
|
+
const fallback = recipe.on_error?.fallback;
|
|
396
|
+
const fallbackFailOpen = fallback === "log_only" || fallback === "deliver_original";
|
|
397
|
+
const failOpen = step.optional === true || fallbackFailOpen;
|
|
398
|
+
if (thrownError) {
|
|
399
|
+
stepResults.push({
|
|
400
|
+
id: stepId,
|
|
401
|
+
tool: step.tool,
|
|
402
|
+
status: "error",
|
|
403
|
+
error: thrownError,
|
|
404
|
+
...(thrownErrorCode ? { errorCode: thrownErrorCode } : {}),
|
|
405
|
+
durationMs: Date.now() - stepStart,
|
|
406
|
+
});
|
|
407
|
+
if (!failOpen) {
|
|
408
|
+
runError = runError ?? `${step.tool} failed: ${thrownError}`;
|
|
409
|
+
}
|
|
410
|
+
else if (fallbackFailOpen && !step.optional) {
|
|
411
|
+
console.warn(`step ${stepId} failed but on_error.fallback=${fallback} — treating as non-fatal: ${thrownError}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
stepResults.push({
|
|
416
|
+
id: stepId,
|
|
417
|
+
tool: step.tool,
|
|
418
|
+
status: result === null ? "skipped" : stepError ? "error" : "ok",
|
|
419
|
+
error: stepError,
|
|
420
|
+
durationMs: Date.now() - stepStart,
|
|
421
|
+
});
|
|
422
|
+
if (stepError) {
|
|
423
|
+
if (!failOpen) {
|
|
424
|
+
runError = runError ?? `${step.tool} failed: ${stepError}`;
|
|
425
|
+
}
|
|
426
|
+
else if (fallbackFailOpen && !step.optional) {
|
|
427
|
+
console.warn(`step ${stepId} failed but on_error.fallback=${fallback} — treating as non-fatal: ${stepError}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
stepsRun++;
|
|
432
|
+
if (result !== null) {
|
|
433
|
+
// Apply transform if present — render template with $result injected
|
|
434
|
+
if (step.transform) {
|
|
435
|
+
try {
|
|
436
|
+
result = render(step.transform, { ...ctx, $result: result });
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
// warn but fall through with original result
|
|
440
|
+
console.warn(`transform failed for step ${step.into ?? step.tool ?? "?"}: ${err}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (step.into) {
|
|
444
|
+
ctx[step.into] = result;
|
|
445
|
+
if (step.tool) {
|
|
446
|
+
applyToolOutputContext(step.tool, step.into, result, ctx);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
159
449
|
if (step.tool === "file.write" || step.tool === "file.append") {
|
|
160
|
-
|
|
450
|
+
// R2 C-1 / F-02: re-validate the rendered path against the jail so a
|
|
451
|
+
// template substitution that survived earlier checks (e.g. via a
|
|
452
|
+
// chained sub-recipe deps override) cannot smuggle an out-of-jail
|
|
453
|
+
// path into the run log / dashboard outputs list.
|
|
454
|
+
const renderedPath = render(step.path, ctx);
|
|
455
|
+
outputs.push(resolveRecipePath(renderedPath, {
|
|
456
|
+
workspace: stepDeps.workdir,
|
|
457
|
+
write: true,
|
|
458
|
+
}));
|
|
161
459
|
}
|
|
162
460
|
}
|
|
163
461
|
}
|
|
164
|
-
|
|
462
|
+
// Evaluate expect block before persisting so failures are stored in the run log
|
|
463
|
+
const assertionFailures = recipe.expect
|
|
464
|
+
? evaluateExpect({ stepsRun, outputs, context: ctx, errorMessage: runError }, recipe.expect)
|
|
465
|
+
: [];
|
|
466
|
+
// Write to RecipeRunLog so the dashboard Runs page shows this execution.
|
|
467
|
+
// Bridge path: completeRun on the running entry opened above (live-tail).
|
|
468
|
+
// CLI path: construct a local log + appendDirect (no live-tail).
|
|
469
|
+
if (!stepDeps.testMode) {
|
|
470
|
+
try {
|
|
471
|
+
const doneAt = Date.now();
|
|
472
|
+
const outputTail = stepResults
|
|
473
|
+
.map((s) => `[${s.status}] ${s.tool ?? s.id}${s.error ? `: ${s.error}` : ""}`)
|
|
474
|
+
.join("\n")
|
|
475
|
+
.slice(0, 2000);
|
|
476
|
+
const finalStepResults = stepResults.map((s) => ({
|
|
477
|
+
id: s.id,
|
|
478
|
+
tool: s.tool,
|
|
479
|
+
status: s.status,
|
|
480
|
+
error: s.error,
|
|
481
|
+
durationMs: s.durationMs,
|
|
482
|
+
}));
|
|
483
|
+
if (deps.runLog && runSeq !== undefined) {
|
|
484
|
+
deps.runLog.completeRun(runSeq, {
|
|
485
|
+
status: runError ? "error" : "done",
|
|
486
|
+
doneAt,
|
|
487
|
+
durationMs: doneAt - recipeStartedAt,
|
|
488
|
+
stepResults: finalStepResults,
|
|
489
|
+
outputTail,
|
|
490
|
+
...(runError !== undefined && { errorMessage: runError }),
|
|
491
|
+
...(assertionFailures.length > 0 ? { assertionFailures } : {}),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const { RecipeRunLog } = await import("../runLog.js");
|
|
496
|
+
const { homedir } = await import("node:os");
|
|
497
|
+
const resolvedLogDir = deps.logDir ?? path.join(homedir(), ".patchwork");
|
|
498
|
+
const log = new RecipeRunLog({ dir: resolvedLogDir });
|
|
499
|
+
log.appendDirect({
|
|
500
|
+
taskId: `yaml:${recipe.name}:${recipeStartedAt}`,
|
|
501
|
+
recipeName: recipe.name,
|
|
502
|
+
trigger: yamlTriggerKind,
|
|
503
|
+
status: runError ? "error" : "done",
|
|
504
|
+
createdAt: recipeStartedAt,
|
|
505
|
+
startedAt: recipeStartedAt,
|
|
506
|
+
doneAt,
|
|
507
|
+
durationMs: doneAt - recipeStartedAt,
|
|
508
|
+
outputTail,
|
|
509
|
+
errorMessage: runError,
|
|
510
|
+
stepResults: finalStepResults,
|
|
511
|
+
...(assertionFailures.length > 0 ? { assertionFailures } : {}),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// Non-fatal — run log write failure should never break recipe execution
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Notify via Slack if any step failed
|
|
520
|
+
if (runError && !stepDeps.testMode) {
|
|
521
|
+
try {
|
|
522
|
+
const { isConnected, postMessage } = await import("../connectors/slack.js");
|
|
523
|
+
if (isConnected()) {
|
|
524
|
+
// Read notification channel from ~/.patchwork/config.json, fallback to first available
|
|
525
|
+
let notifyChannel = "all-massappealdesigns";
|
|
526
|
+
try {
|
|
527
|
+
const cfgPath = path.join(os.homedir(), ".patchwork", "config.json");
|
|
528
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
529
|
+
const notifications = cfg.notifications;
|
|
530
|
+
if (typeof notifications?.slackChannel === "string") {
|
|
531
|
+
notifyChannel = notifications.slackChannel;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
/* use default */
|
|
536
|
+
}
|
|
537
|
+
const failedSteps = stepResults
|
|
538
|
+
.filter((s) => s.status === "error")
|
|
539
|
+
.map((s) => `• ${s.tool ?? s.id}: ${s.error ?? "unknown error"}`)
|
|
540
|
+
.join("\n");
|
|
541
|
+
await postMessage(notifyChannel, `⚠️ *Recipe failed: ${recipe.name}*\n\n${failedSteps}\n\n_${new Date().toISOString()}_`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// Non-fatal — notification failure should never mask the original error
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
recipe: recipe.name,
|
|
550
|
+
stepsRun,
|
|
551
|
+
outputs,
|
|
552
|
+
context: ctx,
|
|
553
|
+
stepResults,
|
|
554
|
+
errorMessage: runError,
|
|
555
|
+
...(assertionFailures.length > 0 ? { assertionFailures } : {}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
export async function executeStep(step, ctx, deps) {
|
|
559
|
+
const toolId = step.tool;
|
|
560
|
+
if (!toolId) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
// Check if tool is registered in the new registry
|
|
564
|
+
if (hasTool(toolId)) {
|
|
565
|
+
const tool = getTool(toolId);
|
|
566
|
+
// Build params with template rendering for string values
|
|
567
|
+
const params = {};
|
|
568
|
+
for (const [key, value] of Object.entries(step)) {
|
|
569
|
+
if (key === "tool" || key === "agent" || key === "into")
|
|
570
|
+
continue;
|
|
571
|
+
params[key] = deepRender(value, ctx);
|
|
572
|
+
}
|
|
573
|
+
// Check if mock connector is available for this tool
|
|
574
|
+
if (deps.mockConnectors?.[toolId]) {
|
|
575
|
+
return deps.mockConnectors[toolId].invoke("execute", params);
|
|
576
|
+
}
|
|
577
|
+
if (tool &&
|
|
578
|
+
deps.recordFixturesDir &&
|
|
579
|
+
tool.namespace !== "file" &&
|
|
580
|
+
tool.namespace !== "git" &&
|
|
581
|
+
tool.namespace !== "diagnostics") {
|
|
582
|
+
return captureFixture(path.join(deps.recordFixturesDir, `${tool.namespace}.json`), tool.namespace, toolId.split(".")[1] ?? toolId, params, async () => executeTool(toolId, { params, step, ctx, deps }));
|
|
583
|
+
}
|
|
584
|
+
return executeTool(toolId, { params, step, ctx, deps });
|
|
585
|
+
}
|
|
586
|
+
// Unknown tool — skip, don't throw (forward compat)
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
/** Minimal `{{ expr }}` renderer — flat keys and dot-notation paths. */
|
|
590
|
+
export function render(template, ctx) {
|
|
591
|
+
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_, expr) => {
|
|
592
|
+
const key = expr.trim();
|
|
593
|
+
const coerce = (v) => {
|
|
594
|
+
if (v == null)
|
|
595
|
+
return "";
|
|
596
|
+
if (typeof v === "object")
|
|
597
|
+
return JSON.stringify(v);
|
|
598
|
+
return String(v);
|
|
599
|
+
};
|
|
600
|
+
// Fast path: flat key exists
|
|
601
|
+
if (Object.hasOwn(ctx, key))
|
|
602
|
+
return coerce(ctx[key]);
|
|
603
|
+
// Dot-notation: resolve nested path into ctx values (JSON-parse string intermediates)
|
|
604
|
+
const parts = key.split(".");
|
|
605
|
+
// biome-ignore lint/suspicious/noExplicitAny: resolved values are dynamic JSON shapes
|
|
606
|
+
let val = ctx;
|
|
607
|
+
for (const part of parts) {
|
|
608
|
+
if (val == null)
|
|
609
|
+
return "";
|
|
610
|
+
if (typeof val === "string") {
|
|
611
|
+
try {
|
|
612
|
+
val = JSON.parse(val);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return "";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (typeof val !== "object")
|
|
619
|
+
return "";
|
|
620
|
+
// Object.hasOwn — bracket access on a Record walks the prototype chain,
|
|
621
|
+
// which would expose Object.prototype members (toString, constructor,
|
|
622
|
+
// etc.) to attacker-controllable template paths. String(toString)
|
|
623
|
+
// renders the function source and leaks it into recipe output.
|
|
624
|
+
const obj = val;
|
|
625
|
+
val = Object.hasOwn(obj, part) ? obj[part] : undefined;
|
|
626
|
+
}
|
|
627
|
+
return val == null
|
|
628
|
+
? ""
|
|
629
|
+
: typeof val === "object"
|
|
630
|
+
? JSON.stringify(val)
|
|
631
|
+
: String(val);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/** Recursively render all string leaves in a value (for nested params like blocks). */
|
|
635
|
+
function deepRender(value, ctx) {
|
|
636
|
+
if (typeof value === "string")
|
|
637
|
+
return render(value, ctx);
|
|
638
|
+
if (Array.isArray(value))
|
|
639
|
+
return value.map((v) => deepRender(v, ctx));
|
|
640
|
+
if (value !== null && typeof value === "object") {
|
|
641
|
+
const out = {};
|
|
642
|
+
for (const [k, v] of Object.entries(value)) {
|
|
643
|
+
out[k] = deepRender(v, ctx);
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
function parseSinceToGitArg(since) {
|
|
650
|
+
const m = /^(\d+)(h|d)$/i.exec(since.trim());
|
|
651
|
+
if (!m)
|
|
652
|
+
return since;
|
|
653
|
+
const [, num, unit = "h"] = m;
|
|
654
|
+
return unit.toLowerCase() === "h" ? `${num} hours ago` : `${num} days ago`;
|
|
655
|
+
}
|
|
656
|
+
// Exported for test coverage of the regression fix (was returning the
|
|
657
|
+
// `(git log unavailable)` placeholder string on any failure, which
|
|
658
|
+
// silently looked like success to pre-#72 runners).
|
|
659
|
+
export function defaultGitLogSince(since, workdir) {
|
|
660
|
+
// Same antipattern that broke `defaultGitStaleBranches` (PR #70): on
|
|
661
|
+
// any error this used to return `(git log unavailable)`. The runner
|
|
662
|
+
// saw that as success-with-empty-data and downstream agents
|
|
663
|
+
// summarized "no recent commits" — false signal.
|
|
664
|
+
//
|
|
665
|
+
// Fix: return a JSON `{ok: false, error}` shape on failure so the
|
|
666
|
+
// runner's existing JSON-error detection (yamlRunner step-error
|
|
667
|
+
// block) flags the step as `error`. Successful runs still return
|
|
668
|
+
// bare git output text.
|
|
669
|
+
try {
|
|
670
|
+
const sinceArg = parseSinceToGitArg(since);
|
|
671
|
+
const result = spawnSync("git", ["log", "--oneline", `--since=${sinceArg}`], {
|
|
672
|
+
cwd: workdir ?? process.cwd(),
|
|
673
|
+
encoding: "utf-8",
|
|
674
|
+
timeout: 5000,
|
|
675
|
+
});
|
|
676
|
+
if (result.error) {
|
|
677
|
+
return JSON.stringify({
|
|
678
|
+
ok: false,
|
|
679
|
+
error: `git log failed: ${result.error.message}`,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (result.status !== 0) {
|
|
683
|
+
const stderr = (result.stderr ?? "").toString().trim().slice(0, 200);
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
ok: false,
|
|
686
|
+
error: `git log exited ${result.status}${stderr ? `: ${stderr}` : ""}`,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return (result.stdout ?? "").trim();
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
return JSON.stringify({
|
|
693
|
+
ok: false,
|
|
694
|
+
error: `git log threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Exported for test coverage of the regression fix (was using `git branch
|
|
699
|
+
// --since=<date>` which isn't a real flag).
|
|
700
|
+
export function defaultGitStaleBranches(days, workdir) {
|
|
701
|
+
// Two bugs were caught dogfooding the `branch-health` recipe:
|
|
702
|
+
// 1) `git branch --since=<date>` is NOT a valid flag — git exits 129
|
|
703
|
+
// with "unknown option `since=...`". The function used to ALWAYS
|
|
704
|
+
// fall through to the "(git branches unavailable)" placeholder.
|
|
705
|
+
// 2) Even if `--since` had been a real flag, its semantics ("commits
|
|
706
|
+
// since") would have produced the OPPOSITE list of what
|
|
707
|
+
// "stale_branches" implies — branches with recent activity, not
|
|
708
|
+
// ones that have gone quiet.
|
|
709
|
+
//
|
|
710
|
+
// Fix: use `git for-each-ref` with a `committerdate` format, parse the
|
|
711
|
+
// ISO date in JS, and emit branches whose last commit is OLDER than
|
|
712
|
+
// the cutoff. Output is one per line: `<short-name> <YYYY-MM-DD>`.
|
|
713
|
+
try {
|
|
714
|
+
const cutoffMs = Date.now() - days * 86_400_000;
|
|
715
|
+
const r = spawnSync("git", [
|
|
716
|
+
"for-each-ref",
|
|
717
|
+
"--sort=committerdate",
|
|
718
|
+
"--format=%(refname:short)\t%(committerdate:iso-strict)",
|
|
719
|
+
"refs/heads/",
|
|
720
|
+
], {
|
|
721
|
+
cwd: workdir ?? process.cwd(),
|
|
722
|
+
encoding: "utf-8",
|
|
723
|
+
timeout: 5000,
|
|
724
|
+
});
|
|
725
|
+
if (r.error || r.status !== 0)
|
|
726
|
+
return "(git branches unavailable)";
|
|
727
|
+
const lines = (r.stdout ?? "").split("\n").filter(Boolean);
|
|
728
|
+
const stale = [];
|
|
729
|
+
for (const line of lines) {
|
|
730
|
+
const tab = line.indexOf("\t");
|
|
731
|
+
if (tab < 0)
|
|
732
|
+
continue;
|
|
733
|
+
const name = line.slice(0, tab);
|
|
734
|
+
const dateStr = line.slice(tab + 1);
|
|
735
|
+
const ts = Date.parse(dateStr);
|
|
736
|
+
if (Number.isNaN(ts))
|
|
737
|
+
continue;
|
|
738
|
+
if (ts < cutoffMs) {
|
|
739
|
+
stale.push(`${name}\t${dateStr.slice(0, 10)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (stale.length === 0) {
|
|
743
|
+
return `(no branches inactive >${days}d)`;
|
|
744
|
+
}
|
|
745
|
+
return stale.join("\n");
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return "(git branches unavailable)";
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** Resolve all RunnerDeps to concrete StepDeps with production defaults filled in. */
|
|
752
|
+
function resolveStepDeps(deps) {
|
|
753
|
+
const workdir = deps.workdir ?? process.cwd();
|
|
754
|
+
// Defense-in-depth: even if a file.* tool somehow forgets to call
|
|
755
|
+
// resolveRecipePath in its execute(), the default StepDeps file ops will
|
|
756
|
+
// jail the path before touching the filesystem (G-security F-01 / R2 C-1
|
|
757
|
+
// chained-runner third-substitution-site coverage).
|
|
758
|
+
return {
|
|
759
|
+
readFile: deps.readFile ??
|
|
760
|
+
((p) => readFileSync(resolveRecipePath(p, { workspace: workdir }), "utf-8")),
|
|
761
|
+
writeFile: deps.writeFile ??
|
|
762
|
+
((p, content) => {
|
|
763
|
+
const abs = resolveRecipePath(p, { workspace: workdir, write: true });
|
|
764
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
765
|
+
writeFileSync(abs, content);
|
|
766
|
+
}),
|
|
767
|
+
appendFile: deps.appendFile ??
|
|
768
|
+
((p, content) => {
|
|
769
|
+
const abs = resolveRecipePath(p, { workspace: workdir, write: true });
|
|
770
|
+
mkdirSync(path.dirname(abs), { recursive: true });
|
|
771
|
+
appendFileSync(abs, content);
|
|
772
|
+
}),
|
|
773
|
+
mkdir: deps.mkdir ??
|
|
774
|
+
((p) => mkdirSync(resolveRecipePath(p, { workspace: workdir, write: true }), {
|
|
775
|
+
recursive: true,
|
|
776
|
+
})),
|
|
777
|
+
workdir,
|
|
778
|
+
gitLogSince: deps.gitLogSince ?? defaultGitLogSince,
|
|
779
|
+
gitStaleBranches: deps.gitStaleBranches ?? defaultGitStaleBranches,
|
|
780
|
+
// The `diagnostics.get` recipe tool is registered (src/recipes/tools/
|
|
781
|
+
// diagnostics.ts) but only meaningful when the bridge wires a real
|
|
782
|
+
// `getDiagnostics` impl backed by the LSP / extension client. CLI runs
|
|
783
|
+
// and tests have no bridge to ask, so the default returns a JSON error
|
|
784
|
+
// shape that the step-error detector flags as `error` instead of the
|
|
785
|
+
// pre-fix empty string that silently passed as success.
|
|
786
|
+
getDiagnostics: deps.getDiagnostics ??
|
|
787
|
+
(() => JSON.stringify({
|
|
788
|
+
ok: false,
|
|
789
|
+
error: "diagnostics.get unavailable (no bridge / no `deps.getDiagnostics` injected)",
|
|
790
|
+
})),
|
|
791
|
+
fetchFn: deps.fetchFn ?? globalThis.fetch,
|
|
792
|
+
claudeFn: deps.claudeFn ?? defaultClaudeFn,
|
|
793
|
+
claudeCodeFn: deps.claudeCodeFn ?? defaultClaudeCodeFn,
|
|
794
|
+
localFn: deps.localFn ?? defaultLocalFn,
|
|
795
|
+
providerDriverFn: deps.providerDriverFn ?? makeProviderDriverFn(),
|
|
796
|
+
mockConnectors: deps.mockConnectors ?? {},
|
|
797
|
+
recordFixturesDir: deps.recordFixturesDir,
|
|
798
|
+
getGmailToken: deps.getGmailToken ??
|
|
799
|
+
(async () => {
|
|
800
|
+
const { getValidAccessToken } = await import("../connectors/gmail.js");
|
|
801
|
+
return getValidAccessToken();
|
|
802
|
+
}),
|
|
803
|
+
getDriveToken: deps.getDriveToken ??
|
|
804
|
+
(async () => {
|
|
805
|
+
const { getValidAccessToken } = await import("../connectors/googleDrive.js");
|
|
806
|
+
return getValidAccessToken();
|
|
807
|
+
}),
|
|
808
|
+
logDir: deps.logDir,
|
|
809
|
+
testMode: deps.testMode ?? false,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride) {
|
|
813
|
+
const claudeCliFn = claudeCodeFnOverride ?? stepDeps.claudeCodeFn;
|
|
814
|
+
return {
|
|
815
|
+
anthropicFn: (prompt, model) => stepDeps.claudeFn(prompt, model),
|
|
816
|
+
providerDriverFn: (driver, prompt, model) => stepDeps.providerDriverFn(driver, prompt, model),
|
|
817
|
+
claudeCliFn: (prompt) => claudeCliFn(prompt),
|
|
818
|
+
localFn: (prompt, model) => stepDeps.localFn(prompt, model),
|
|
819
|
+
probeClaudeCli: () => {
|
|
820
|
+
if (runnerDeps.claudeFn !== undefined)
|
|
821
|
+
return false;
|
|
822
|
+
const probe = spawnSync("claude", ["--version"], {
|
|
823
|
+
encoding: "utf-8",
|
|
824
|
+
timeout: 5000,
|
|
825
|
+
});
|
|
826
|
+
return !probe.error;
|
|
827
|
+
},
|
|
828
|
+
loadPatchworkConfig: () => {
|
|
829
|
+
// Synchronous static import — earlier `require()` form silently failed
|
|
830
|
+
// under "type": "module" and returned {}, dropping config-driven
|
|
831
|
+
// model/driver preferences for no-driver agent steps.
|
|
832
|
+
try {
|
|
833
|
+
return loadPatchworkConfigSync();
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
return {};
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
};
|
|
165
840
|
}
|
|
166
841
|
function defaultClaudeCodeFn(prompt) {
|
|
167
842
|
try {
|
|
@@ -188,6 +863,51 @@ function defaultClaudeCodeFn(prompt) {
|
|
|
188
863
|
return Promise.resolve(`[agent step failed: ${err instanceof Error ? err.message : String(err)}]`);
|
|
189
864
|
}
|
|
190
865
|
}
|
|
866
|
+
/** Returns a providerDriverFn with a per-run driver cache (not shared across runs). */
|
|
867
|
+
function makeProviderDriverFn() {
|
|
868
|
+
const cache = new Map();
|
|
869
|
+
return async function defaultProviderDriverFn(driverName, prompt, model) {
|
|
870
|
+
try {
|
|
871
|
+
let driver = cache.get(driverName);
|
|
872
|
+
if (!driver) {
|
|
873
|
+
const { createDriver } = await import("../drivers/index.js");
|
|
874
|
+
const d = createDriver(driverName, { binary: "claude", antBinary: "ant" }, () => { });
|
|
875
|
+
if (!d)
|
|
876
|
+
return `[agent step failed: ${driverName} driver returned null]`;
|
|
877
|
+
driver = d;
|
|
878
|
+
cache.set(driverName, driver);
|
|
879
|
+
}
|
|
880
|
+
const controller = new AbortController();
|
|
881
|
+
const timeoutMs = 300_000;
|
|
882
|
+
const startupTimeoutMs = 30_000;
|
|
883
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
884
|
+
try {
|
|
885
|
+
const result = await driver.run({
|
|
886
|
+
prompt,
|
|
887
|
+
workspace: process.cwd(),
|
|
888
|
+
timeoutMs,
|
|
889
|
+
startupTimeoutMs,
|
|
890
|
+
signal: controller.signal,
|
|
891
|
+
model,
|
|
892
|
+
});
|
|
893
|
+
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
894
|
+
const detail = result.stderrTail ?? result.text ?? "";
|
|
895
|
+
return `[agent step failed: ${driverName} exited ${result.exitCode}${detail ? ` — ${detail.slice(0, 200)}` : ""}]`;
|
|
896
|
+
}
|
|
897
|
+
if (!result.text) {
|
|
898
|
+
return `[agent step failed: ${driverName} returned empty output (possible timeout or auth error)]`;
|
|
899
|
+
}
|
|
900
|
+
return result.text;
|
|
901
|
+
}
|
|
902
|
+
finally {
|
|
903
|
+
clearTimeout(timeout);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
return `[agent step failed: ${err instanceof Error ? err.message : String(err)}]`;
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
191
911
|
async function defaultClaudeFn(prompt, model) {
|
|
192
912
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
193
913
|
if (!apiKey)
|
|
@@ -222,338 +942,217 @@ async function defaultClaudeFn(prompt, model) {
|
|
|
222
942
|
return `[agent step failed: ${err instanceof Error ? err.message : String(err)}]`;
|
|
223
943
|
}
|
|
224
944
|
}
|
|
225
|
-
async function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
945
|
+
async function defaultLocalFn(prompt, model) {
|
|
946
|
+
try {
|
|
947
|
+
const { createLocalAdapter } = await import("../adapters/local.js");
|
|
948
|
+
const { loadConfig: loadPatchworkConfig } = await import("../patchworkConfig.js");
|
|
949
|
+
const cfg = loadPatchworkConfig();
|
|
950
|
+
const adapter = createLocalAdapter({
|
|
951
|
+
endpoint: cfg.localEndpoint,
|
|
952
|
+
defaultModel: cfg.localModel ?? model,
|
|
953
|
+
});
|
|
954
|
+
const result = await adapter.complete({
|
|
955
|
+
systemPrompt: "",
|
|
956
|
+
messages: [{ role: "user", content: prompt }],
|
|
957
|
+
});
|
|
958
|
+
return result.text ?? "[agent step failed: empty response from local LLM]";
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
return `[agent step failed: ${err instanceof Error ? err.message : String(err)}]`;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Build ExecutionDeps for ChainedRecipeRunner backed by the yamlRunner step
|
|
966
|
+
* handlers. This lets chained recipes use the same tool set (file.*, git.*,
|
|
967
|
+
* gmail.*, github.*, linear.*, diagnostics.*) as simple YAML recipes.
|
|
968
|
+
*
|
|
969
|
+
* Pass the result as `chainedDeps` when calling `dispatchRecipe` or
|
|
970
|
+
* `runChainedRecipe` so that `executeTool` is properly wired.
|
|
971
|
+
*/
|
|
972
|
+
export function buildChainedDeps(runnerDeps, claudeCodeFnOverride) {
|
|
973
|
+
const stepDeps = resolveStepDeps(runnerDeps);
|
|
974
|
+
function normalizeNestedRecipeLookupName(ref) {
|
|
975
|
+
return ref.trim().replace(/\.ya?ml$/i, "");
|
|
976
|
+
}
|
|
977
|
+
function tryLoadRecipeFile(filePath) {
|
|
978
|
+
if (!existsSync(filePath))
|
|
979
|
+
return null;
|
|
980
|
+
try {
|
|
981
|
+
const recipe = loadYamlRecipe(filePath);
|
|
982
|
+
return { recipe, sourcePath: filePath };
|
|
237
983
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return JSON.stringify({
|
|
330
|
-
count: 0,
|
|
331
|
-
issues: [],
|
|
332
|
-
error: err instanceof Error ? err.message : String(err),
|
|
333
|
-
});
|
|
984
|
+
catch {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const executeTool = async (tool, params) => {
|
|
989
|
+
// R2 C-1 third-substitution-site coverage: the chained runner has its
|
|
990
|
+
// own template-resolution path (`chainedRunner.ts:194-205`). By the
|
|
991
|
+
// time we reach this dispatch point the params have been rendered
|
|
992
|
+
// *and* JSON-parsed, so a `path` field that survived the chained
|
|
993
|
+
// substitution may have just been promoted from inside-jail to
|
|
994
|
+
// outside-jail. Re-jail any `path` field on file.* tools here so that
|
|
995
|
+
// chained sub-recipes can't bypass the per-tool jail in `tools/file.ts`
|
|
996
|
+
// by injecting `..` segments via outer-recipe vars.
|
|
997
|
+
if ((tool === "file.read" ||
|
|
998
|
+
tool === "file.write" ||
|
|
999
|
+
tool === "file.append") &&
|
|
1000
|
+
typeof params.path === "string") {
|
|
1001
|
+
params = {
|
|
1002
|
+
...params,
|
|
1003
|
+
path: resolveRecipePath(params.path, {
|
|
1004
|
+
workspace: stepDeps.workdir,
|
|
1005
|
+
write: tool !== "file.read",
|
|
1006
|
+
}),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
// Construct a YamlStep-compatible object so we can reuse executeStep.
|
|
1010
|
+
const step = { tool, ...params };
|
|
1011
|
+
// executeStep uses a RunContext for {{}} rendering — by the time executeTool
|
|
1012
|
+
// is called the chained runner has already resolved templates, so we pass
|
|
1013
|
+
// an empty context (no double-rendering).
|
|
1014
|
+
const result = await executeStep(step, {}, stepDeps);
|
|
1015
|
+
return result ?? "";
|
|
1016
|
+
};
|
|
1017
|
+
const executeAgent = async (prompt, model, driver) => _executeAgent({ prompt, model, driver }, buildAgentExecutorDeps(stepDeps, runnerDeps, claudeCodeFnOverride));
|
|
1018
|
+
// ---------------------------------------------------------------------
|
|
1019
|
+
// BEGIN A-PR2 EDIT BLOCK — `loadNestedRecipe` jail (dogfood F-04).
|
|
1020
|
+
//
|
|
1021
|
+
// Path-shaped recipe references (`recipe: ./inner.yaml`, `recipe: /abs.yaml`)
|
|
1022
|
+
// are restricted to three allowed roots:
|
|
1023
|
+
// 1. parent recipe's directory (`path.dirname(parentSourcePath)`)
|
|
1024
|
+
// 2. user recipes dir (`~/.patchwork/recipes/`)
|
|
1025
|
+
// 3. bundled templates dir (`BUNDLED_TEMPLATES_DIR`, captured at boot)
|
|
1026
|
+
//
|
|
1027
|
+
// Resolved candidates that escape all three (e.g. `/etc/passwd.yaml`) are
|
|
1028
|
+
// rejected with `null` — same shape as a not-found lookup so the chained
|
|
1029
|
+
// runner reports its existing "nested_recipe_not_found" error rather than
|
|
1030
|
+
// surfacing a security-implementation detail to the recipe author.
|
|
1031
|
+
//
|
|
1032
|
+
// Coordination note (A-PR1 may also touch this file): the helper
|
|
1033
|
+
// `pathIsWithin` below is local to this module — A-PR1 is changing
|
|
1034
|
+
// unrelated `vars` validation paths and should not collide here. If a merge
|
|
1035
|
+
// conflict surfaces, keep BOTH the jail AND the A-PR1 vars validation.
|
|
1036
|
+
// ---------------------------------------------------------------------
|
|
1037
|
+
const pathIsWithin = (candidate, base) => {
|
|
1038
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
1039
|
+
const resolvedBase = path.resolve(base);
|
|
1040
|
+
if (resolvedCandidate === resolvedBase)
|
|
1041
|
+
return true;
|
|
1042
|
+
return resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
1043
|
+
};
|
|
1044
|
+
const loadNestedRecipe = async (name, parentSourcePath) => {
|
|
1045
|
+
const lookupName = normalizeNestedRecipeLookupName(name);
|
|
1046
|
+
const { homedir: nestedHomedir } = await import("node:os");
|
|
1047
|
+
const userRecipesDir = path.join(nestedHomedir(), ".patchwork", "recipes");
|
|
1048
|
+
if (parentSourcePath) {
|
|
1049
|
+
const parentDir = path.dirname(parentSourcePath);
|
|
1050
|
+
const pathLike = path.isAbsolute(name) ||
|
|
1051
|
+
name.startsWith("./") ||
|
|
1052
|
+
name.startsWith("../") ||
|
|
1053
|
+
/[\\/]/.test(name) ||
|
|
1054
|
+
/\.ya?ml$/i.test(name);
|
|
1055
|
+
if (pathLike) {
|
|
1056
|
+
const resolvedBase = path.isAbsolute(name)
|
|
1057
|
+
? path.resolve(name)
|
|
1058
|
+
: path.resolve(parentDir, name);
|
|
1059
|
+
const candidates = /\.ya?ml$/i.test(resolvedBase)
|
|
1060
|
+
? [resolvedBase]
|
|
1061
|
+
: [`${resolvedBase}.yaml`, `${resolvedBase}.yml`, resolvedBase];
|
|
1062
|
+
// Jail: every candidate must live inside one of the three allowed
|
|
1063
|
+
// roots (parent dir, user recipes, bundled templates). Reject silently
|
|
1064
|
+
// — null mirrors the existing not-found path so error messages stay
|
|
1065
|
+
// generic and don't leak the jail boundaries.
|
|
1066
|
+
const allowedRoots = [parentDir, userRecipesDir, BUNDLED_TEMPLATES_DIR];
|
|
1067
|
+
for (const candidate of candidates) {
|
|
1068
|
+
const inJail = allowedRoots.some((root) => pathIsWithin(candidate, root));
|
|
1069
|
+
if (!inJail)
|
|
1070
|
+
continue;
|
|
1071
|
+
const loaded = tryLoadRecipeFile(candidate);
|
|
1072
|
+
if (loaded)
|
|
1073
|
+
return loaded;
|
|
1074
|
+
}
|
|
334
1075
|
}
|
|
335
1076
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1077
|
+
// END A-PR2 EDIT BLOCK
|
|
1078
|
+
// Reuses `userRecipesDir` already resolved above for the jail check.
|
|
1079
|
+
const recipesDir = userRecipesDir;
|
|
1080
|
+
// Check for manifest-based package directory first.
|
|
1081
|
+
// Supports both plain names ("morning-brief") and scoped names ("@acme/morning-brief").
|
|
1082
|
+
const pkgDirCandidates = [
|
|
1083
|
+
path.join(recipesDir, lookupName),
|
|
1084
|
+
// scoped: @acme/morning-brief → recipesDir/@acme/morning-brief
|
|
1085
|
+
];
|
|
1086
|
+
for (const pkgDir of pkgDirCandidates) {
|
|
343
1087
|
try {
|
|
344
|
-
const
|
|
345
|
-
|
|
1088
|
+
const { loadManifestFromDir } = await import("./manifest.js");
|
|
1089
|
+
const manifest = loadManifestFromDir(pkgDir);
|
|
1090
|
+
if (manifest) {
|
|
1091
|
+
const mainPath = path.join(pkgDir, manifest.recipes.main);
|
|
1092
|
+
const loaded = tryLoadRecipeFile(mainPath);
|
|
1093
|
+
if (loaded)
|
|
1094
|
+
return loaded;
|
|
1095
|
+
}
|
|
346
1096
|
}
|
|
347
|
-
catch
|
|
348
|
-
|
|
349
|
-
count: 0,
|
|
350
|
-
events: [],
|
|
351
|
-
error: err instanceof Error ? err.message : String(err),
|
|
352
|
-
});
|
|
1097
|
+
catch {
|
|
1098
|
+
// not a manifest dir — try flat file candidates
|
|
353
1099
|
}
|
|
354
1100
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return Object.hasOwn(ctx, key) ? (ctx[key] ?? "") : "";
|
|
365
|
-
});
|
|
1101
|
+
const candidate = findYamlRecipePath(recipesDir, lookupName);
|
|
1102
|
+
if (candidate) {
|
|
1103
|
+
const loaded = tryLoadRecipeFile(candidate);
|
|
1104
|
+
if (loaded)
|
|
1105
|
+
return loaded;
|
|
1106
|
+
}
|
|
1107
|
+
return null;
|
|
1108
|
+
};
|
|
1109
|
+
return { executeTool, executeAgent, loadNestedRecipe };
|
|
366
1110
|
}
|
|
367
1111
|
/**
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
1112
|
+
* Dispatch a loaded recipe to the appropriate runner.
|
|
1113
|
+
*
|
|
1114
|
+
* Recipes with `trigger.type: "chained"` are routed to the ChainedRecipeRunner
|
|
1115
|
+
* (parallel execution, template variables, nested recipes, dry-run).
|
|
1116
|
+
* All other recipes use the existing synchronous yamlRunner path.
|
|
1117
|
+
*
|
|
1118
|
+
* `chainedDeps` is only required when the recipe is chained; omit for simple recipes.
|
|
371
1119
|
*/
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return l === r;
|
|
401
|
-
case "!=":
|
|
402
|
-
return l !== r;
|
|
403
|
-
default:
|
|
404
|
-
return true;
|
|
1120
|
+
export async function dispatchRecipe(recipe, deps, seedContext = {}) {
|
|
1121
|
+
const triggerType = recipe.trigger
|
|
1122
|
+
?.type;
|
|
1123
|
+
if (triggerType === "chained") {
|
|
1124
|
+
const { runChainedRecipe } = await import("./chainedRunner.js");
|
|
1125
|
+
const chainedRecipe = recipe;
|
|
1126
|
+
const now = deps.now ? deps.now() : new Date();
|
|
1127
|
+
const options = {
|
|
1128
|
+
env: {
|
|
1129
|
+
...process.env,
|
|
1130
|
+
DATE: now.toISOString().slice(0, 10),
|
|
1131
|
+
TIME: now.toTimeString().slice(0, 5),
|
|
1132
|
+
...seedContext,
|
|
1133
|
+
},
|
|
1134
|
+
maxConcurrency: chainedRecipe.maxConcurrency ?? 4,
|
|
1135
|
+
maxDepth: chainedRecipe.maxDepth ?? 3,
|
|
1136
|
+
dryRun: deps.chainedOptions?.dryRun ?? false,
|
|
1137
|
+
sourcePath: deps.chainedOptions?.sourcePath,
|
|
1138
|
+
onStepStart: deps.chainedOptions?.onStepStart,
|
|
1139
|
+
onStepComplete: deps.chainedOptions?.onStepComplete,
|
|
1140
|
+
runLogDir: deps.chainedOptions?.runLogDir,
|
|
1141
|
+
runLog: deps.chainedOptions?.runLog,
|
|
1142
|
+
activityLog: deps.chainedOptions?.activityLog,
|
|
1143
|
+
mockedOutputs: deps.chainedOptions?.mockedOutputs,
|
|
1144
|
+
taskIdPrefix: deps.chainedOptions?.taskIdPrefix,
|
|
1145
|
+
};
|
|
1146
|
+
if (!deps.chainedDeps) {
|
|
1147
|
+
throw new Error("chainedDeps required for chained recipes (provide executeTool, executeAgent, loadNestedRecipe)");
|
|
405
1148
|
}
|
|
1149
|
+
return runChainedRecipe(chainedRecipe, options, deps.chainedDeps);
|
|
406
1150
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// "24h" → "1d", "7d" → "7d", "1h" → "1d" (round up)
|
|
413
|
-
const m = /^(\d+)(h|d)$/.exec(since.trim().toLowerCase());
|
|
414
|
-
if (!m)
|
|
415
|
-
return "1d";
|
|
416
|
-
const [, num, unit] = m;
|
|
417
|
-
if (unit === "d")
|
|
418
|
-
return `${num}d`;
|
|
419
|
-
// hours → round up to days (min 1d)
|
|
420
|
-
const days = Math.max(1, Math.ceil(Number(num) / 24));
|
|
421
|
-
return `${days}d`;
|
|
422
|
-
}
|
|
423
|
-
function getHeader(headers, name) {
|
|
424
|
-
return (headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ??
|
|
425
|
-
"");
|
|
426
|
-
}
|
|
427
|
-
async function gmailSearch(query, max, deps) {
|
|
428
|
-
const errorResult = (msg) => JSON.stringify({ count: 0, messages: [], error: msg });
|
|
429
|
-
let token;
|
|
430
|
-
try {
|
|
431
|
-
token = await deps.getGmailToken();
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
return errorResult("Gmail not connected");
|
|
435
|
-
}
|
|
436
|
-
try {
|
|
437
|
-
const listUrl = `https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${encodeURIComponent(query)}&maxResults=${max}`;
|
|
438
|
-
const listRes = await deps.fetchFn(listUrl, {
|
|
439
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
440
|
-
});
|
|
441
|
-
if (!listRes.ok)
|
|
442
|
-
return errorResult("Gmail API error");
|
|
443
|
-
const listJson = (await listRes.json());
|
|
444
|
-
const ids = listJson.messages ?? [];
|
|
445
|
-
const messages = await Promise.all(ids.slice(0, max).map(async (m) => {
|
|
446
|
-
const detailUrl = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${m.id}?format=metadata&metadataHeaders=Subject,From,Date`;
|
|
447
|
-
const detailRes = await deps.fetchFn(detailUrl, {
|
|
448
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
449
|
-
});
|
|
450
|
-
if (!detailRes.ok)
|
|
451
|
-
return { id: m.id, subject: "", from: "", date: "", snippet: "" };
|
|
452
|
-
const detail = (await detailRes.json());
|
|
453
|
-
const hdrs = detail.payload?.headers ?? [];
|
|
454
|
-
return {
|
|
455
|
-
id: detail.id,
|
|
456
|
-
subject: getHeader(hdrs, "Subject"),
|
|
457
|
-
from: getHeader(hdrs, "From"),
|
|
458
|
-
date: getHeader(hdrs, "Date"),
|
|
459
|
-
snippet: detail.snippet ?? "",
|
|
460
|
-
};
|
|
461
|
-
}));
|
|
462
|
-
const result = { count: messages.length, messages };
|
|
463
|
-
return JSON.stringify(result);
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
return errorResult("Gmail fetch failed");
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
async function gmailFetchThread(id, deps) {
|
|
470
|
-
const errorResult = (msg) => JSON.stringify({ subject: "", messages: [], error: msg });
|
|
471
|
-
let token;
|
|
472
|
-
try {
|
|
473
|
-
token = await deps.getGmailToken();
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
return errorResult("Gmail not connected");
|
|
477
|
-
}
|
|
478
|
-
try {
|
|
479
|
-
const url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${id}?format=metadata&metadataHeaders=Subject,From,Date`;
|
|
480
|
-
const res = await deps.fetchFn(url, {
|
|
481
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
482
|
-
});
|
|
483
|
-
if (!res.ok)
|
|
484
|
-
return errorResult("Gmail API error");
|
|
485
|
-
const thread = (await res.json());
|
|
486
|
-
const msgs = thread.messages ?? [];
|
|
487
|
-
const firstHdrs = msgs[0]?.payload?.headers ?? [];
|
|
488
|
-
const subject = getHeader(firstHdrs, "Subject");
|
|
489
|
-
const messages = msgs.map((m) => {
|
|
490
|
-
const hdrs = m.payload?.headers ?? [];
|
|
491
|
-
return {
|
|
492
|
-
from: getHeader(hdrs, "From"),
|
|
493
|
-
date: getHeader(hdrs, "Date"),
|
|
494
|
-
body_snippet: m.snippet ?? "",
|
|
495
|
-
};
|
|
496
|
-
});
|
|
497
|
-
const result = { subject, messages };
|
|
498
|
-
return JSON.stringify(result);
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
return errorResult("Gmail fetch failed");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
function expandHome(p) {
|
|
505
|
-
if (p.startsWith("~/"))
|
|
506
|
-
return path.join(os.homedir(), p.slice(2));
|
|
507
|
-
return p;
|
|
508
|
-
}
|
|
509
|
-
function parseSinceToGitArg(since) {
|
|
510
|
-
const m = /^(\d+)(h|d)$/i.exec(since.trim());
|
|
511
|
-
if (!m)
|
|
512
|
-
return since;
|
|
513
|
-
const [, num, unit = "h"] = m;
|
|
514
|
-
return unit.toLowerCase() === "h" ? `${num} hours ago` : `${num} days ago`;
|
|
515
|
-
}
|
|
516
|
-
function defaultGitLogSince(since, workdir) {
|
|
517
|
-
try {
|
|
518
|
-
const sinceArg = parseSinceToGitArg(since);
|
|
519
|
-
const result = spawnSync("git", ["log", "--oneline", `--since=${sinceArg}`], {
|
|
520
|
-
cwd: workdir ?? process.cwd(),
|
|
521
|
-
encoding: "utf-8",
|
|
522
|
-
timeout: 5000,
|
|
523
|
-
});
|
|
524
|
-
if (result.error || result.status !== 0)
|
|
525
|
-
return "(git log unavailable)";
|
|
526
|
-
return (result.stdout ?? "").trim();
|
|
527
|
-
}
|
|
528
|
-
catch {
|
|
529
|
-
return "(git log unavailable)";
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
function defaultGitStaleBranches(days, workdir) {
|
|
533
|
-
try {
|
|
534
|
-
const cutoff = new Date(Date.now() - days * 86_400_000)
|
|
535
|
-
.toISOString()
|
|
536
|
-
.slice(0, 10);
|
|
537
|
-
const r = spawnSync("git", ["branch", "--format=%(refname:short) %(committerdate:short)"], {
|
|
538
|
-
cwd: workdir ?? process.cwd(),
|
|
539
|
-
encoding: "utf-8",
|
|
540
|
-
timeout: 5000,
|
|
541
|
-
});
|
|
542
|
-
const branches = r.error || r.status !== 0 ? "" : (r.stdout ?? "").trim();
|
|
543
|
-
if (!branches)
|
|
544
|
-
return "(no local branches)";
|
|
545
|
-
return (branches
|
|
546
|
-
.split("\n")
|
|
547
|
-
.filter((line) => {
|
|
548
|
-
const parts = line.trim().split(/\s+/);
|
|
549
|
-
const dateStr = parts[1];
|
|
550
|
-
return dateStr && dateStr < cutoff;
|
|
551
|
-
})
|
|
552
|
-
.join("\n") || "(none older than 30 days)");
|
|
553
|
-
}
|
|
554
|
-
catch {
|
|
555
|
-
return "(git unavailable)";
|
|
556
|
-
}
|
|
1151
|
+
// For non-chained recipes, lift `runLog` from chainedOptions onto the
|
|
1152
|
+
// RunnerDeps so runYamlRecipe gets the bridge's singleton too.
|
|
1153
|
+
return runYamlRecipe(recipe, deps.chainedOptions?.runLog
|
|
1154
|
+
? { ...deps, runLog: deps.chainedOptions.runLog }
|
|
1155
|
+
: deps, seedContext);
|
|
557
1156
|
}
|
|
558
1157
|
/** List all YAML recipes in a directory. Returns names. */
|
|
559
1158
|
export function listYamlRecipes(recipesDir) {
|